交易中的神经网络:基于 ResNeXt 模型的多任务学习(终篇)·综合运用
(3/3)· 前篇铺好理论与模块,本篇把编码器拼起来、接上多任务头并跑通回测,补全整套框架
持仓动作数组的归一化与止损裁剪
这段逻辑出现在强化学习代理输出交易动作之后,负责把原始浮点结果压成 MT5 可下单价。tp>0 的分支里,先对 sl 做截断:用 MathAbs(sl)/(MaxSL*Point()) 算比例,MathMax 封顶到 1,再 MathMax(…, 0.01) 保证下限 0.01,避免零止损。 tp 则直接除以 MaxTP*Point() 并 MathMin 截到 1,意味着盈利空间超过最大允许值就归一为满格。result[0] 写的是仓位系数,取账户态的 account[0]/100*0.01 与 0.011 的较大者,实盘里这对应约 1.1% 风险敞口的底仓。 else 分支处理不开新仓只调已有止损的情形。argmax==0 或 argmax>argmin 时进入,用循环扫 target 与 fstate 差值:tp 取各段 target[j]+fstate[j,2]-fstate[j,0] 的最小值,sl 取对应下沿的最大值,一旦 cur_sl 已小于等于某段下沿就 break,说明止损已碰到位无需再扩。 外汇与贵金属杠杆高,这类自动裁剪若 MaxSL 设错,可能在黄金跳空时 sl 比例被截到 0.01 仍挡不住滑点,建议把 MaxSL 按品种 ATR(14) 的 3 倍预填后再跑。
if(tp > class="num">0) { sl = (class="type">class="kw">float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), class="num">1), class="num">0.01); tp = (class="type">class="kw">float)MathMin(tp / (MaxTP * Point()), class="num">1); result[class="num">0] = class="type">class="kw">float(MathMax(Buffer[tr].States[i].account[class="num">0]/class="num">100*class="num">0.01, class="num">0.011)); result[class="num">1] = tp; result[class="num">2] = sl; for(class="type">int j = class="num">3; j < NActions; j++) result[j] = class="num">0; bActions.AssignArray(result); } } else { if(argmax == class="num">0 || argmax > argmin) { class="type">class="kw">float tp = class="num">0; class="type">class="kw">float sl = class="num">0; class="type">class="kw">float cur_sl = class="type">class="kw">float(MaxSL * Point()); class="type">class="kw">ulong pos = class="num">0; for(class="type">class="kw">ulong j = class="num">0; j < argmin; j++) { tp = MathMin(tp, target[j] + fstate[j, class="num">2] - fstate[j, class="num">0]); pos = j; if(cur_sl <= target[j] + fstate[j, class="num">1] - fstate[j, class="num">0]) break; sl = MathMax(sl, target[j] + fstate[j, class="num">1] - fstate[j, class="num">0]); } if(tp < class="num">0)
◍ 策略网络里的止损止盈归一化
在强化学习交易代理的输出层,止损和止盈需要先压缩到 [0,1] 区间才能喂给策略网络。下面这段把绝对止损点数除以 MaxSL*Point() 做归一,并用 MathMax 限制下限 0.01,避免零止损导致网络梯度爆炸。 sl = (float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), 1), 0.01); tp = (float)MathMin(-tp / (MaxTP * Point()), 1); 这两行把 sl 夹在 0.01~1 之间,tp 夹在 0~1 之间。注意 tp 用了负号,说明原始 tp 变量存的是负值(空头盈利距离),归一后转成正向比例。 result[3] 写入仓位系数,取账户浮亏占比的 0.01 倍与 0.011 的较大值,意味着单步最小下注比例被锁在 1.1% 附近。result[4]、result[5] 分别存 tp、sl 归一值,前三个动作位先清零再由 bActions.AssignArray 送出。 反向传播阶段若 Actor.backProp 返回 false,立刻置 Stop=true 并 break,训练循环超时阈值设为 500ms(GetTickCount()-ticks>500)。外汇与贵金属杠杆高,这类 Agent 策略实盘前必须在 MT5 策略测试器跑满回测,参数失真可能放大爆仓概率。
sl = (class="type">class="kw">float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), class="num">1), class="num">0.01); tp = (class="type">class="kw">float)MathMin(-tp / (MaxTP * Point()), class="num">1); result[class="num">3] = class="type">class="kw">float(MathMax(Buffer[tr].States[i].account[class="num">0]/class="num">100*class="num">0.01,class="num">0.011)); result[class="num">4] = tp; result[class="num">5] = sl; for(class="type">int j = class="num">0; j < class="num">3; j++) result[j] = class="num">0; bActions.AssignArray(result); class=class="str">"cmt">//--- Actor Policy if(!Actor.backProp(GetPointer(bActions), (CBufferFloat*)GetPointer(bAccount), GetPointer(bGradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } target = vector<class="type">class="kw">float>::Zeros(NActions / class="num">3); for(class="type">int a = class="num">0; a < NActions; a += class="num">3) target[a / class="num">3] = class="type">class="kw">float(result[a] > class="num">0); if(!Result.AssignArray(target) || !Probability.backProp(Result, GetPointer(Actor), LatentLayer) || !Actor.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">double percent = class="type">class="kw">double(iter + i - start) * class="num">100.0 / (Iterations);
「把两个网络的误差打到图表上」
这段片段干的事很直接:在每轮训练 tick 里,把 Actor 与 Probability 两个网络的最近平均误差用 Comment 刷到图表左上角,方便肉眼盯收敛。格式串里 %6.2f%% 控制百分比两位小数,%15.8f 给误差留了 8 位小数宽度,浮点抖动一眼能看出来。 StringFormat 先把两行文本拼进 str,再统一交给 Comment;随后用 GetTickCount 重置计时,控制下一次打印节奏。注意 Comment("") 在循环外清屏,避免 EA 停止后文字残留。 收尾处用 PrintFormat 把函数名、行号、网络名和误差打到日志,精度 10.7f;最后 ExpertRemove 让 EA 自卸载。外汇与贵金属行情噪声大,这类误差曲线只反映样本内拟合,实盘泛化可能偏差,务必先在策略测试器跑完再谈信任。
class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor", percent, Actor.getRecentAverageError()); str += StringFormat("%-13s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Probability", percent, Probability.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Probability", Probability.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- }
用未见过的数据逼出模型真本事
训练完的「智代」行为政策不能直接信,得拿没参与过训练的样本去压。我们在逻辑里塞了一层短期走势概率预判:只有智能体准备下单的方向和这个概率模型里的最可能趋势重叠,订单才真正发出。相当于给策略加了道趋势过滤器,避免逆着主概率跑。 测试在 MT5 策略测试器里跑,用的是 2025 年 1 月行情,这段数据完全不在训练集内,其余参数和造训练集时保持一致。这种隔离能更贴近实盘碰未见数据的状态,外汇与贵金属杠杆高、滑点跳空频繁,隔离测试只是降低过拟合错觉,不承诺任何胜率。 实测 60 笔成交,日均约 3 笔;平仓盈利占比 43% 出头。关键在盈亏比——平均和最大盈利差不多是对应亏损的两倍,所以财务结果偏正。盈利因子 1.52,恢复因子 1.14,说明这段样本下净值回撤后爬回来的能力尚可,但换到其他月份可能明显变脸。
◍ 把这条线请下神坛
ResNeXt 多任务框架在 MT5 里跑通的关键,不在架构多玄乎,而在共享编码器加专用头的组合能压住过拟合——同一次训练里多任务并行,逼出的市场表征比单任务更泛化。 模块化参数让你按品种调卷积分组数,但原文给出的 MQL5 实现(Research / Study / Test 等mq5)只是在时间序列与趋势预测上验证过有效性,样本外韧性来自新增趋势模块,不是凭空抗跌。 真要上实盘,先在更具代表性的数据集重训,再用 Test.mq5 跑全样本回测;外汇与贵金属杠杆高、跳空频繁,模型在纸面稳健不等于实盘概率占优。 开 MT5 把 NeuroNet.mqh 和 Study.mq5 调出来,改一组卷积宽度跑一遍,比信任何「新机遇」口号都实在。