交易中的神经网络:TEMPO 方法的实施结果(基础篇)
📘

交易中的神经网络:TEMPO 方法的实施结果(基础篇)

第 1/3 篇

用 TEMPO 把神经网络搬进 MT5

  • 年 4 月 15 日发布的 TEMPO 方法,把时间序列的时序模式编码后直接喂给神经网络,在 MetaTrader 5 里做信号生成。作者 Dmitriy Gizlyk 的公开帖获得 730 次浏览、11 条讨论,说明这类做法在实盘圈已有一定关注度。

这套思路的核心不是堆层数,而是先把价格运动转成带时间偏移的特征向量,再让网络学「当前形态之后倾向于怎么走」。外汇与贵金属杠杆高、滑点跳空频繁,任何历史拟合出的倾向都只是概率,不是保证。 想验证的人可以直接开 MT5 新建一个 EA 骨架,把 TEMPO 的特征构造逻辑写进 OnTick,用策略测试器跑 EURUSD 的 M15 看看信号分布,再决定是否深挖。

◍ 用冻结的GPT-2给行情序列做嵌入

TEMPO 的思路是把已经预训练好的语言模型(作者实验里用的是 GPT-2)直接拿来跑时间序列,而不是从零训一个模型。GPT-2 在多种语言的大规模语料上见过词与词之间的长期依赖,这种能力正好能借来预测价格序列里的先后关系。 但语言模型和裸行情数据之间隔着一层:模型不吃纯数字,它要先经过嵌入把文本变成隐藏状态,再算概率、出下一个符号。TEMPO 的做法是在训练时把 GPT-2 的参数冻结,只去优化「把行情转成它能懂的嵌入」这一截。 具体落地时,先把时间序列拆成趋势、季节性和其余分量,各分段后映射为模型可用的嵌入;作者还加了软性提示来引导模型偏向趋势或季节性分析。这样模型可解释性比黑箱 LSTM 强,你能看清哪个分量在拽预测值。外汇和贵金属波动受消息面干扰大,这套方法也只是概率层面辅助,实盘前务必在 MT5 用历史数据回测验证。

「把 TEMPO 封装进单层模块的真实代价」

这套预测架构表面只有 3 个神经层,但 CNeuronTEMPOOCL 内部藏了 24 个嵌套层,其中 12 层带可训练参数,还塞了 2 个注意力单元——每个都是四层自注意力、4 个头。作者把它包成单层模块,用户只填 Init 参数就能搭出复杂模型,代价是调结构自由度被类里参数卡死,想换复杂性得改代码。 原生数据进来后故意跳过批量归一化。TEMPO 把序列拆成趋势、季节、残差三条线:趋势用分段线性提,季节用离散傅里叶按振幅抓周期,残差就是前两者减完的剩量。单变量序列数值本来就可比,提前归一化反而白吃算力。 但分解后必须在分量级做归一化。趋势装了信息主体,季节振幅明显低,残差多是噪声(新闻冲击之类)。若只在分解前归一化原生数据,分量之间仍不可比,模型稳定性会掉。实验里频域和交叉注意力模块都设了 4 头、4 嵌套层。 输出处没用简单相加,而是无激活卷积做加权总和,参数上限锁在小于 1,避免爆值。可这和环境里 RSI(0–100)这类指标尺度对不上,MSE 一跑预测可能顶满。补的 CNeuronFreDFOCL 频率对齐层对缩放不敏感,学的是走势结构而非绝对值。外汇/贵金属杠杆高,这套结构若直接照搬实盘,训练成本与过拟合风险都偏大,建议先 MT5 用附件架构跑历史回测看误差曲线。

MQL5 / C++
class="type">bool CreateEncoderDescriptions(CArrayObj *&encoder)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
       encoder = new CArrayObj();
       if(!encoder)
         class="kw">return false;
     }
