交易中的神经网络:搭配预测编码的混合交易框架(终篇)·进阶篇
🧠

交易中的神经网络:搭配预测编码的混合交易框架(终篇)·进阶篇

(2/3)·接上篇的 StockFormer 理论,本篇拆解编码器-解码器落地与智代政策训练,跨资产依赖不再靠肉眼猜

案例拆解 第 2/3 篇

接上篇,我们继续深挖 StockFormer 的工程落地。多数人在多标的联动时仍靠切换窗口肉眼比对,漏掉跨尺度依赖是常态。本篇把依赖项搜索模型与政策训练拆开讲,让你看清状态空间是怎么被拼起来的。

◍ 状态校验与多编码器前向传播的实现细节

这段循环体在做一件很具体的事:先把历史状态序列读进张量,凡是下标越界、状态全零或赋值失败的样本直接 iter-- 并 continue,不浪费算力。外汇与贵金属行情里有效状态稀疏,这种前置过滤能显著降低后续神经网络推理的无效开销。 前向传播部分同时驱动了 Relate、Short、Long 三套自编码结构,各自 encoder 接 decoder,且 decoder 把对应 encoder 当上下文网络传入。任意一条 feedForward 返回失败就打印函数名与行号、置 Stop 并 break,说明这套训练回路对数值稳定性零容忍。 预测阶段把重构状态 reshape 成 (NForecast+1) 行、BarDescr 列的矩阵,用末行减倒数第二行再除以最大绝对值,得到归一化方向向量 target。MT5 上跑这段代码时,把 NForecast 从默认改到 5~10 之间,能直观看到 target 对噪声的敏感度变化,贵金属品种尤其明显,注意杠杆风险偏高。

MQL5 / C++
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,能在回测时更快观察误差曲线,但实盘建议保留原值。

MQL5 / C++
      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 就能跑通骨架。

MQL5 / C++
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,这是避免野指针的硬规矩。外汇与贵金属杠杆高,这类网络若直接上实盘,参数漂移可能导致策略失效,建议先在策略测试器跑样本外验证。

MQL5 / C++
  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 这行的分母,能快速验证网络宽度对你的品种是否过窄。

MQL5 / C++
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));
把跨资产依赖监控交给小布
这些多模态序列的批量归一与依赖提取逻辑,小布盯盘的 AIGC 已内置到品种页,打开就能看到实时关联强度,你只需判断要不要动手。

常见问题

多模态时间序列来自指标和不同品种,分布差异大,批量归一化把输入拉到相近尺度,避免某一通道主导注意力计算。
作者用两个预测分支分别覆盖短期与长期计划范围,具体窗口在训练前按品种波动特性设定,没有统一默认值。
文中沿用强化学习回路,以统一状态空间的表征质量与模拟权益变化作为奖励倾向,而非单步胜率。
目前小布内置的是通用跨资产依赖视图,自定义变换器权重需通过接口导出,后续版本可能开放私有模型挂载。
解耦后依赖搜索与走势预测可独立训练、分别微调,避免梯度在长序列上相互干扰,也方便复用编码器表征。