交易中的神经网络:状态空间模型·进阶篇
🧠

交易中的神经网络:状态空间模型·进阶篇

(2/3)·自注意力遇上限长窗口与二次缩放,SSM 如何以线性成本留住长期依赖

含代码示例实战向 第 2/3 篇
把 transformer 直接搬进行情序列,常卡在有限上下文窗口和随长度平方暴涨的计算量。状态空间模型把 RNN 与 CNN 的长处揉在一起,用近线性缩放处理长周期数据,更适合 tick 级时序。

◍ Mamba 神经元的权重更新与类结构

在 SSMOCL 的反馈阶段,若未处于训练模式(bTrain 为假),会直接调用 SumAndNormilize 对 cA、cB 与隐藏状态做归一化,失败时返回 false,否则返回 true。这一步跳过了梯度累积,适合推理或前向验证场景。 updateInputWeights 里依次更新 cA 指向隐藏状态的权重、做归一化、再更新 cB 与 cC 的入边权重,任何一环失败立即退出。这种串行的失败即返结构,能保证 OpenCL 显存里的中间张量状态一致,避免半更新导致的数值漂移。 CNeuronMambaOCL 类继承了 CNeuronBaseOCL,内部挂了 6 个组件:cXProject、cZProject、cInsideConv 三个卷积层,cSSM 状态空间模块,cZSSM 与 cOutProject。Temp 缓冲用于暂存前向中间结果,减少重复分配。 Init 函数中,基类用 window*units_count 作为展开维度初始化;随后 cXProject 以 window_key+2 作为卷积核宽度、units_count 为输出通道数拉起。把 window_key 加 2 通常是为了容纳左右 padding 或前后状态偏移,具体效果可在 MT5 策略测试器里改这个数看预测延迟变化。

MQL5 / C++
if(!bTrain)
   if(!SumAndNormilize(cA.getOutput(), cB.getOutput(), cHiddenStates.getOutput(), iWindowHidden, true))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronSSMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!cA.UpdateInputWeights(cHiddenStates.AsObject()))
     class="kw">return false;
  if(!SumAndNormilize(cA.getOutput(), cB.getOutput(), cHiddenStates.getOutput(), iWindowHidden, true))
     class="kw">return false;
  if(!cB.UpdateInputWeights(NeuronOCL))
     class="kw">return false;
  if(!cC.UpdateInputWeights(cAB.AsObject()))
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class CNeuronMambaOCL   :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
  CNeuronConvOCL    cXProject;
  CNeuronConvOCL    cZProject;
  CNeuronConvOCL    cInsideConv;
  CNeuronSSMOCL     cSSM;
  CNeuronBaseOCL    cZSSM;
  CNeuronConvOCL    cOutProject;
  CBufferFloat      Temp;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class=class="str">"cmt">//---
class="kw">public:
                     CNeuronMambaOCL(class="type">void) {};
                    ~CNeuronMambaOCL(class="type">void) {};
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint window, class="type">uint window_key, class="type">uint units_count,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void)  const   { class="kw">return defNeuronMambaOCL; }
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Save(class="type">int const file_handle);
  class="kw">virtual class="type">bool      Load(class="type">int const file_handle);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau);
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
  };
class="type">bool CNeuronMambaOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                          class="type">uint window, class="type">uint window_key, class="type">uint units_count,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
     class="kw">return false;
  if(!cXProject.Init(class="num">0, class="num">0, OpenCL, window, window, window_key + class="num">2, units_count, class="num">1, optimization, iBatch))

「Mamba 编码层的初始化与前向链路」

在 MT5 的 OpenCL 神经网络封装里,CNeuronMambaOCL 的 init 把投影、卷积与 SSM 子模块逐一定义:cXProject 与 cZProject 用线性投影,激活函数分别设为 None 与 SIGMOID;cInsideConv 以窗口参数 (3,1,1) 做一维卷积,同样接 SIGMOID。 cSSM 与 cZSSM 承担状态空间计算,后者输入维度被拉到 2*window_key*units_count,激活置为 None;cOutProject 再把拼接后的特征投回 window 维度,无激活直接出结果。任一子模块 Init 返回 false 都会让整层初始化中断并返回 false。 feedForward 的调用顺序很固定:先跑两个投影,再用 cXProject 的输出喂给卷积,卷积结果进 cSSM,随后把 cSSM 与 cZProject 的输出做 Concat 送入 cZSSM,最后由 cOutProject 出层输出。想验证的话,在 MT5 策略测试器里给 HistoryBars 设 50、BarDescr 设 4,输入层节点数会落到 200(50×4),优化器默认 ADAM。 CreateEncoderDescriptions 负责把描述对象塞进数组:若 encoder 为空就 new 一个 CArrayObj,清表后先加输入层 descr,type 写 defNeuronBaseOCL、激活 None、优化 ADAM,prev_count 记录上一层节点数供后续层引用。

