交易中的神经网络:对比形态变换器(终章)(基础篇)
📘

交易中的神经网络:对比形态变换器(终章)(基础篇)

第 1/3 篇

「形态变换器在MT5里的落地起点」

把神经网络塞进MT5做价格形态识别,第一步不是调模型,而是搞清平台能喂给模型什么。MetaTrader 5 的 OnTick 与 OnCalculate 提供的是带时间戳的报价序列与指标缓冲,而非图像,所以所谓「形态变换器」在这里本质是把K线窗口编码成向量序列再跑注意力。 外汇与贵金属杠杆高、滑点随机,任何模型信号都只是概率倾向,不能直接当成交指令。实盘前务必用策略测试器跑至少 2020–2024 的 tick 级历史,观察不同波动 regime 下的误报率。 一个小布盯盘式的做法:先写一段脚本把最近 60 根收盘价归一化后打印,确认你的预处理和原文假设一致,再接变换器头。这样能避开九成「代码能跑但信号全错」的坑。

◍ 原子与基序双层级怎么统调

AMCT 框架把市场拆成两层看:原子层是单根烛条,基序层是它们拼出的复杂形态。核心假设是——同一段行情里,烛条和形态只是同一件事的两种说法,所以训练时让两种表征一起调参,能挖出单层看不到的额外信息,预测质量可能明显抬升。 跨周期或跨品种常出现长得像的形态,给出的信号也倾向相似。用对比学习去抓这些关键形态,并强化它们的解释力,是这套方法的关键一步。 为了判定趋势时不被噪声带偏,开发者加了属性感知注意力,配合之前写好的交叉注意力类,专门处理市场属性与烛条形态之间的依赖。上一篇文章里我们已经实现了烛条与形态管道,也建好了相对交叉注意力类,今天接着把这个依赖模块补完。

让模型自己学市场属性

传统技术分析把趋势切成上行、下行、横盘三类,但这种静态分类很难刻画价格运动的动态与强度。在 AMCT 框架思路下,与其手工定义「属性」,不如让模型从训练集里自主提取与任务相关的市场特征——类似 RefMask3D 里学到的语言基元,生成一个可学习的属性张量。 这套逻辑落在 CNeuronPropertyAwareAttention 类里。它以 CNeuronRMAT 为父类,父类内部组件封装在动态数组中,方便改架构而不动类声明。但交叉注意力要用两个独立输入源,不满足纯线性约束,所以必须重写前馈与反向传播的虚方法。 Init 方法里先调 CNeuronBaseOCL 的基础接口,再建动态数组存内部指针。头两个连续全连接层负责生成属性嵌入:第一层是固定值 1 的神经元,第二层产出可训练嵌入序列。之后按 layers 次数循环,每层塞入相对自注意力(序列长由 properties 决定)和相对交叉注意力(主输入是属性嵌入,FeedForward 长度也等于属性数),最后换数据缓冲区指针省训练开销。 feedForward 只收一个输入指针,因为「属性」源在类内部生成。训练期才跑第二层前馈去学嵌入;实盘跳过这步,直接复用已学嵌入,决策延迟由此下降。余下内层顺序跑前馈,形态嵌入作为附加参数进解码器,让输出聚焦最相关属性。 反向的 calcInputGradients 要镜像前馈但逆序。交叉注意力层都用了形态嵌入作第二输入,所以误差回传时得从每个交叉注意力模块累加梯度份额,再写回形态嵌入对象——这是线性模型默认覆盖不掉的多路径收集,得用临时缓冲区拐个弯。

