神经网络变得轻松(第四十七部分):连续动作空间·综合运用
「挂单前的边界判定与异常重置」
卖单止盈与止损直接锚定 Bid:止盈取 Bid 减去 ActorResult[4],止损取 Bid 加上 ActorResult[5],两者都用 NormalizeDouble 按品种小数位收敛,避免报价精度引发的 reject。 开仓条件分两头看:买侧要求 ActorResult[0] 手数大于 min_lot、[1][2] 止损止盈距离大于 stops 且 buy_sl 为正;卖侧对称校验 [3][4][5] 与 sell_tp 为正,满足即走 Trade.Buy / Trade.Sell。 若 temp 数组出现负值,或 [0][3] 超 1.0、[1][4] 超 Point*5000、[2][5] 超 Point*2000,说明上轮输出越界,触发随机重采样:手数在 LotsMin 的 1~6 倍间撒点,止损止盈在 StopsLevel 基础上加 0~500 / 0~200 点随机扰动,再回写 Result 并调 backProp。外汇与贵金属杠杆高,这类越界重置若频繁发生,可能暗示模型输入分布漂移,建议先在 MT5 策略测试器跑一轮看触发频率。
class="type">class="kw">double sell_tp = NormalizeDouble(Symb.Bid() - ActorResult[class="num">4], Symb.Digits()); class="type">class="kw">double sell_sl = NormalizeDouble(Symb.Bid() + ActorResult[class="num">5], Symb.Digits()); class=class="str">"cmt">//--- if(ActorResult[class="num">0] > min_lot && ActorResult[class="num">1] > stops && ActorResult[class="num">2] > stops && buy_sl > class="num">0) Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), buy_sl, buy_tp); if(ActorResult[class="num">3] > min_lot && ActorResult[class="num">4] > stops && ActorResult[class="num">5] > stops && sell_tp > class="num">0) Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), sell_sl, sell_tp); if(temp.Min() < class="num">0 || MathMax(temp[class="num">0], temp[class="num">3]) > class="num">1.0f || MathMax(temp[class="num">1], temp[class="num">4]) > (Symb.Point() * class="num">5000) || MathMax(temp[class="num">2], temp[class="num">5]) > (Symb.Point() * class="num">2000)) { temp[class="num">0] = (class="type">float)(Symb.LotsMin() * (class="num">1 + MathRand() / class="num">32767.0 * class="num">5)); temp[class="num">3] = (class="type">float)(Symb.LotsMin() * (class="num">1 + MathRand() / class="num">32767.0 * class="num">5)); temp[class="num">1] = (class="type">float)(Symb.Point() * (MathRand() / class="num">32767.0 * class="num">500.0 + Symb.StopsLevel())); temp[class="num">4] = (class="type">float)(Symb.Point() * (MathRand() / class="num">32767.0 * class="num">500.0 + Symb.StopsLevel())); temp[class="num">2] = (class="type">float)(Symb.Point() * (MathRand() / class="num">32767.0 * class="num">200.0 + Symb.StopsLevel())); temp[class="num">5] = (class="type">float)(Symb.Point() * (MathRand() / class="num">32767.0 * class="num">200.0 + Symb.StopsLevel())); Result.AssignArray(temp); Actor.backProp(Result, GetPointer(PrevAccount), GetPointer(Gradient)); } }
训练集里的模型表现与单代理验证
模型用 2023 年初 EURUSD H1 的默认参数训练完后,先跑单代理慢速优化做算法正确性初检,避免并行代理干扰实时训练的自适应。为了控制训练迭代轮次,EA 里加了个外部参数 Agent,但算法本身并不读取它。 约 3000 次验算后,训练集上拿到一个能盈利的模型:5 个月 334 笔交易,胜率超 84%,利润约初始资金 33%。余额回撤不到 1%,净值回撤 7.6%,盈利因子超 26,恢复因子 3.16。 余额线几乎一直压在净值线下方,说明持仓方向大概率没错;资金负载约 20%,偏高但还没吃掉累计利润。代价是这套结果明显只在训练集内成立,丢到样本外就温和得多——外汇和贵金属这种高波动品种,过拟合风险得时刻记着。
◍ 在线训练瓶颈与样本外泛化落差
DDPG 代理在训练集内能够产出正收益,但切换至样本外行情后,同等训练量下策略表现明显衰减,说明当前在线训练范式对分布外泛化支撑不足。 核心瓶颈在于模型只能串行在线学习,无法并行跑多个代理去扩充环境采样,直接拉长了探索—收敛周期,也限制了风险与仓位策略的鲁棒性提升。 若要在 MT5 上复现这一落差,可先固定训练窗口长度,对比 in-sample 与 out-of-sample 的权益曲线斜率;样本外回撤扩大即提示需引入并行经验回放或离线预训练。 外汇与贵金属杠杆品种下,这类样本外失效会放大爆仓概率,任何代理权重上线前都应在多品种多周期做压力验证。
「延伸阅读的技术脉络」
这一节把文中用到的几类算法出处列了出来,方便你顺着去翻原系列。涉及的方法包括连续控制用的深度强化学习、第二十七篇讲的基础 DQN,以及第二十九篇的 A2C 优势演员-评论家结构。 第四十六篇提出的条件导向目标强化学习(GCRL)是后面策略改写的关键参照。MT5 上做智能体训练时,建议先对照这几篇把网络头和奖励函数对齐,再移植到实盘品种——外汇与贵金属杠杆高、滑点突变频繁,回测和模拟验证不充分时实盘亏损概率显著放大。
随文附带的四个程序文件
这套 LSTM 智能体方案落地到 MT5,核心就是四个文件:Study.mq5 是代理者训练 EA,负责在策略测试器里跑强化学习;Test.mq5 是模型测试 EA,加载训练好的 .nnw 做前向推理;NeuroNet.mqh 是封装好的神经网络 C++ 类库;NeuroNet.cl 则是 OpenCL 核函数代码库,把矩阵运算丢给显卡。 社区里有人反馈 Test EA 跑完一条直线、零成交。作者原话里点出一个关键参数:库内训练学习率写死为 3.0e-4f,意味着单个样本约需 4000 次迭代模型才记得住,小学习率是为让权重做样本平均。若训练后无交易,不是停训信号,而是惩罚项没压够——对空仓的惩罚应大于零、但不超过单笔潜在亏损,否则模型会为躲惩罚乱开仓。 下面这段是从测试 EA 里抠出来的推理与下单片段,可照着加 PrintFormat 查 Actor 输出: double sell_sl = NormalizeDouble(Symb.Bid() + ActorResult[5], Symb.Digits()); PrintFormat("ActorResult[0]=%f ActorResult[1]=%f ActorResult[2]=%f buy_sl=%f",ActorResult[0], ActorResult[1], ActorResult[2], buy_sl); PrintFormat("ActorResult[3]=%f ActorResult[4]=%f ActorResult[5]=%f sell_tp=%f",ActorResult[3], ActorResult[4], ActorResult[5], sell_tp); //--- if(ActorResult[0] > 0 && ActorResult[1] > 0 && ActorResult[2] > 0 && buy_sl > 0) Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), buy_sl, buy_tp); if(ActorResult[3] > 0 && ActorResult[4] > 0 && ActorResult[5] > 0 && sell_tp > 0) Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), sell_sl, sell_tp); 逐行看:sell_sl 用 Bid 加 ActorResult[5] 再按品种精度截断;两个 PrintFormat 把 Actor 输出的六个值及算出的止损止盈打印到日志;后面两个 if 要求对应维度全为正且止损价有效才发 Buy / Sell。日志实截里 ActorResult[1/2/5] 全是负的小量(如 -0.000476、-0.000742),条件不满足,所以不下单——这也是直线现象的现场证据。外汇与贵金属杠杆高,这类模型若惩罚项设错,可能频繁裸开仓,实盘前务必在测试器验证。
class="type">class="kw">double sell_sl = NormalizeDouble(Symb.Bid() + ActorResult[class="num">5], Symb.Digits()); PrintFormat("ActorResult[class="num">0]=%f ActorResult[class="num">1]=%f ActorResult[class="num">2]=%f buy_sl=%f",ActorResult[class="num">0], ActorResult[class="num">1], ActorResult[class="num">2], buy_sl); PrintFormat("ActorResult[class="num">3]=%f ActorResult[class="num">4]=%f ActorResult[class="num">5]=%f sell_tp=%f",ActorResult[class="num">3], ActorResult[class="num">4], ActorResult[class="num">5], sell_tp); class=class="str">"cmt">//--- if(ActorResult[class="num">0] > class="num">0 && ActorResult[class="num">1] > class="num">0 && ActorResult[class="num">2] > class="num">0 && buy_sl > class="num">0) Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), buy_sl, buy_tp); if(ActorResult[class="num">3] > class="num">0 && ActorResult[class="num">4] > class="num">0 && ActorResult[class="num">5] > class="num">0 && sell_tp > class="num">0) Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);
◍ 最后说句实在话
上面那几行是某次回测里 Core 01 在 2023.05.30 22:00–23:00 吐出的 ActorResult 与挂单价快照:ActorResult[0] 和 [3] 都是 0.085580,[1][4] 为 -0.000476,[2][5] 为 -0.000742,对应的 sell_tp 从 1.070090 漂到 1.071130,buy_sl 落在 1.073750。 这类输出说明模型对同向时段的信号强度几乎不变,但止损止盈价随小时线微移,实盘里得防着点滑点把薄利吃掉。 外汇和贵金属杠杆高,这种毫秒级回测结果只证明逻辑跑通,不等于能直接搬去 live 账户,开 MT5 用历史中心自己复一遍最踏实。
class="num">2023.12.class="num">01 class="num">23:class="num">15:class="num">18.641 Core class="num">01 class="num">2023.05.class="num">30 class="num">22:class="num">00:class="num">00 ActorResult[class="num">3]=class="num">0.085580 ActorResult[class="num">4]=-class="num">0.000476 ActorResult[class="num">5]=-class="num">0.000742 sell_tp=class="num">1.070090 class="num">2023.12.class="num">01 class="num">23:class="num">15:class="num">18.641 Core class="num">01 class="num">2023.05.class="num">30 class="num">23:class="num">00:class="num">00 ActorResult[class="num">0]=class="num">0.085580 ActorResult[class="num">1]=-class="num">0.000476 ActorResult[class="num">2]=-class="num">0.000742 buy_sl=class="num">1.073750 class="num">2023.12.class="num">01 class="num">23:class="num">15:class="num">18.641 Core class="num">01 class="num">2023.05.class="num">30 class="num">23:class="num">00:class="num">00 ActorResult[class="num">3]=class="num">0.085580 ActorResult[class="num">4]=-class="num">0.000476 ActorResult[class="num">5]=-class="num">0.000742 sell_tp=class="num">1.071130