class=class="str">"cmt">//--- Encoder
   encoder.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;          class=class="str">"cmt">// 基础全连接输入层类型
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr); class=class="str">"cmt">// 层节点数=历史柱数*单柱描述维度,须与输入张量同尺寸
   descr.activation = None;                class=class="str">"cmt">// 输入层不激活
   descr.optimization = ADAM;              class=class="str">"cmt">// 用 ADAM 优化
   if(!encoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTEMPOOCL;         class=class="str">"cmt">// TEMPO 封装模块
   descr.count = HistoryBars;              class=class="str">"cmt">// 输出预测步数=历史柱数
   descr.window = BarDescr;                class=class="str">"cmt">// 单时间序列特征宽
   descr.step = NForecast;                 class=class="str">"cmt">// 预测前向步长
   descr.window_out = class="num">4;                   class=class="str">"cmt">// 输出通道4(趋势/季节/残差+聚合)
   descr.layers = class="num">4;                       class=class="str">"cmt">// 注意力嵌套4层
   descr.batch = class="num">1e4;                      class=class="str">"cmt">// 批大小1万
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
       class="kw">delete descr;

编码器与 actor 的网络层描述构造

下面这段是构建预测编码器第二层与强化学习 actor 网络的核心代码,直接决定 MT5 里 OpenCL 神经层的结构与内存分配。 编码器第二层把 descr.type 设为 defNeuronFreDFOCL,window 绑定 BarDescr,count 取 NForecast,step 写成 int(true) 即 1,probability 给到 0.7f,优化器用 ADAM,激活函数为 None。若 encoder.Add(descr) 失败则 delete 并回 false,成功才继续。 actor 网络先清表,输入层用 defNeuronBaseOCL,节点数 = AccountDescr,无激活;第一隐藏层同样类型,count = EmbeddingSize,激活改 SIGMOID。第二层换成 defNeuronMLCrossAttentionMLKV,通过 ArrayCopy 把 units 设成 {1, NForecast}、windows 设成 {EmbeddingSize, BarDescr}、heads 设成 {4, 2},layers=4、step=1、window_out=32。 最后 cSum 以 Init(0,24,OpenCL,3,3,1,iVariables,iForecast,optimization,iBatch) 起手,激活置 None,并宏定义 MAX_WEIGHT 为 1.0e-3f 限制权重上限。外汇与贵金属行情下用这套结构做推理属高风险,层参数若与显存不匹配会直接初始化失败,建议先在策略测试器开 OpenCL 日志核验。

MQL5 / C++
   class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = BarDescr;
   descr.count =  NForecast;
   descr.step = class="type">int(true);
   descr.probability = class="num">0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CreateDescriptions(CArrayObj *&actor, CArrayObj *&critic)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         class="kw">return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         class="kw">return false;
     }
class=class="str">"cmt">//--- Actor
   actor.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLCrossAttentionMLKV;
     {
      class="type">int temp[] = {class="num">1, NForecast};
      ArrayCopy(descr.units, temp);
     }
     {
      class="type">int temp[] = {EmbeddingSize, BarDescr};
      ArrayCopy(descr.windows, temp);
     }
     {
      class="type">int temp[] = {class="num">4, class="num">2};
      ArrayCopy(descr.heads, temp);
     }
   descr.layers = class="num">4;
   descr.step = class="num">1;
   descr.window_out = class="num">32;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
   if(!cSum.Init(class="num">0, class="num">24, OpenCL, class="num">3, class="num">3, class="num">1, iVariables, iForecast, optimization, iBatch))
      class="kw">return false;
   cSum.SetActivationFunction(None);
class="macro">#define MAX_WEIGHT class="num">1.0e-3f
class=class="str">"cmt">//--- layer class="num">3

◍ Actor-Critic 网络的末端层装配

上面这段把智能体的 Actor 与 Critic 两条网络在 Latent 层之后的结构一次性铺开。Actor 从第 4 层起连续两个 LatentCount 规模的 sigmoid 隐层,第 5 层直接拉到 2*NActions 且不用激活函数,第 6 层用 VAE 层压缩到 NActions,第 7 层是概率 0.7 的自由度滤波层(defNeuronFreDFOCL),输出维度锁死为 1。 Critic 这边先 Clear 清空旧结构,输入层节点数等于 NActions、无激活;第 6 层接 NRewards 个线性节点;第 7 层同样挂一个 window=NRewards、probability=0.7f 的 FreDF 层。两层 FreDF 的 step 都写成 int(false) 即 0,意味着不做跨步滑动。 逐行拆一下 Actor 第 7 层那段:new CLayerDescription() 失败就直接 return false;descr.window 设成 NActions 控制滤波窗口;descr.probability=0.7f 表示约七成概率保留该自由度分量;若 actor.Add(descr) 返回否,先 delete descr 再退出,避免内存泄漏。 在 MT5 里把 LatentCount、NActions、NRewards 三个宏改成你品种的实际维度(比如 XAUUSD 用 4 动作 3 奖励),编译后观察专家函数是否因 Add 失败而中断——外汇与贵金属波动剧烈,这类网络结构只是概率建模,实盘仍属高风险。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = NActions;
   descr.count =  class="num">1;
   descr.step = class="type">int(false);
   descr.probability = class="num">0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- Critic
   critic.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = NRewards;
   descr.count =  class="num">1;
   descr.step = class="type">int(false);
   descr.probability = class="num">0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {

常见问题

可以先用现成封装好的单层模块,但冻结大模型做嵌入这一步最好找现成接口,别从零写。
正常,嵌入阶段吃显存和算力,实盘前先在历史数据上测耗时,避免卡顿漏单。
小布可以读取你的回测曲线和参数分布,标出疑似过拟合区间,并提示换时段验证。
先核对编码器输出维度是否和actor输入对齐,单层模块里维度不匹配是最常见报错源。
不一定,贵金属波动受消息影响大,神经网络可能倾向捕捉噪声,建议先小资金验证。