交易中的神经网络:面向自适应智能体行为的技能层次结构(完结篇)·综合运用
状态编码后如何换算多空止损止盈
编码器把状态数组塞进 Result 后,立刻跑一次无标签的反向传播(backProp 传 NULL 目标),若失败就打印函数名与行号并中止循环——这一步在 MT5 里若频繁打印同一行号,说明你的 CBufferFloat 管道没接对,而不是行情问题。 真正产生交易动作的是对 result[0] 与 result[3] 的比较。当 result[0] > result[3] 时走多头分支:用 fstate 的累计和 target 做基准,在 NForecast 步内逐列算 tp 与 sl,一旦 cur_sl 触及 target[j]+fstate[j,2]-fstate[j,0] 就 break。注意 cur_sl 初始为 -(result[2]>0?result[2]:1)*MaxSL*Point(),也就是用上一轮 sl 比例乘最大止损点数。 多头下若 tp>0,会把 sl 夹在 [0.01,1] 区间(除以 MaxSL*Point() 取绝对值后 MathMin 封顶),tp 则除以 MaxTP*Point() 且封顶 1。result[0] 被重写为 max(result[0]-result[3], 0.011f),这个 0.011f 是硬性地板值,避免信号无限趋近于零。 空头分支镜像处理:result[0]<result[3] 时 cur_sl 变正数,tp 取 target[j]+fstate[j,2]-fstate[j,0] 的最小值,sl 取反向最大值,同样靠 cur_sl 越界来提前 break。两个分支都把算出的 tp/sl 写回 result[1]~[5] 再赋给 bActions,供后续下单模块直接读。 开 MT5 把 MaxSL、MaxTP 和 NForecast 三个参数单独拉出来试:NForecast 设小(如 5)会让 tp/sl 更早 break,回测中信号触发频率可能提高但单笔空间被压缩,外汇与贵金属杠杆高,参数乱调易放大回撤。
PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } class=class="str">"cmt">//--- State Encoder Result.AssignArray(fstate); if(!Encoder.backProp(Result, (CBufferFloat*)NULL, NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } target = fstate.Col(class="num">0).CumSum(); if(result[class="num">0] > result[class="num">3]) { 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(-(result[class="num">2] > class="num">0 ? result[class="num">2] : class="num">1) * MaxSL * Point()); class="type">int pos = class="num">0; for(class="type">int j = class="num">0; j < NForecast; j++) { tp = MathMax(tp, target[j] + fstate[j, class="num">1] - fstate[j, class="num">0]); pos = j; if(cur_sl >= target[j] + fstate[j, class="num">2] - fstate[j, class="num">0]) break; sl = MathMin(sl, target[j] + fstate[j, class="num">2] - fstate[j, class="num">0]); } 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] = MathMax(result[class="num">0] - result[class="num">3], class="num">0.011f); result[class="num">5] = result[class="num">1] = tp; result[class="num">4] = result[class="num">2] = sl; result[class="num">3] = class="num">0; bActions.AssignArray(result); } } else { if(result[class="num">0] < result[class="num">3]) { 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((result[class="num">5] > class="num">0 ? result[class="num">5] : class="num">1) * MaxSL * Point()); class="type">int pos = class="num">0; for(class="type">int j = class="num">0; j < NForecast; 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) {
「止损止盈数组的归一化与裁剪逻辑」
这段逻辑干的事很直接:把原始止损、止盈距离按 MaxSL、MaxTP 折算成 0~1 之间的相对值,再写回 result 数组交给 bActions。sl 那行先取绝对值除以 MaxSL*Point(),用 MathMin 夹到 1 以内,再用 MathMax 兜底到 0.01,意味着即使止损极小也不会被压成 0,最低保留 1% 的尺度。 tp 的折算更简单粗暴,负号来自空头方向的距离,除以 MaxTP*Point() 后只 MathMin 到 1,不设下限——若折算值本就为负则直接变 0,对应无盈利空间的状态。 后面的 else 分支在做样本裁剪:用 ArgMin/ArgMax 定位 target 数组里最极端的多空收益,只要最大盈利不到对应反向亏损的一半且未超 max_sl,就保留;否则 Resize 截掉较长那段重算。这种递归收缩能让训练集偏向「盈亏比至少 2:1 且止损可控」的片段,外汇与贵金属波动跳空频繁,这类过滤在实盘回测里常把无效信号砍掉三成以上。 开 MT5 把这段贴进 EA 的 signal 构造段,改 MaxSL、MaxTP 两个宏,跑一遍品种历史数据,看 result 数组第三、四位的分布是否如预期收敛在 0.01~1 区间。
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] = MathMax(result[class="num">3] - result[class="num">0], class="num">0.011f); result[class="num">2] = result[class="num">4] = tp; result[class="num">1] = result[class="num">5] = sl; result[class="num">0] = class="num">0; bActions.AssignArray(result); } } else { class="type">class="kw">ulong argmin = target.ArgMin(); class="type">class="kw">ulong argmax = target.ArgMax(); class="type">class="kw">float max_sl = class="type">class="kw">float(MaxSL * Point()); class="kw">while(argmax > class="num">0 && argmin > class="num">0) { if(argmax < argmin && target[argmax] / class="num">2 > MathAbs(target[argmin]) && MathAbs(target[argmin]) < max_sl) break; if(argmax > argmin && target[argmax] < MathAbs(target[argmin] / class="num">2) && target[argmax] < max_sl) break; target.Resize(MathMin(argmax, argmin)); argmin = target.ArgMin(); argmax = target.ArgMax(); } if(argmin == class="num">0 || (argmax < argmin && argmax > class="num">0)) { 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 < argmax; j++) { tp = MathMax(tp, target[j] + fstate[j, class="num">1] - fstate[j, class="num">0]); pos = j; if(cur_sl >= target[j] + fstate[j, class="num">2] - fstate[j, class="num">0]) break; sl = MathMin(sl, target[j] + fstate[j, class="num">2] - fstate[j, class="num">0]); } 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">5] = result[class="num">1] = tp; result[class="num">4] = result[class="num">2] = sl; result[class="num">3] = class="num">0; bActions.AssignArray(result); } } else { if(argmax == class="num">0 || argmax > argmin) {
◍ 止损被击穿时翻转为逆向开仓
这段逻辑处理的是 argmin 区间内止损空间被价格结构吃掉后的动作重构。循环里先取 cur_sl = MaxSL * Point() 作为当前允许的最大止损距离,再逐个比对 target[j] 与 fstate 的偏移;只要某一级的潜在回撤不超过 cur_sl 就继续累积 sl,一旦超出立即 break,此时 pos 停留在最后一个合规层级。 若最终 tp 算出来为负,说明顺着原方向的盈利空间已不存在,代码转而把 sl 与 tp 归一化到 [0.01,1] 区间:sl 取绝对值除以 MaxSL*Point() 再夹断,tp 取 -tp/(MaxTP*Point()) 上限 1。result[3] 的下单手风则取账户状态值的 0.01% 与 0.011 的较大者,这是个硬下限,避免极小仓位导致策略哑火。 翻身后 result[2]=result[4]=tp、result[1]=result[5]=sl、result[0]=0,交给 bActions.AssignArray 直接作为反向动作向量。后面 Actor.backProp 用 -1 奖励信号做负向传播,失败就置 Stop 退出;target 向量里大于 0 的 [0] 和 [3] 被钳成 1,再复制 BarDescr 份塞进 Result 让 Task 网络回传梯度。外汇与贵金属杠杆高,这种翻转逻辑在滑点扩大时可能连续触发,实盘前务必在 MT5 策略测试器用点差 20 点以上回测。
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) { 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">2] = result[class="num">4] = tp; result[class="num">1] = result[class="num">5] = sl; result[class="num">0] = class="num">0; bActions.AssignArray(result); } } } } class=class="str">"cmt">//--- Actor Policy if(!Actor.backProp(GetPointer(bActions), (CNet*)GetPointer(Task), -class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Agents target=result; if(target[class="num">0] > class="num">0) target[class="num">0] = class="num">1; if(target[class="num">3] > class="num">0) target[class="num">3] = class="num">1; Result.Clear(); for(class="type">int i = class="num">0; i < BarDescr; i++) { if(!Result.AddArray(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } if(!Task.backProp(Result, (CNet*)GetPointer(Encoder), LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Probability target = vector<class="type">class="kw">float>::Zeros(NActions / class="num">3);
训练回路里的方向判定与超时打点
这段逻辑先把特征序列做累计求和,用绝对值最大的位置判断当前趋势偏向:若累计值大于 0 则给 target[0] 置 1(倾向多头),小于 0 则给 target[1] 置 1(倾向空头)。外汇与贵金属杠杆高,这种标签只反映历史样本的统计倾向,不等于后续必沿该方向走。 反向传播一旦在 AssignArray、backProp 或 backPropGradient 任一环节失败,立刻打印函数名与行号、置 Stop 并 break,避免脏梯度继续污染网络。 每超过 500 毫秒(GetTickCount 差值阈值)就把 Encoder / Task / Actor / Probability 四个模块的近期平均误差按百分比拼成字符串用 Comment 推到图表,便于肉眼盯训练进度;循环结束清空 Comment,并用 PrintFormat 把四个误差以 10.7f 精度落日志,最后调 ExpertRemove 卸掉 EA。 在 MT5 里把 Iterations 调小到几百跑一遍,看 Comment 里的百分比和误差是否随迭代单调下降;若 500ms 打点太稀,可改到 200ms 观察更细的波动。
vector<class="type">class="kw">float> trend=fstate.Col(class="num">0).CumSum(); class="type">class="kw">ulong argmax=MathAbs(trend).ArgMax(); if(trend[argmax] > class="num">0) target[class="num">0] = class="num">1; else if(trend[argmax] < class="num">0) target[class="num">1] = class="num">1; if(!Result.AssignArray(target) || !Probability.backProp(Result, (CNet*)GetPointer(Encoder),LatentLayer) || !Encoder.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); class="type">class="kw">string str = StringFormat("%-12s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Encoder", percent, Encoder.getRecentAverageError()); str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Task", percent, Task.getRecentAverageError()); 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__, "Encoder", Encoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Task", Task.getRecentAverageError()); 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">//--- }
「样本外三个月跑出来的真实账」
模型训练吃的是2024全年数据,验证则丢进 MT5 策略测试器,用2025年1至3月样本外数据跑。时间周期、市场环境、模拟配置全部锁死,只换数据,避免偷看未来。 三个月里共成交860笔,盈利340笔,胜率39.53%。单看胜率偏低,但盈利单平均收益比亏损单高约70%,靠赔率把净值拉正。 测试期三个月各自独立收盈,没有哪个月翻车。外汇与贵金属属高风险品种,样本外盈利不代表未来稳健,开 MT5 用自己的品种复跑这套参数再下结论。
◍ 把工具请下神坛
HiSSD 把交易智能体拆成通用技能与任务专属技能两层,在 EURUSD 高度波动的环境里确实跑通了快速适配,不用推倒重训。我们拿 2024 年真实 EURUSD 行情训模型,2025 年初换一批没见过的数据测,这种就近实盘的验证比纸面回测更有参考价值。 但别把它当印钞机。外汇和贵金属本身就是高杠杆、高波动的品种,模型只在单品种、单时段验证过,直接上实盘等于蒙眼过桥。真要落地,得在更多品种和牛熊震荡各种形态下把训练集撑厚,再全面压测。 本系列给的 Research.mq5、Study.mq5、Test.mq5 那套 EA 和 NeuroNet 类库就搁在附件 ZIP 里,2622 KB 不到。开 MT5 把代码拖进去,先在自己账号的模拟盘跑一遍采集和训练,比听谁吹框架更有用。