交易中的神经网络:一种复杂的轨迹预测方法(Traj-LLM)·综合运用
📘

交易中的神经网络:一种复杂的轨迹预测方法(Traj-LLM)·综合运用

第 3/3 篇

◍ 前向传播与梯度回传的层序耦合

这段 CNeuronTrajLLMOCL 的实现把推理和训练拆成严格对称的两条链。前向从上下文 MLP 出发,经高层交互进 Mamba 序列,再走 LaneAware 与预测 MLP,任意一层 FeedForward 失败立即返回 false,说明容错是逐层短路的。 梯度侧完全逆序:从转置输出层往回,caForecastMLP[1] 先吃 cTransposeOut 的梯度,一路退到 cContext,最后做 DeConcat 与 SumAndNormilize。Mamba 块用 for 从 Size()-2 递减到 0 调 calcHiddenGradients,和前向的 i=1 递增形成镜像。 在 MT5 里接 OpenCL 跑这套网络时,若某次 calcInputGradients 返回 false,优先查 caMamba 最后一块的 getOutput 与 Activation 枚举——这两者在 LaneAware 梯度计算里被同时引用,类型或维度不对会直接断链。外汇与贵金属标的上用此类模型,参数敏感度高,实盘前务必在 Tick 回测里验证梯度对称性,高风险。

MQL5 / C++
   class="kw">return false;
class=class="str">"cmt">//--- Lane aware
   if(!cHighLevelInteraction.FeedForward(cContextMLP[class="num">1].AsObject()))
      class="kw">return false;
   if(!caMamba[class="num">0].FeedForward(cHighLevelInteraction.AsObject()))
      class="kw">return false;
   for(class="type">int i = class="num">1; i < class="type">int(caMamba.Size()); i++)
     {
      if(!caMamba[i].FeedForward(caMamba[i - class="num">1].AsObject()))
         class="kw">return false;
     }
   if(!cLaneAware.FeedForward(cVariablesPE.AsObject(), caMamba[caMamba.Size() - class="num">1].getOutput()))
      class="kw">return false;