MQL5 / C++
  class="kw">return false;
   cXProject.SetActivationFunction(None);
   if(!cZProject.Init(class="num">0, class="num">1, OpenCL, window, window, window_key, units_count, class="num">1, optimization, iBatch))
      class="kw">return false;
   cZProject.SetActivationFunction(SIGMOID);
   if(!cInsideConv.Init(class="num">0, class="num">2, OpenCL, class="num">3, class="num">1, class="num">1, window_key, units_count, optimization, iBatch))
      class="kw">return false;
   cInsideConv.SetActivationFunction(SIGMOID);
   if(!cSSM.Init(class="num">0, class="num">3, OpenCL, window_key, window_key, units_count, optimization, iBatch))
      class="kw">return false;
   if(!cZSSM.Init(class="num">0, class="num">4, OpenCL, class="num">2 * window_key * units_count, optimization, iBatch))
      class="kw">return false;
   cZSSM.SetActivationFunction(None);
   if(!cOutProject.Init(class="num">0, class="num">5, OpenCL, class="num">2*window_key, class="num">2*window_key, window, units_count, class="num">1, optimization, iBatch))
      class="kw">return false;
   cOutProject.SetActivationFunction(None);
   if(!Temp.BufferInit(window * units_count, class="num">0))
      class="kw">return false;
   if(!Temp.BufferCreate(OpenCL))
      class="kw">return false;
   if(!SetOutput(cOutProject.getOutput()))
      class="kw">return false;
   if(!SetGradient(cOutProject.getGradient()))
      class="kw">return false;
   SetActivationFunction(None);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronMambaOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
   if(!cXProject.FeedForward(NeuronOCL))
      class="kw">return false;
   if(!cZProject.FeedForward(NeuronOCL))
      class="kw">return false;
   if(!cInsideConv.FeedForward(cXProject.AsObject()))
      class="kw">return false;
   if(!cSSM.FeedForward(cInsideConv.AsObject()))
      class="kw">return false;
   if(!Concat(cSSM.getOutput(), cZProject.getOutput(), cZSSM.getOutput(), class="num">1, class="num">1, cSSM.Neurons()))
      class="kw">return false;
   if(!cOutProject.FeedForward(cZSSM.AsObject()))
      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)
         class="kw">return false;
      }
class=class="str">"cmt">//---
   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;
      }

编码器后半段的层堆叠与维度变换

这段配置从 layer 1 一路铺到 layer 9,核心是把历史 K 线张量逐步压缩再展开成预测窗口。layer 1 先放一个 BatchNorm 层,batch 设成 1e4、优化器走 ADAM,不做激活;紧接着塞进 MambaOCL 层,window 取单根 Bar 描述长度,window_out 拉到 4 倍 BarDescr,循环里连续 Add 三次(i=2 到 4),Units 数量直接绑定 HistoryBars。 layer 5 用 TransposeOCL 做轴交换,count 和 window 都回到 BarDescr;之后 layer 6、7 是两记 ConvOCL:第一层 window_out = 4*NForecast 且用 LReLU,第二层收到 NForecast 并切 TANH,把输出规整到预测步数内。 layer 8 再转置一次,window 设为 NForecast,无激活;到 layer 9 时只 new 了描述对象还没填字段就截断在原文里。外汇与贵金属行情下用这类结构做推理,过拟合和滑点风险都偏高,参数务必在 MT5 用历史数据先跑通内存与维度。 下面把前半段关键代码逐行拆一下: //--- layer 1 if(!(descr = new CLayerDescription())) // 尝试新建层描述对象,失败直接返回 false return false; descr.type = defNeuronBatchNormOCL; // 层类型:OpenCL 批归一化 descr.count = prev_count; // 神经元数沿用上一层输出 descr.batch = 1e4; // 批大小 10000 descr.activation = None; // 无激活函数 descr.optimization = ADAM; // 优化器 ADAM if(!encoder.Add(descr)) // 加入编码器,失败则 { delete descr; // 释放描述对象 return false; // 返回失败 } if(!(descr = new CLayerDescription())) // 再建一个新描述 return false; descr.type = defNeuronMambaOCL; // Mamba 序列层 descr.window = BarDescr; // 输入窗口=单Bar描述维度 descr.window_out = 4 * BarDescr; // 内部维度扩 4 倍 prev_count = descr.count = HistoryBars; // 单元数=历史Bar数 descr.batch = 1e4; // 同样万级批 descr.activation = None; descr.optimization = ADAM; for(int i = 2; i <= 4; i++) // 重复添加 3 次同配置 if(!encoder.Add(descr)) { delete descr; return false; }

