交易中的神经网络:状态空间模型(基础篇)
📘

交易中的神经网络:状态空间模型(基础篇)

第 1/3 篇

用状态空间模型给行情建模

传统均线类指标把价格当成静态序列处理,容易在趋势切换时滞后。状态空间模型(State Space Model)把市场看作一个隐含状态随时间演化的动态系统,观测到的报价只是隐藏状态的噪声投影。 在 MQL5 里可以用卡尔曼滤波近似这类模型:先定义状态转移矩阵与观测矩阵,再递推预测与修正。下面这段演示了单变量水平+斜率模型的最小实现。 外汇与贵金属杠杆高、跳空频繁,隐含状态估计可能在大事件窗口失效,任何信号都只是概率倾向,需配合仓位控制。

MQL5 / C++
class="type">class="kw">double KalmanFilter(class="type">class="kw">double measurement, class="type">class="kw">double &x, class="type">class="kw">double &p, class="type">class="kw">double q, class="type">class="kw">double r)
{
   p = p + q;                 class=class="str">"cmt">// 预测协方差
   class="type">class="kw">double k = p / (p + r);    class=class="str">"cmt">// 卡尔曼增益
   x = x + k * (measurement - x); class=class="str">"cmt">// 状态修正
   p = (class="num">1 - k) * p;           class=class="str">"cmt">// 更新协方差
   class="kw">return x;                  class=class="str">"cmt">// 返回滤波后状态
}

「从变换器瓶颈到选择性状态空间」

近一年大模型适配新任务的主流做法,是先拿跨模态原生数据(文本、图像、音频、时间序列等)做预训练,再微调到具体任务。这套范式大多压在变换器架构上,核心是自注意力层。自注意力能在有限上下文窗口里密集路由信息,但对窗口外的内容无能为力,且计算量随窗口长度呈二次方增长,序列一长就吃力。 另一条路是用结构化状态空间模型(SSM)做序列建模,本质可看作 RNN 与 CNN 的杂交。SSM 按序列长度线性或近线性缩放,还能内建对长期依赖的建模机制,不依赖固定窗口。 Mamba 论文点出的关键是:旧模型不会按输入内容做信息筛选。作者让 SSM 参数直接由输入数据决定,不相关的就滤掉,相关的无限期保留,再把 SSM 与 MLP 压进单一同构模块。结果是全递归的选择性 SSM,具备三个硬指标:密集模态下高性能;训练计算和内存随长度线性走,推理时每步恒定时间、不缓存历史;长序列上质量与效率不互损。外汇与贵金属行情序列长、噪声多,这类模型处理高频 tick 流时可能比自注意力更省资源,但实盘信号延迟与过拟合风险仍高,需 MT5 接数据自测。

◍ 曼巴靠选择性状态压缩换效率

序列建模的本质难题,是把上下文压进一个更小的状态里。注意力机制不显式压缩上下文,自回归推理必须存全部键值缓存,导致推理线性、训练二次;递归模型只维护有限状态,推理恒定、训练线性,但性能上限受状态压缩能力约束。 曼巴作者用两个合成任务点破 LTI(线性时不变)模型的故障:选择性复制要求内容感知、过滤噪声;感应头要求情境依赖推理。固定动态的 LTI 模型既选不出正确信息,也改不了隐藏状态随输入的传播方式,静态卷积核更建模不了可变距离。 破法是把参数变成输入的函数。Δ、B、C 不再固定,而是对输入做 Linear 映射,Δ 再走 SoftPlus,张量形状多出长度维 L,模型从非时变转为时变。这就把「选择性」塞进了 SSM:依据内容决定记住或丢弃哪个顺序状态。 效率上仍有坑。朴素循环算 O(BLDN) FLOP、卷积算 O(BLD log L) FLOP,前者系数低,长序列且 N 不大时循环反而更省;但递归要存全状态 h,内存吃紧。作者用 GPU 内核融合把 h 算在更快的内存层级、减少 I/O,再用逆向重计算跳过中间状态存储,速度显著提升、显存下降。 实证上,很多序列模型随上下文变长并不变强,因为不会忽略无关语境;选择性模型可随时重置状态、丢垃圾历史,长语境下性能倾向单向改善。外汇与贵金属行情里这种噪声令牌极多,拿曼巴类结构做序列特征提取时,先在小样本 tick 上验证状态重置频率再上实盘,属高风险尝试。

在 MT5 里搭一个最简 SSM 层

把选择性状态空间模型(SSM)塞进 MT5 的 OpenCL 神经层体系,第一步是写一个继承自 CNeuronBaseOCL 的 CNeuronSSMOCL 类。原文实验刻意没做多通道切分,只保留单序列元素内部的隐藏状态,属于曼巴方法的极简版,方便在终端里先跑通再扩。 类里静态声明了 5 个内部层:cHiddenStates 存隐藏状态(关掉激活,纯临时缓冲)、cA 与 cB 是两个卷积投影层、cAB 做拼接后的加权投影、cC 出最终结果。Init 方法接收 window、window_key、units_count 三个参数,先调父类初始化,再按 window_key 决定隐藏层维度——若用户传错值,内部层初始化直接报错,相当于隐式校验。 前向 feedForward 里只做双路投影和拼接,不更新隐藏状态;隐藏状态更新被推迟到反向传播,但推理模式(deploy)下会在前向补一步「旧状态+输入投影」的归一化,否则部署时状态永远不滚,逻辑就破了。训练时 updateInputWeights 先调 cA 参数再刷新隐藏状态,顺序不能反。 下面这段是类的骨架声明,对应上文说的常量和层对象布局,可直接贴进 MT5 代码看结构:

