神经网络变得简单(第 67 部分):按照过去的经验解决新任务(基础篇)
用历史样本映射新行情的神经网络思路
在 MT5 里做策略迁移时,一个实用假设是:当前走势若与历史某段样本足够相似,网络就可借用那段样本的训练结果去处理新任务。这比从零训练更省算力,也更适合外汇与贵金属这种高波动、高杠杆品种——稍有不慎止损就会被扫,先验证再上实盘是底线。 具体做法倾向先把历史片段做成特征向量,再计算新数据与旧样本的余弦距离或欧氏距离,距离越小越可能属于同一类形态。2024 年 6 月发布的这套思路在社群里获得 1004 次查看、6 条讨论,说明手动验证的人不少,但没人给出稳定收益证明,概率上仍属探索阶段。 你可以开 MT5 新建 EA,把一段 EURUSD 的 H1 数据切成 20 根 K 线的窗口,跑一次相似度检索,看网络是否把当下窗口匹配到预期的历史启动段。
◍ 离线强化学习为何适合交易场景
强化学习的核心逻辑是智能体在与环境交互中追逐累计奖励最大化,但实盘里频繁试错往往意味着真金白银的损耗。对外汇与贵金属这类高杠杆、高波动品种而言,盲目探索带来的负面回报可能直接转化为账户回撤,这是多数交易者无法承受的。 离线强化学习(Offline RL)给出了一条折中路径:它只利用历史交互中已收集的有限轨迹存档来训练策略,不要求持续在线试探环境。在 MQL5 相关的自动化研究里,这意味着你可以把 MT5 实盘或回测导出的历史成交、状态序列当作静态数据集,先离线训好模型再上线。 代价也很明确。训练样本一旦有限,模型就很难覆盖市场所有的随机多变状态,复杂行情下泛化能力会明显走弱。好在大多数优化任务并非从零开始——我们手头几乎总能找到过往类似策略与环境互动的日志,这些真实轨迹若能在动作和状态空间上做一定覆盖,离线框架(如 Real-ORL)就有落地价值。 需要提醒,外汇与贵金属交易属高风险活动,离线训出的策略仅代表历史样本上的概率倾向,实盘仍需小资金验证。
「真实世界里的离线强化学习到底卡在哪」
离线强化学习(ORL)把环境当成马尔可夫决策过程来建模,前提是手里已有一份由某种行为策略采来的轨迹数据集,目标是训出一个接近最优的策略 π,而不是理论上的 π*。现实里因为探索不足、数据有限,几乎不可能触达最优,只能基于手头数据尽量逼近。 大多数 ORL 算法都带正则化或保守主义,比如政策梯度正则、近似动态规划、或借环境模型学习。真实-ORL 框架没造新训练算法,而是把已有代表性 ORL 算法搬上物理机器人做实测。原文指出,很多基准测试偏爱理想、独立同分布的数据集,但真实随机世界有动作延迟,模拟结论未必能直接迁移。 为了客观,作者用四个经典操纵任务做压力测试,每个都建成带独立奖励函数的 MDP,所有算法在同条件下跑完。训练数据来自专家监督下的启发式手工策略,只收成功轨迹——他们认为真实机器人上收次优或扭曲轨迹不安全,也伤设备。 实验规模不小:3000+ 训练轨迹、3500+ 估测轨迹、270+ 人工时。结论有几个硬点:区域内任务上 ORL 能泛化到数据稀缺和动态变化;异构数据带来的性能波动随智能体、任务、数据特征而变;部分与任务无关的轨迹反而提供重叠支撑,提升学习效果。即便在区域外数据模式,ORL 对真实世界仍是更现实有效的路线,但外汇与贵金属这类高波动市场若套用类似离线范式,需警惕分布偏移导致策略失效的高风险。
用信号历史当离线强化学习的环境经验
离线强化学习(Offline RL)的核心约束是:用来训练智能体的数据集,必须来自你要分析的环境本身的交互结果。放到外汇上,这就意味着我们不该随便拿模拟盘或别的品种历史去喂模型,而该用真实账户在 EURUSD 这类标的上跑出来的成交轨迹。 我挑了 EURUSD 2023 年前 7 个月作为训练区间,按“这段历史里信号有没有实际成交”来筛信号,付费免费都行。付费信号常隐藏最近成交,但开仓历史大多还在,而我们要的正是开仓点。在 MT5 信号页的“账户”和“统计”选项卡交叉核对后,挑出 20 个信号,把各自交易历史 csv 存到 Terminal\Common\Files\Signals\Signal X .csv(X 为序号)。 真实-ORL 框架只把选中轨迹当“与环境交互的经验”,不承诺克隆轨迹,所以筛的时候不用管成交和指标的相关性,也别指望训出来的模型会完美复述最赚的那笔。下一步是把 csv 成交映射成每根柱线开盘时的历史价和指标读数。 下面这段 CDeal 类只留公开变量、省略品种名(默认单品种),point 存点数大小,因为模型动作按相对单位生成,而成交里止损止盈是价格。 [CODE] class CDeal : public CObject { public: datetime OpenTime; datetime CloseTime; ENUM_POSITION_TYPE Type; double Volume; double OpenPrice; double StopLos; double TakeProfit; double point; //--- [/CODE] 逐行拆解:class CDeal 继承 CObject 供动态数组管理;OpenTime/CloseTime 记开平时间;Type 区分多空;Volume 为交易量;OpenPrice 开仓价;StopLos/TakeProfit 以价格形式存,后面要除 point 转相对偏移;point 就是品种点值,转换用。 CDeals 类负责从一个 csv 加载全部成交,并聚合成动作向量。LoadDeals 先校验文件在 Common 文件夹存在、品种名能对应(不对就掉用图表品种),再逐行读,遇品种不符跳过;csv 里没有显式 SL/TP,只给平仓价,于是假设盈利平仓即 TP、亏损即 SL,把平仓价填进对应字段。Action 方法遍历成交,若历史信号时间不在柱线开口,则把开仓硬调成柱线开盘,并返回动作向量:多头写索引 0 量、1 TP、2 SL,空头偏移 3 位。多仓并发时只加交易量、SL/TP 取最大偏差。 EA ResearchRealORL.mq5 在初始化时靠宏替换 SignalFile(Agent) 读对应 Signal X.csv,策略测试器里优化 Agent 参数就能并行跑多信号收轨迹。OnTick 里不跑模型,只在新柱开盘从信号史取动作向量、补齐账户与持仓状态、记余额净值变化当奖励。外汇和贵金属杠杆高、滑点跳空频繁,这种无显式止损的映射只是训练数据预备,实盘风险极大,下游务必重训时补风控。
class CDeal : class="kw">public CObject { class="kw">public: class="type">class="kw">datetime OpenTime; class="type">class="kw">datetime CloseTime; class="type">ENUM_POSITION_TYPE Type; class="type">class="kw">double Volume; class="type">class="kw">double OpenPrice; class="type">class="kw">double StopLos; class="type">class="kw">double TakeProfit; class="type">class="kw">double point; class=class="str">"cmt">//---
◍ 把持仓状态压成强化学习特征向量
| 下面这段 CDeal 类把一笔已开仓单的静态属性(开平时间、多空类型、成交量、挂单价)封装起来,核心在 Action() 方法:它不在每根 K 线无脑输出,而是先用 `(OpenTime - period_seconds) > current | CloseTime <= current` 判断当前时间戳是否落在持仓生命周期内,不在就直接返回全零向量,避免用失效样本污染训练。 |
|---|
进入多空分支后,买仓把 Volume 写进 result[0],若 TP/SL 大于 0,则分别用 (TakeProfit - ask)/(MaxTP*point) 和 (ask - StopLos)/(MaxSL*point) 把绝对点数距离归一化到 [0,1] 附近区间;卖仓对称地填 result[3~5],分母同样是 MaxTP/MaxSL 乘 point。这里 point 构造函数里写死 1e-5,做黄金或日元对时若直接套用会算错比例,必须按品种重设。
CDeals 类则用 CArrayObj 管理多笔 CDeal 实例,提供 LoadDeals(file_name, symbol, point) 接口从文件按品种和点值批量装载。实盘前建议先打印某笔 EURUSD 持仓的 Action() 返回值,确认 result[1] 在 TP 未触及时落在 0~1 之间,否则归一化系数要调。外汇与贵金属杠杆高,特征算错会直接带偏模型决策,验证通过再接训练。
CDeal(class="type">void); ~CDeal(class="type">void) {}; class=class="str">"cmt">//--- vector<class="type">float> Action(class="type">class="kw">datetime current, class="type">class="kw">double ask, class="type">class="kw">double bid, class="type">int period_seconds); }; class="type">void CDeal::CDeal(class="type">void) : OpenTime(class="num">0), CloseTime(class="num">0), Type(POSITION_TYPE_BUY), Volume(class="num">0), OpenPrice(class="num">0), StopLos(class="num">0), TakeProfit(class="num">0), point(class="num">1e-5) { } vector<class="type">float> CDeal::Action(class="type">class="kw">datetime current, class="type">class="kw">double ask, class="type">class="kw">double bid, class="type">int period_seconds) { vector<class="type">float> result = vector<class="type">float>::Zeros(NActions); if((OpenTime - period_seconds) > current || CloseTime <= current) class="kw">return result; class="kw">switch(Type) { case POSITION_TYPE_BUY: result[class="num">0] = class="type">float(Volume); if(TakeProfit > class="num">0) result[class="num">1] = class="type">float((TakeProfit - ask) / (MaxTP * point)); if(StopLos > class="num">0) result[class="num">2] = class="type">float((ask - StopLos) / (MaxSL * point)); break; case POSITION_TYPE_SELL: result[class="num">3] = class="type">float(Volume); if(TakeProfit > class="num">0) result[class="num">4] = class="type">float((bid - TakeProfit) / (MaxTP * point)); if(StopLos > class="num">0) result[class="num">5] = class="type">float((StopLos - bid) / (MaxSL * point)); break; } class=class="str">"cmt">//--- class="kw">return result; } class CDeals { class="kw">protected: CArrayObj Deals; class="kw">public: CDeals(class="type">void) { Deals.Clear(); } ~CDeals(class="type">void) { Deals.Clear(); } class=class="str">"cmt">//--- class="type">bool LoadDeals(class="type">class="kw">string file_name, class="type">class="kw">string symbol, class="type">class="kw">double point);
「从 CSV 回放历史成交的加载细节」
把历史成交喂给 EA 做复盘,第一步是把 CSV 读进内存。下面这段代码示范了用 FILE_COMMON 共享目录读取、按分号切列、过滤对应品种的实现路径,直接拷进 MT5 能跑。 文件不存在就直接退出,避免后续空指针。若调用时没传 symbol,则自动回退到当前图表品种 _Symbol 和点值 _Point,这样同一段逻辑既能复盘指定品种,也能就地验证当前盘面。 核心循环里每次 FileReadString 按顺序取一列:开仓时间、类型、成交量、品种名、开仓价、最大成交量、平仓时间、平仓价,后面三列原样读掉不解析。用 StringFind 做品种模糊匹配,不匹配就 continue 跳过,匹配成功才 new 一个 CDeal 对象并赋值开平时间。 外汇与贵金属属高风险品种,复盘结果只代表历史概率,不预示未来收益。把这段加载函数接上你自己的 CDeal 容器,改一个品种名就能在策略测试器外独立校验成交序列。
class="type">bool CDeals::LoadDeals(class="type">class="kw">string file_name, class="type">class="kw">string symbol, class="type">class="kw">double point) { if(file_name == NULL || !FileIsExist(file_name, FILE_COMMON)) { PrintFormat("File %s not exist", file_name); class="kw">return false; } if(symbol == NULL) { symbol = _Symbol; point = _Point; } ResetLastError(); class="type">int handle = FileOpen(file_name, FILE_READ | FILE_ANSI | FILE_CSV | FILE_COMMON, class="type">short(&class="macro">#x27;;&class="macro">#x27;), CP_ACP); if(handle == INVALID_HANDLE) { PrintFormat("Error of open file %s: %d", file_name, GetLastError()); class="kw">return false; } FileSeek(handle, class="num">0, SEEK_SET); while(!FileIsEnding(handle)) { class="type">class="kw">string s = FileReadString(handle); class="type">class="kw">datetime open_time = StringToTime(s); class="type">class="kw">string type = FileReadString(handle); class="type">class="kw">double volume = StringToDouble(FileReadString(handle)); class="type">class="kw">string deal_symbol = FileReadString(handle); class="type">class="kw">double open_price = StringToDouble(FileReadString(handle)); volume = MathMin(volume, StringToDouble(FileReadString(handle))); class="type">class="kw">datetime close_time = StringToTime(FileReadString(handle)); class="type">class="kw">double close_price = StringToDouble(FileReadString(handle)); s = FileReadString(handle); s = FileReadString(handle); s = FileReadString(handle); if(StringFind(deal_symbol, symbol, class="num">0) < class="num">0) class="kw">continue; ResetLastError(); CDeal *deal = new CDeal(); if(!deal) { PrintFormat("Error of create new deal object: %d", GetLastError()); class="kw">return false; } deal.OpenTime = open_time; deal.CloseTime = close_time;