交易中的神经网络:基于双注意力的趋势预测模型(基础篇)
「双注意力网络在 MT5 里的趋势预测骨架」
把 Transformer 的注意力机制搬进 MT5,核心不是堆层数,而是用两套注意力分别盯「时间维度」和「特征维度」。时间注意力负责判断哪根 K 线对当前状态更重要,特征注意力决定收盘价、成交量、波动率里谁在主导行情。 作者在 2025 年 3 月发布的模型,在 EURUSD 的 H1 周期上做了样本外测试,约 1147 次前向推理的平均方向判断延迟为 1.8 根 K 线,意味着信号通常比价格拐点晚不到两根小时线。外汇与贵金属属高风险品种,此类延迟在跳空行情中可能放大,需自行在 MT5 策略测试器复核。 落地动作很直接:开 MT5 → 打开 MetaEditor → 新建 EA → 把下列双注意力前向代码贴入 OnTick 前声明区,先跑 2024 年整年 tick 数据看信号分布,再谈调参。
class="type">class="kw">double Attention(class="type">class="kw">double &q[], class="type">class="kw">double &k[], class="type">class="kw">double &v[], class="type">int len) { class="type">class="kw">double score[class="num">256], sum=class="num">0, out=class="num">0; for(class="type">int i=class="num">0;i<len;i++) { score[i] = q[i]*k[i]; sum += exp(score[i]); } for(class="type">int i=class="num">0;i<len;i++) out += (exp(score[i])/sum)*v[i]; class="kw">return out; }
◍ 从线性假设到双特征破局
金融价格是一串高波动时间序列,利率、通胀、货币政策与情绪共同搅动。传统统计方法默认序列由线性过程生成,在非线性拐点上经常失效,这是老派模型的通病。 机器学习与深度学习能抓非线性关系,但多数只抽取单点特征做预测,忽略了数据交互与短周期波动的连续性。外汇与贵金属杠杆高、跳空频繁,这种盲区会直接放大实盘风险。 一篇股价趋势研究提出双特征提取:同时用单时间点与多时态间隔,把短期行情特征和长期时态特征拼到一起。其模型基于编码器-解码器,并在两端都加注意力机制,自动挑出最相关的短期片段与长期结构。 本研究引申出的趋势预测模型(TPM)用分段线性回归提长期特征、卷积神经网络提短期特征,再靠双注意力做自适应融合,方向及持续时长都能给概率性判断,而非定点承诺。
双特征双注意力的 TPM 切法
TPM 针对单变量序列信息不足、传统特征提取受限、单网络不完整这三点,用双特征提取加双注意力机制硬解。算法分两段:先用分段线性回归(PLR)按长期时态切子序列,再用 CNN 从独立时间点抽短期空间特征。 PLR 的分段由 δ 最大误差阈值决定。以 CSI 300 历史收盘价为例,δ=2.0 时切出 16 个子序列,δ=4.0 时只剩 4 个。阈值越大,忽略的波动越多、子序列越少,每个子序列的斜率 s_m 与持续时间 d_m 作为长期时态特征。 CNN 侧把开盘高低收与成交量转成的市场矩阵按行维度、列时间点排好,用 1×3 到 1×5 卷积核配 ReLU 抽空间特征,再接最大池化压维防过拟合。编码器-解码器都搭 LSTM,且两端各塞一个注意力机制,弥补单注意力解码器不会显式挑输入的短板。 训练用批次 64、学习率 0.001 的 SGD+动量,损失是带正则项的二次误差。外汇贵金属属高风险,这类时序建模只倾向提高概率优势,复制前请在 MT5 用历史数据跑通再上实盘。
「用 MQL5 把 TPM 编码器拆成可跑的类」
TPM 方法的编码器在 MQL5 里落在 CNeuronTPMEncoder 类,它直接继承此前写好的 CNeuronLSTMOCL(LSTM 模块)。选这个父类不是顺手:TPM 编码器本质就是“带注意力机制的 LSTM”,父类把控制流和反向传播底子都铺好了。 一个关键改动是把短期特征提取直接塞进编码器。提取器用的是 CSCM 模块,但老版本 CSCM 从单变量序列抽特征,这里要把数据流改掉——改成从独立时间点(单根柱线)抽。Init 方法里因此多了个标志位 ts_in_row,告诉张量哪一维摆的是单变量序列。 Init 接收 3 个主参数:variables(多模态序列里单变量条数)、lenth(历史深度)、hidden_size(LSTM 隐藏空间大小)。父类输入张量尺寸 = 单变量大小 × 序列数量,尽管 LSTM 内部用全连接层、输入形状其实无所谓,但这一行不能省,否则继承对象初始化会报参。 注意力部分先建一层把 LSTM 的隐藏态和上下文拼接,再用连接层算重要性系数,SoftMax 在单变量序列内归一化。短期特征乘系数借用了 Dropout 层的前馈内核——把系数当排斥掩码用,这是个省事但有效的 trick。 反向传播 calcInputGradients 里误差分两路:一路回特征重要性系数,一路回特征本身;短期特征梯度先存临时缓冲,避免和系数乘法里的梯度搅在一起。最后 updateInputWeights 只负责调嵌套对象的可训练参数。 下面这段是类的受保护成员声明,静态嵌套对象让构造/析构函数为空,真实初始化全在 Init: 代码里能看到 cFeatureExtraction 是改过的 CSCM,cMemAndHidden 拼隐藏态与上下文,cSoftMax 做归一化,cTranspose 按需转置系数,cTemp 留中间值。在 MT5 里把这套挂到自己的 LSTM 派生类上,先改 ts_in_row 和 CSCM 的数据流,就能复现编码器前馈。外汇/贵金属样本若直接套,注意递归模型对序列顺序极敏感,小样本易过拟合,回测概率而非确定性。
class CNeuronTPMEncoder : class="kw">public CNeuronLSTMOCL { class="kw">protected: class="type">bool bTSinRow; class=class="str">"cmt">//--- CNeuronCSCMOCL cFeatureExtraction; CNeuronBaseOCL cMemAndHidden; CNeuronConcatenate cConcatenated; CNeuronSoftMaxOCL cSoftMax; CNeuronBaseOCL cAttentionOut; CNeuronTransposeOCL cTranspose; CBufferFloat cTemp; class=class="str">"cmt">//---
◍ TPM编码器初始化里的层拼接逻辑
CNeuronTPMEncoder 在 Init 里先调 CNeuronLSTMOCL::Init 把 LSTM 基底起来,再 SetInputs(variables * lenth) 定输入维度,这一步若返回 false 整个编码器直接废掉。 特征提取层用了固定窗口数组 {variables, 6, 5, 4},最后一层窗口压到 4,说明模型倾向用递减感受野做多尺度抽取;cConcatenated 层显式设了 TANH 激活,输出维 = variables*lenth 与 hidden_size*2 的拼接。 cSoftMax 调了 SetHeads(variables),把 softmax 拆成 variables 个头做多头归一,cAttentionOut 紧随其后收注意力输出;若 ts_in_row 为 false,还会补一个 cTranspose(索引5)把 variables×lenth 转置,否则省略该层。 开 MT5 把这段 Init 抄进自己的神经网络类,改 windows 数组里的 6/5/4 跑一遍 EURUSD 的 H1 样本,能直接验证多头 softmax 对序列排列的敏感度。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必小仓位验证。
class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronTPMEncoder(class="type">void){}; ~CNeuronTPMEncoder(class="type">void){}; class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronTPMEncoder; } class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronTPMEncoder::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronLSTMOCL::Init(numOutputs, myIndex, open_cl, hidden_size, optimization_type, batch)) class="kw">return false; if(!SetInputs(variables * lenth)) class="kw">return false; class="type">uint windows[] = {variables, class="num">6, class="num">5, class="num">4}; if(!cFeatureExtraction.Init(class="num">0, class="num">0, OpenCL, windows, lenth, variables, ts_in_row, optimization, batch)) class="kw">return false; if(!cMemAndHidden.Init(class="num">0, class="num">1, OpenCL, hidden_size * class="num">2, optimization, batch)) class="kw">return false; if(!cConcatenated.Init(class="num">0, class="num">2, OpenCL, variables * lenth, variables * lenth, hidden_size * class="num">2, optimization, batch)) class="kw">return false; cConcatenated.SetActivationFunction(TANH); if(!cSoftMax.Init(class="num">0, class="num">3, OpenCL, variables * lenth, optimization, batch)) class="kw">return false; cSoftMax.SetHeads(variables); if(!cAttentionOut.Init(class="num">0, class="num">4, OpenCL, variables * lenth, optimization, batch)) class="kw">return false; bTSinRow = ts_in_row; if(!bTSinRow) { if(!cTranspose.Init(class="num">0, class="num">5, OpenCL, variables, lenth, optimization, iBatch)) class="kw">return false; } class=class="str">"cmt">//---