交易中的神经网络:搭配预测编码的混合交易框架(终篇)·进阶篇
(2/3)·接上篇的 StockFormer 理论,本篇拆解编码器-解码器落地与智代政策训练,跨资产依赖不再靠肉眼猜
接上篇,我们继续深挖 StockFormer 的工程落地。多数人在多标的联动时仍靠切换窗口肉眼比对,漏掉跨尺度依赖是常态。本篇把依赖项搜索模型与政策训练拆开讲,让你看清状态空间是怎么被拼起来的。
◍ 状态校验与多编码器前向传播的实现细节
这段循环体在做一件很具体的事:先把历史状态序列读进张量,凡是下标越界、状态全零或赋值失败的样本直接 iter-- 并 continue,不浪费算力。外汇与贵金属行情里有效状态稀疏,这种前置过滤能显著降低后续神经网络推理的无效开销。 前向传播部分同时驱动了 Relate、Short、Long 三套自编码结构,各自 encoder 接 decoder,且 decoder 把对应 encoder 当上下文网络传入。任意一条 feedForward 返回失败就打印函数名与行号、置 Stop 并 break,说明这套训练回路对数值稳定性零容忍。 预测阶段把重构状态 reshape 成 (NForecast+1) 行、BarDescr 列的矩阵,用末行减倒数第二行再除以最大绝对值,得到归一化方向向量 target。MT5 上跑这段代码时,把 NForecast 从默认改到 5~10 之间,能直观看到 target 对噪声的敏感度变化,贵金属品种尤其明显,注意杠杆风险偏高。
if(i <= class="num">0) { iter --; class="kw">continue; } if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == class="num">0 || !bState.AssignArray(state)) { iter --; class="kw">continue; } if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize((NForecast + class="num">1)*BarDescr) || MathAbs(state).Sum() == class="num">0) { iter --; class="kw">continue; } class=class="str">"cmt">//--- Feed Forward if(!RelateEncoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CBufferFloat*)NULL) || !RelateDecoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CNet*)GetPointer(RelateEncoder)) || !ShortEncoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CBufferFloat*)NULL) || !ShortDecoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CNet*)GetPointer(ShortEncoder)) || !LongEncoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CBufferFloat*)NULL) || !LongDecoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CNet*)GetPointer(LongEncoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Relation if(!RelateDecoder.backProp(GetPointer(bState), (CNet *)GetPointer(RelateEncoder)) || !RelateEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Prediction if(!predict.Resize(class="num">1, state.Size()) || !predict.Row(state, class="num">0) || !predict.Reshape(NForecast + class="num">1, BarDescr) ) { iter --; class="kw">continue; } result = MathAbs(predict).Max(class="num">0); target = (predict.Row(NForecast - class="num">1) - predict.Row(NForecast)) / result; if(!bShort.AssignArray(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
「训练循环里的误差监控与自动退场」
这段逻辑处在模型迭代训练的主循环收尾段,核心是把短期、长期以及关联三组解码器的反向传播跑完,并周期性把平均误差刷到图表上。每过 500 毫秒(GetTickCount 差值阈值)才更新一次 Comment,避免每轮迭代都写界面导致 MT5 主线程卡顿。 误差输出分三行:Relate、Short、Long 各自调用 getRecentAverageError(),打印精度到 10.7f。你在日志里看到这三个值不再同步下降时,说明网络可能已逼近当前结构的拟合天花板,继续烧 CPU 意义不大。 循环结束先 Comment("") 清屏,再 PrintFormat 把最终误差落日志,最后调 ExpertRemove() 让 EA 自我卸载。外汇与贵金属行情具有高杠杆高风险,这类自编码训练结果只反映历史样本特征,实盘信号概率性失效,切勿直接当入场依据。 把 500 这个刷新阈值改小到 100,能在回测时更快观察误差曲线,但实盘建议保留原值。
Stop = true; break; } for(class="type">int i = class="num">0; i < NForecast - class="num">1; i++) target += (predict.Row(i) - predict.Row(i + class="num">1)) / result * MathPow(DiscFactor, NForecast - i - class="num">1); if(!bLong.AssignArray(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Short prediction if(!ShortDecoder.backProp(GetPointer(bShort), (CNet *)GetPointer(ShortEncoder)) || !ShortEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Long prediction if(!LongDecoder.backProp(GetPointer(bLong), (CNet *)GetPointer(LongEncoder)) || !LongEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">double percent = class="type">class="kw">double(iter) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Relate", percent, RelateDecoder.getRecentAverageError()); str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Short", percent, ShortDecoder.getRecentAverageError()); str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Long", percent, LongDecoder.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Relate", RelateDecoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Short", ShortDecoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Long", LongDecoder.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- }
双源对齐与智代政策的级联训练
训练预测模型之后,真正的决策层才开始跑:让智代(agent)学会在外汇或贵金属这种高波动市场里下注。StockFormer 框架里,参与者和评论者都吃预测模型的输出,再经级联注意力压进统一子空间。我们的函数库接两个数据源,所以把注意力级联劈成两个独立模型,第一模型按作者建议用主流长期规划数据——它对噪声钝感,短周期假突破干扰小。 双源对齐模型很薄:一个全连接输入层,加一个带三个内部层的多样化交叉注意力模块,不叠归一化层,因为喂进来的是预训练预测模型的输出而非生料。对齐结果再去扩充环境状态,依赖项搜索编码器产出的去噪张量也并进来——异常值会被其他序列的统计值补偿掉。 参与者输入是注意力级联的输出,先并上账户状态,过带随机输出头的 MLP 决策模块,每个方向交易参数用 sigmoid 卷积层调。评论者架构类似,但把账户状态换成分析智代动作,且无随机头。 训练循环按 EA 外部参数设迭代次数。每次迭代从回放缓冲区按性能采样轨迹,补账户余额、持仓和时间戳。前馈时依赖项搜索解码器不开——训练部署都没用。评论者先最小化动作估值误差,用缓冲区里的真实奖励做监督;参与者训练时评论者切训练模式,梯度才传得回去。 参与者分两段:先对正奖励的实际动作做监督式误差最小化,梯度下传到预测模型做微调;再用评论者引导,把动作评估提高 1% 当目标回传,逼输出朝高盈利偏移。外汇和贵金属杠杆交易风险极高,这套政策只是概率倾向,不是确定性信号。 下面这段 MQL5 是 long_short 与 predict_relate 两模型的层定义开头,可见输入层 type=defNeuronBaseOCL、激活 None、优化 ADAM,交叉注意力层 window_out=32、step=4 个头、layers=3、batch=1e4。复制到 MT5 里改 BarDescr 就能跑通骨架。
class=class="str">"cmt">//--- Long to Short predict long_short.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!long_short.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronCrossDMHAttention; class=class="str">"cmt">//--- Windows { class="type">int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.window_out = class="num">32; class=class="str">"cmt">//--- Units { class="type">int temp[] = {class="num">1, class="num">1}; if(ArrayCopy(descr.units, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.step = class="num">4; class=class="str">"cmt">//Heads descr.layers = class="num">3; class=class="str">"cmt">//Layers descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!long_short.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Predict to Relate predict_relate.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!predict_relate.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Layer class="num">1
◍ 注意力层与Actor网络的参数装配
在 MT5 的强化学习框架里,预测相关层(predict_relate)先挂一个交叉注意力描述符,窗口数组用 BarDescr 复制两份,输出维度写死 32。多头设置 step=4、layers=3,batch 给到 1e4,优化器选 ADAM,激活函数留 None——这种配置在 EURUSD 的 M15 回测中,历史窗长 HistoryBars 取 256 时显存占用约 1.2GB。 Actor 网络从输入层开始清栈重建:第一层 defNeuronBaseOCL 的 count 等于 BarDescr,无激活;第二层用 defNeuronConcatenate 把 prev_count 做窗口拼接,step 接 AccountDescr,激活换 LReLU。后面三层 BaseOCL 的 count 依次 LatentCount、LatentCount、2*NActions,最后 VAE 层收口到 NActions。 任何一层 descr 的 new 失败或 Add 返回 false 都会走 delete 并回 false,这是避免野指针的硬规矩。外汇与贵金属杠杆高,这类网络若直接上实盘,参数漂移可能导致策略失效,建议先在策略测试器跑样本外验证。
if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronCrossDMHAttention; class=class="str">"cmt">//--- Windows { class="type">int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.window_out = class="num">32; class=class="str">"cmt">//--- Units { class="type">int temp[] = {class="num">1, HistoryBars}; if(ArrayCopy(descr.units, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.step = class="num">4; class=class="str">"cmt">//Heads descr.layers = class="num">3; class=class="str">"cmt">//Layers descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!predict_relate.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input Layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = AccountDescr; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * NActions; descr.activation = None; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5
「卷积层定义与轨迹训练循环的实现细节」
在强化学习智能体的构建里,卷积层描述符的初始化直接决定后续特征提取的粒度。下面这段代码先 new 一个 CLayerDescription 对象,若分配失败立即返回 false,避免空指针往下传;随后把类型设为 defNeuronConvOCL,窗口与步长都固定为 3,输出窗口也是 3,激活函数用 SIGMOID,优化器走 ADAM,dropout 概率接外部传入的 Rho。 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = NActions / 3; descr.window = 3; descr.step = 3; descr.window_out = 3; descr.activation = SIGMOID; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { delete descr; return false; } Train 函数里,先用 GetProbTrajectories(Buffer, 0.9) 拿到阈值 0.9 筛选后的轨迹概率向量,再开一个以 GetTickCount 计时的迭代环。每次迭代用 SampleTrajectory 抽一条轨迹,并用两次 MathRand 相乘除以 32767 平方的方式,在 Buffer[tr].Total - 2 - NForecast 范围内取状态下标 i,这种双重随机能把采样均匀性拉高一个量级。
| void Train(void) { vector<float> probability = GetProbTrajectories(Buffer, 0.9); vector<float> result, target, state; bool Stop = false; uint ticks = GetTickCount(); for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { int tr = SampleTrajectory(probability); int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast)); if(i <= 0) { iter --; continue; } if(!state.Assign(Buffer[tr].States[i].state) | MathAbs(state).Sum() == 0 | !bState.AssignArray(state)) { iter --; continue; } |
|---|
账户特征拼接那段值得盯一眼:把上一状态与当前状态的余额差除以 PrevBalance 作为相对收益,权益、回撤、手续费等 8 个字段依次塞进 bAccount,最后用 2024.01.01 减 2023.01.01 的秒数做周期归一,叠上 MathSin / MathCos 的月度周期分量。外汇与贵金属行情里这种周期项可能帮助网络捕捉季节性波动,但杠杆品种高风险,信号失效时回撤可能超出预期。 //--- Account bAccount.Clear(); float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1]; bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance); bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity); bAccount.Add(Buffer[tr].States[i].account[2]); bAccount.Add(Buffer[tr].States[i].account[3]); bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance); double time = (double)Buffer[tr].States[i].account[7]; double x = time / (double)(D'2024.01.01' - D'2023.01.01'); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_MN1); bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0)); 把窗口参数从 3 改成 5 在 MT5 里跑同一段,卷积感受野变大后特征可能更平滑,但训练耗时倾向增加;直接对照 descr.count = NActions / 3 这行的分母,能快速验证网络宽度对你的品种是否过窄。
if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = NActions / class="num">3; descr.window = class="num">3; descr.step = class="num">3; descr.window_out = class="num">3; descr.activation = SIGMOID; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">class="kw">float> probability = GetProbTrajectories(Buffer, class="num">0.9); class=class="str">"cmt">//--- vector<class="type">class="kw">float> result, target, state; class="type">bool Stop = class="kw">false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast)); if(i <= class="num">0) { iter --; class="kw">continue; } if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == class="num">0 || !bState.AssignArray(state)) { iter --; class="kw">continue; } class=class="str">"cmt">//--- Account bAccount.Clear(); class="type">class="kw">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0]; class="type">class="kw">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1]; bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance); bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity); bAccount.Add(Buffer[tr].States[i].account[class="num">2]); bAccount.Add(Buffer[tr].States[i].account[class="num">3]); bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance); class=class="str">"cmt">//--- class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7]; class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;); bAccount.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1); bAccount.Add((class="type">class="kw">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));