MQL5 / C++
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;
     }
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMambaOCL;
   descr.window = BarDescr;                                                                     class=class="str">"cmt">//window
   descr.window_out = class="num">4 * BarDescr;                                                             class=class="str">"cmt">//Inside Dimension
   prev_count = descr.count = HistoryBars;                                                      class=class="str">"cmt">//Units
   descr.batch = class="num">1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   for(class="type">int i = class="num">2; i <= class="num">4; i++)
      if(!encoder.Add(descr))
        {
         class="kw">delete descr;
         class="kw">return false;
        }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   descr.window = 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">6
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = BarDescr;
   descr.window = prev_count;
   descr.window_out = class="num">4 * NForecast;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = BarDescr;
   descr.window = class="num">4 * NForecast;
   descr.window_out = NForecast;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">8
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = BarDescr;
   descr.window = NForecast;
   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">9
   if(!(descr = new CLayerDescription()))
      class="kw">return false;

◍ 编码层里的逆归一与丢弃概率

这段 CLayerDescription 配置直接决定了编码器尾部两层的行为。第 9 层把 type 设成 defNeuronRevInDenormOCL,作用是把前面归一化的输入反向还原,count 取 BarDescr 乘 NForecast,单层、无激活、用 ADAM 优化。 第 10 层换成 defNeuronFreDFOCL,window 等于 BarDescr,count 压缩到 NForecast,step 写成 int(true) 即 1,probability 给了 0.7f。这个 0.7 的丢弃概率意味着训练时约七成神经元可能被随机屏蔽,是防止过拟合的硬参数。 两层都走 encoder.Add(descr),失败就 delete 并 return false,最后成功才 return true。开 MT5 把 probability 从 0.7 调到 0.5 跑同一段样本,能直观看到收敛曲线变抖,说明正则强度在起作用。外汇与贵金属模型训练波动大,高概率丢弃不保证泛化更好,需结合验证集判断。

MQL5 / C++
  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">10
  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;
  }

「用 2023 EURUSD 数据跑通的两阶段验证」

模型先用 2023 年 EURUSD 的 H1 历史数据做训练,指标全取默认值。第一阶段只训状态编码器去预测未来横向区间的价格走向,不掺任何参与者动作,所以旧数据集能直接全量复用,除非训练窗口拉长或变动才需重采。 编码器本身很紧凑,训练稳且快;预测图上能看出:一张平滑示意趋势转折,另一张近乎线性跟随行进中的趋势。 第二阶段迭代训参与者策略,同时训评论者数值函数做效率引导。这阶段必须周期刷新训练集,把当前策略产生的新数据补进去,否则模型会训偏。 实盘回测落在 2024 年 1 月:52 笔交易,27 笔盈利,胜率近 52%;平均盈利 39.36 点,平均亏损 -29.82 点,但最大亏损比最大盈利多约 30%,净值回撤超 35%。模型能盈利却无持续增长趋势,明显还需调参。 按周期拆,周五整体赚、周三亏;日内某些时段盈亏集中堆叠。平均持仓刚过 1 小时、最长 2 小时,这种时段聚集现象值得单独拉出来深挖。外汇与贵金属属高风险品类,上述回测仅反映历史样本,实盘可能偏离。

把序列诊断交给小布盯盘
小布盯盘的 AIGC 已内置对长周期依赖的初步诊断,打开对应品种页即可看到状态压缩效果的参考曲线,你只需判断参数是否过拟合。

常见问题

SSM 通过结构化状态转移兼顾卷积并行训练与递归推理,推理每步恒定时间,而传统 RNN 状态压缩能力弱、易丢长期上下文。
它让 SSM 参数依赖输入,对无关标记降低权重、对相关标记无限期保留,从而在特定模态中建模长期依赖,概率上提升信噪比。
目前小布提供的是基于同构思路的轻量参考指标,完整自定义训练仍需你在 MT5 端部署代码,贵金属与外汇均属高风险品类。
其计算与内存随窗口长度二次缩放,超出有限窗口无法建模,长序列下显存与时间开销急剧上升。
它要求模型记住相关标记并忽略无关标记,对应到交易即筛选有效突破、丢弃杂波,可能改善信号触发质量。