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