MQL5 / C++
class CNeuronPropertyAwareAttention    :  class="kw">public CNeuronRMAT
  {
class="kw">protected:
   CBufferFloat      cTemp;
class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   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="kw">public:
                    CNeuronPropertyAwareAttention(class="type">void) {};
                   ~CNeuronPropertyAwareAttention(class="type">void) {};
   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 properties,
                          class="type">uint units_count, class="type">uint heads, class="type">uint layers,
                          ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---

「属性感知注意力层的初始化与前向链路」

属性感知注意力类在 Init 里先调基类 CNeuronBaseOCL::Init,把窗口长度 window 与属性维度 properties 相乘作为基础输入宽,这意味着一层就把多属性时序压成单张量送入后续子层。 随后代码手动堆出 layers 个「自注意力→交叉注意力→残差卷积」单元:自注意力用 window 和 window_key 做相对位置偏置,交叉注意力额外吃 units_count 个外部单元,残差卷积则保持 window×properties 形状不变。 初始化时有两个细节容易踩坑:第一层神经元硬编码输入宽 1、第二层输入宽 0,仅作占位与掩码用途,真正数据从 idx=2 起的循环子层才开始流动;任何子层 new 失败都会 delete 并返回 false,MT5 终端会在专家日志里直接报初始化中断。 feedForward 在训练态下只取 cLayers[1] 做前向锚点,说明第二层占位神经元同时承担梯度门控角色。外汇与贵金属行情用这类结构做特征提取时波动剧烈,过拟合概率偏高,建议先用小 window(如 32)和 heads=2 在策略测试器里跑样本内验证。

MQL5 / C++
class="kw">virtual class="type">int      Type(class="type">void) class="kw">override  const   { class="kw">return defNeuronPropertyAwareAttention; }
};
class="type">bool CNeuronPropertyAwareAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                          class="type">uint window, class="type">uint window_key, class="type">uint properties,
                                          class="type">uint units_count, class="type">uint heads, class="type">uint layers,
                                          ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * properties, optimization_type, batch))
      class="kw">return false;
   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);
   CNeuronBaseOCL *neuron=NULL;
   CNeuronRelativeSelfAttention *self_attention = NULL;
   CNeuronRelativeCrossAttention *cross_attention = NULL;
   CResidualConv *ff = NULL;
   class="type">int idx = class="num">0;
   neuron = new CNeuronBaseOCL();
   if (!neuron ||
      !neuron.Init(window * properties, idx, OpenCL, class="num">1, optimization, iBatch) ||
      !cLayers.Add(neuron))
      class="kw">return false;
   CBufferFloat *temp = neuron.getOutput();
   if (!temp.Fill(class="num">1))
      class="kw">return false;
   idx++;
   neuron = new CNeuronBaseOCL();
   if (!neuron ||
      !neuron.Init(class="num">0, idx, OpenCL, window * properties, optimization, iBatch) ||
      !cLayers.Add(neuron))
      class="kw">return false;
   for (class="type">uint i = class="num">0; i < layers; i++)
   {
      idx++;
      self_attention = new CNeuronRelativeSelfAttention();
      if (!self_attention ||
         !self_attention.Init(class="num">0, idx, OpenCL, window, window_key, properties, heads, optimization, iBatch) ||
         !cLayers.Add(self_attention)
      )
      {
         class="kw">delete self_attention;
         class="kw">return false;
      }
      idx++;
      cross_attention = new CNeuronRelativeCrossAttention();
      if (!cross_attention ||
         !cross_attention.Init(class="num">0, idx, OpenCL, window, window_key, properties, heads, window, units_count,
                               optimization, iBatch) ||
         !cLayers.Add(cross_attention)
      )
      {
         class="kw">delete cross_attention;
         class="kw">return false;
      }
      idx++;
      ff = new CResidualConv();
      if (!ff ||
         !ff.Init(class="num">0, idx, OpenCL, window, window, properties, optimization, iBatch) ||
         !cLayers.Add(ff)
      )
      {
         class="kw">delete ff;
         class="kw">return false;
      }
   }
   if (!SetOutput(ff.getOutput()) ||
      !SetGradient(ff.getGradient()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronPropertyAwareAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      class="kw">return false;
   CNeuronBaseOCL *neuron = NULL;
   if (bTrain)
   {
      neuron = cLayers[class="num">1];

常见问题

先用历史K线导出原子序列,搭一个最小双层级结构,只调注意力权重不调全局参数,能出图就说明链路通了。
先冻结基序层只训原子层,收敛后再放开联合微调,学习率设原子层为主、基序层减半,避免梯度冲突。
小布可读取品种波动与相关性特征,直接给出属性感知层的初始化建议,省去手动试错。
大概率会,建议用滚动窗口验证,属性向量加L2约束,回测区间至少跨两种波动率 regime。
初始化别全零,用正交矩阵铺底;前向时打印各头权重和,若某头恒等于均值说明梯度断了。