交易中的神经网络:基于双注意力的趋势预测模型(基础篇)
📘

交易中的神经网络:基于双注意力的趋势预测模型(基础篇)

第 1/3 篇

「双注意力网络在 MT5 里的趋势预测骨架」

把 Transformer 的注意力机制搬进 MT5,核心不是堆层数,而是用两套注意力分别盯「时间维度」和「特征维度」。时间注意力负责判断哪根 K 线对当前状态更重要,特征注意力决定收盘价、成交量、波动率里谁在主导行情。 作者在 2025 年 3 月发布的模型,在 EURUSD 的 H1 周期上做了样本外测试,约 1147 次前向推理的平均方向判断延迟为 1.8 根 K 线,意味着信号通常比价格拐点晚不到两根小时线。外汇与贵金属属高风险品种,此类延迟在跳空行情中可能放大,需自行在 MT5 策略测试器复核。 落地动作很直接:开 MT5 → 打开 MetaEditor → 新建 EA → 把下列双注意力前向代码贴入 OnTick 前声明区,先跑 2024 年整年 tick 数据看信号分布,再谈调参。

MQL5 / C++
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 的数据流,就能复现编码器前馈。外汇/贵金属样本若直接套,注意递归模型对序列顺序极敏感,小样本易过拟合,回测概率而非确定性。

MQL5 / C++
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 对序列排列的敏感度。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必小仓位验证。

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

常见问题

普通均线隐含线性假设,双注意力分别抓价格与成交量特征的交互,对突变响应更快;可先拿历史行情回测胜率差异再上实盘。
价格序列走时间注意力、量能序列走特征注意力,两者编码器输出拼接;建议先打印各分支维度确认没串层再训练。
小布可加载你导出的品种数据自动跑 TPM 骨架并标出注意力权重异常段,你只需复核信号,不用从头搭类。
顺序错会导致特征错位、预测偏移;用极小样本前向一次看输出形状与预期对齐即可定位。
贵金属跳空多,建议先用1小时级双特征训练,再切15分钟微调;外汇贵金属高风险,实盘前务必样本外验证。