class=class="str">"cmt">//--- Forecast
   if(!caForecastMLP[class="num">0].FeedForward(cLaneAware.AsObject()))
      class="kw">return false;
   if(!caForecastMLP[class="num">1].FeedForward(caForecastMLP[class="num">0].AsObject()))
      class="kw">return false;
   if(!cTransposeOut.FeedForward(caForecastMLP[class="num">1].AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronTrajLLMOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      class="kw">return false;
class=class="str">"cmt">//--- Forecast
   if(!caForecastMLP[class="num">1].calcHiddenGradients(cTransposeOut.AsObject()))
      class="kw">return false;
   if(!caForecastMLP[class="num">0].calcHiddenGradients(caForecastMLP[class="num">1].AsObject()))
      class="kw">return false;
class=class="str">"cmt">//--- Lane aware
   if(!cLaneAware.calcHiddenGradients(caForecastMLP[class="num">0].AsObject()))
      class="kw">return false;
   if(!cVariablesPE.calcHiddenGradients(cLaneAware.AsObject(), caMamba[caMamba.Size() - class="num">1].getOutput(),
caMamba[caMamba.Size() - class="num">1].getGradient(),
(ENUM_ACTIVATION)caMamba[caMamba.Size() - class="num">1].Activation()))
      class="kw">return false;
   for(class="type">int i = class="type">int(caMamba.Size()) - class="num">2; i >= class="num">0; i--)
      if(!caMamba[i].calcHiddenGradients(caMamba[i + class="num">1].AsObject()))
         class="kw">return false;
   if(!cHighLevelInteraction.calcHiddenGradients(caMamba[class="num">0].AsObject()))
      class="kw">return false;
   if(!cContextMLP[class="num">1].calcHiddenGradients(cHighLevelInteraction.AsObject()))
      class="kw">return false;
   if(!cContextMLP[class="num">0].calcHiddenGradients(cContextMLP[class="num">1].AsObject()))
      class="kw">return false;
   if(!cContext.calcHiddenGradients(cContextMLP[class="num">0].AsObject()))
      class="kw">return false;
   if(!DeConcat(cStatePE.getGradient(), cVariableToState.getGradient(), cContext.getGradient(),
cStateToVariable.Neurons(), cVariableToState.Neurons(), class="num">1))
      class="kw">return false;
   if(!SumAndNormilize(cVariablesPE.getGradient(), cVariablesPE.getGradient(),
cVariablesMLP[class="num">1].getGradient(), class="num">1, false))

「反向传播里的梯度链式归并」

这段逻辑跑在编码器反向传播阶段,每一步都先算隐藏层梯度,再用 SumAndNormilize 把上游梯度归并到当前层,任何一步返回 false 就直接中断。注意 SumAndNormilize 的调用里大量出现常数参组 (1, false, 0, 0, 0, 1),说明缩放系数固定为 1、不启用偏置补偿,归一化维度靠末位 1 锁定。 从注释看,计算顺序严格倒序:先 Position Encoder 相关层(cStatePE、cVariablesMLP[1]),再 Variables Encoder(cVariablesMLP[0]、cVariablesRNN、cTranspose),最后 State Encoder(cStateMLP[0]、cStateRNN、NeuronOCL)。这种分层回传保证了 Transformer 类结构里位置与状态编码的梯度不串层。 NeuronOCL 在这里被当作输出映射层使用,FeedForward 之后立刻用 SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getGradient(), cTranspose.getGradient(), 1, false) 把转置层梯度写回自身,参数比别处少两个零值位,说明该层归并路径更短。 在 MT5 里验证时,可以把这段塞进你自己的 CEncoder::calcGradients 方法,重点观察 cVariablesMLP[1] 的梯度在三次 SumAndNormilize 调用后是否出现数值发散——外汇与贵金属行情噪声大,梯度爆炸概率偏高,属高风险调试。

MQL5 / C++
  class="kw">return false;
  if(!cVariablesPE.calcHiddenGradients(cVariableToState.AsObject(), cStateToVariable.getOutput(),
cStateToVariable.getGradient(),
(ENUM_ACTIVATION)cStateToVariable.Activation()))
    class="kw">return false;
  if(!SumAndNormilize(cStateToVariable.getGradient(), cStatePE.getGradient(),
cStateToVariable.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
    class="kw">return false;
  if(!SumAndNormilize(cVariablesPE.getGradient(), cVariablesMLP[class="num">1].getGradient(),
cVariablesMLP[class="num">1].getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
    class="kw">return false;
  if(!cStateToState.calcHiddenGradients(cStateToVariable.AsObject(), cVariablesPE.getOutput(),
cVariablesPE.getGradient(),
(ENUM_ACTIVATION)cVariablesPE.Activation()))
    class="kw">return false;
  if(!SumAndNormilize(cVariablesPE.getGradient(), cVariablesMLP[class="num">1].getGradient(),
cVariablesPE.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
    class="kw">return false;
  if(!cStatePE.calcHiddenGradients(cStateToState.AsObject()))
    class="kw">return false;
class=class="str">"cmt">//--- Position Encoder
  if(!cStateMLP[class="num">1].calcHiddenGradients(cStatePE.AsObject()))
    class="kw">return false;
  if(!cVariablesMLP[class="num">1].calcHiddenGradients(cVariablesPE.AsObject()))
    class="kw">return false;
class=class="str">"cmt">//--- Variables Encoder
  if(!cVariablesMLP[class="num">0].calcHiddenGradients(cVariablesMLP[class="num">1].AsObject()))
    class="kw">return false;
  if(!cVariablesRNN.calcHiddenGradients(cVariablesMLP[class="num">0].AsObject()))
    class="kw">return false;
  if(!cTranspose.calcHiddenGradients(cVariablesRNN.AsObject()))
    class="kw">return false;
  if(!NeuronOCL.FeedForward(cTranspose.AsObject()))
    class="kw">return false;
  if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getGradient(), cTranspose.getGradient(), class="num">1, false))
    class="kw">return false;
class=class="str">"cmt">//--- State Encoder
  if(!cStateMLP[class="num">0].calcHiddenGradients(cStateMLP[class="num">1].AsObject()))
    class="kw">return false;
  if(!cStateRNN.calcHiddenGradients(cStateMLP[class="num">0].AsObject()))
    class="kw">return false;
  if(!NeuronOCL.calcHiddenGradients(cStateRNN.AsObject()))
    class="kw">return false;
  if(!SumAndNormilize(cTranspose.getGradient(), NeuronOCL.getGradient(),
NeuronOCL.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateEncoderDescriptions(CArrayObj *&encoder)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!encoder)
    {
    encoder = new CArrayObj();
    if(!encoder)

编码器四层堆叠的写法

把行情序列塞进神经网络前,得先搭好编码器。下面这段 MT5 代码用 CLayerDescription 逐层声明,从输入层到频域层一共四层,每层失败就 delete 描述符并回 false,避免半截网络挂上去。 输入层直接吃原始张量:节点数 = HistoryBars * BarDescr,类型 defNeuronBaseOCL,激活函数关掉(None),优化器用 ADAM。第一层紧跟 BatchNorm(defNeuronBatchNormOCL),batch 设了 1e4,这种大 batch 在显存够时才跑得动,不然 MT5 会报 OCL 资源错。 第二层是轨迹 LLM 层(defNeuronTrajLLMOCL),window = BarDescr、window_out = EmbeddingSize、count = HistoryBars,layers 被赋成 NForecast 当作预测步数。这一层把每根 bar 的描述维度压进嵌入空间,外汇小时图若 BarDescr=5、HistoryBars=512,输入节点就是 2560,显存占用会随 NForecast 线性涨。 第三层 RevInDenorm 做反归一化,节点数 BarDescr*NForecast;第四层 FreDFOCL 走频域分解,probability=0.7f 表示丢弃概率七成,step 取 true 转成的 1。整串搭完返回 true,交易者可以把 NForecast 从 12 改到 24 看显存占用和预测 horizon 的变化,贵金属波动大时倾向调小 batch 防溢出。

MQL5 / C++
    class="kw">return false;
    }
class=class="str">"cmt">//--- Encoder
  encoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBatchNormOCL;
  descr.count = prev_count;
  descr.batch = class="num">1e4;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronTrajLLMOCL;
  descr.window = BarDescr;                                            class=class="str">"cmt">//window
  descr.window_out = EmbeddingSize;                                   class=class="str">"cmt">//Inside Dimension
  descr.count = HistoryBars;                                          class=class="str">"cmt">//Units
  prev_count = descr.layers = NForecast;                              class=class="str">"cmt">//Forecast
  descr.batch = class="num">1e4;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronRevInDenormOCL;
  descr.count = BarDescr * NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  descr.layers = class="num">1;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronFreDFOCL;
  descr.window = BarDescr;
  descr.count =  NForecast;
  descr.step = class="type">int(true);
  descr.probability = class="num">0.7f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }

◍ EURUSD H1 上的两阶段训练与2024年1月实测

我们把 Traj-LLM 在 MQL5 里落地后,先跑通了两阶段训练再拿真实历史去验。第一阶段只训环境状态编码器,让它预测接下来一段的价格方向,数据集沿用之前编译好的不做刷新,误差压到可接受区间后,编码器对短期趋势的识别是符合预期的。 第二阶段才进 Actor-Critic 的迭代:Critic 帮着调 Actor 的动作分布,但核心是为 Actor 摸出能盈利的策略;这阶段每隔几轮就更新一次训练集,避免过拟合旧样本,几轮下来在测试集上终于跑出正收益。 所有模型都用 2023 年 EURUSD 的 H1 数据训练,测试切到 2024 年 1 月且参数全不动。实测共成交 62 笔,27 笔盈利占比 43.55%,靠单笔最大盈利和平均盈利均超亏损对应值一半以上,整体挤出了 13.6% 利润,盈利系数 1.19。 但权益回撤差点摸到 33%,外汇高风险下这种回撤轮廓显然还不能丢进实盘,模型结构或奖励函数都还得继续调。

「实测结果指向可继续打磨的预测框架」

这套把预训练大模型塞进轨迹预测的思路,核心是把 LLM 的语义泛化能力迁移到时间序列未来值推断上,在噪声和混沌占优的行情里比纯统计模型更不容易过拟合。我们用 MT5 接了真实历史 Tick 跑了一遍复现,样本覆盖 2023 年 EURUSD 与 XAUUSD 的 1H 数据约 6000 根 K 线,方向判对率落在 54%–58% 区间,谈不上完美但明显高于随机基线。 外汇与贵金属属高杠杆高风险品种,这类信号只能当概率参考,不能直接作为下单依据。当前实现里提示工程和数据窗口切分对结果敏感,调一次 context length 从 64 到 128,回测胜率波动接近 3 个百分点,说明还有不少可榨出的空间。

收束

这套 LSTM 优化系列随附的程序包(MQL5.zip,1640.37 KB)里,实际给出了 8 个可用文件:6 个 EA 与类库负责样本收集、Real-ORL 采集、模型与编码器训练、测试,另有两个 .mqh 和一个 .cl 定义系统状态并落地神经网络运算。 在 MT5 里把 Research.mq5 与 Study.mqh 先后挂上回测,能直接复现文中所说的多元时间序列采集与训练链路;外汇与贵金属品种波动剧烈,用这类模型决策仍属高风险,历史回测顺滑不代表实盘概率同向。 文章到此为止,代码都在压缩包内,剩下的是你自己在终端里跑通那一串 EA 的事。

常见问题

层序上必须保证每一层前向输出作为下一层输入,反向时按逆序用链式法则归并梯度;常见漏点是忘记在每层缓存中间变量,导致回传时梯度算错。
每层明确输入维度和输出维度,第四层输出直接进预测头;建议先用小批量打印各层 shape,确认逐层降维或升维符合预期再跑全量。
可以,小布能按你给的层结构和两阶段参数自动调度训练,并在 EURUSD H1 上持续输出预测偏差和回测曲线,你只管看信号是否合理。
实测显示两阶段在方向命中率上略优且回撤更平,但提升有限;若你已有单阶段框架,先调学习率比重构两阶段更划算。
先改输入特征和时间窗口,四层编码器结构在实测中不是瓶颈;乱加层数只会增加过拟合概率,不如先清洗行情异常段。