MQL5 / C++
class CNeuronSSMOCL  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint              iWindowHidden;
   CNeuronBaseOCL    cHiddenStates;
   CNeuronConvOCL    cA;
   CNeuronConvOCL    cB;
   CNeuronBaseOCL    cAB;
   CNeuronConvOCL    cC;
   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:

「SSMO 神经元的 OpenCL 初始化与前向链路」

CNeuronSSMOCL 把状态空间建模搬到了 GPU 上跑。类声明里先看两个空构造/析构,真正干活的是 Init:它先调基类 CNeuronBaseOCL::Init,输入维度被乘了 units_count,说明特征不是单点而是按单元铺开。 cHiddenStates 作为隐藏状态容器,激活函数设成 None,窗口长度用 window_key 而非主窗口,这一步把“历史状态”和“当前输入”在维度上拆开。随后 cA、cB 两个 sigmoid 子层分别吃隐藏状态和外部神经元,cAB 做拼接且不用激活,最后 cC 用 None 激活输出——整个结构没有在末端压 softmax,倾向用于回归式状态推断而非分类。 feedForward 的顺序很固定:先推 cA 从隐藏状态,再推 cB 从入参神经元,拼接后送 cC。若任何一步返回 false 就中断,MT5 上若报 false,优先查 OpenCL 上下文是否随指标卸载被释放。外汇与贵金属行情下用此类 GPU 模型请意识到:过拟合与显存抖动都可能导致信号失效,属高风险用法。 想验证,把 window=12、window_key=6、units_count=8 填进 Init 参数,看 cC.getOutput 张量是否随每根 K 线刷新;若输出恒为 0,大概率是 batch 维度和 OpenCL 队列没对齐。

MQL5 / C++
CNeuronSSMOCL(class="type">void) {};
~CNeuronSSMOCL(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 defNeuronSSMOCL; }
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 CNeuronSSMOCL::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(!cHiddenStates.Init(class="num">0, class="num">0, OpenCL, window_key * units_count, optimization, iBatch))
    class="kw">return false;
  cHiddenStates.SetActivationFunction(None);
  iWindowHidden = window_key;
  if(!cA.Init(class="num">0, class="num">1, OpenCL, iWindowHidden, iWindowHidden, iWindowHidden, units_count, class="num">1, optimization, iBatch))
    class="kw">return false;
  cA.SetActivationFunction(SIGMOID);
  if(!cB.Init(class="num">0, class="num">2, OpenCL, window, window, iWindowHidden, units_count, class="num">1, optimization, iBatch))
    class="kw">return false;
  cB.SetActivationFunction(SIGMOID);
  if(!cAB.Init(class="num">0, class="num">3, OpenCL, class="num">2 * iWindowHidden * units_count, optimization, iBatch))
    class="kw">return false;
  cAB.SetActivationFunction(None);
  if(!cC.Init(class="num">0, class="num">4, OpenCL, class="num">2*iWindowHidden, class="num">2*iWindowHidden, window, units_count, class="num">1, optimization, iBatch))
    class="kw">return false;
  cC.SetActivationFunction(None);
  SetActivationFunction(None);
  if(!SetOutput(cC.getOutput()) || !SetGradient(cC.getGradient()))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronSSMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(!cA.FeedForward(cHiddenStates.AsObject()))
    class="kw">return false;
  if(!cB.FeedForward(NeuronOCL))
    class="kw">return false;
  if(!Concat(cA.getOutput(), cB.getOutput(), cAB.getOutput(), iWindowHidden, iWindowHidden,
cA.Neurons() / iWindowHidden))
    class="kw">return false;
  if(!cC.FeedForward(cAB.AsObject()))
    class="kw">return false;
  }

常见问题

SSM 用连续状态递推保留历史依赖,对突变行情的滞后比普通EMA小;可用日线欧美对回看,拐头延迟通常少 1~2 根 K。
自注意力对长序列显存和计算平方增长,30 分钟黄金超过 500 根就明显掉帧;SSM 线性复杂度更适合本地跑。
小布会按品种页的波动结构给出是否偏线性可建模的提示,并把 SSM 平滑后的状态曲线叠在盘面上供你比对。
选择性机制按输入调遗忘率,极端一根大阴可能仍被压低权重;建议用 ATR 阈值强制保留异常帧再送模型。
先锁死状态维度 4、遗忘率 0.9,只扫观测噪声;周线回测中噪声 0.05 附近夏普较稳,再外推到其他周期验证。