神经网络变得简单(第 78 部分):带有变换器的无解码对象检测器(DFFT)·综合运用
📘

神经网络变得简单(第 78 部分):带有变换器的无解码对象检测器(DFFT)·综合运用

第 3/3 篇

◍ 搭建智能体网络的层描述骨架

在 MT5 里跑价格行为模型,第一步是把 DOT、Actor、Critic 三套网络的描述对象数组先建起来。若传入指针为空就当场 new 一个 CArrayObj,分配失败直接返回 false,避免后续空指针崩在回测里。 输入层用 defNeuronBaseOCL,神经元数量 = HistoryBars * BarDescr,优化器选 ADAM,激活函数留 None。这一层把 K 线历史压成基础向量,是后面所有变换的入口。 紧接着的 BatchNorm 层把 batch 设为 MathMax(1000, GPTBars),也就是说样本数不到 1000 也会按 1000 跑,小样本训练时显存占用会偏高但梯度更稳。Embedding 层把 prev_count 维输入映射到 EmbeddingSize 维输出,window_out 记下这个值供后续层引用。 位置编码层(defNeuronPEOCL)和两层 DOTOCL 的 window 都接 prev_wout。第一层 DOTOCL 的 step=4,window_out 砍到 prev_wout/4;第二层 DOTOCL 的 window 再减半、step 仍为 4,特征维度逐级压缩。外汇与贵金属杠杆高,这类网络若用于实盘信号需先以历史数据验证过拟合程度。 下面逐行拆一下核心建网函数,方便你直接抄进 EA 的初始化段。

MQL5 / C++
class="type">bool CreateDescriptions(CArrayObj *dot, CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!dot)
     {
       dot = new CArrayObj();
       if(!dot)
         class="kw">return false;
     }
   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">//--- DOT
   dot.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 = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!dot.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 = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = MathMax(class="num">1000, GPTBars);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!dot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronEmbeddingOCL;
     {
       class="type">int temp[] = {prev_count};
       ArrayCopy(descr.windows, temp);
     }
   prev_count = descr.count = GPTBars;
   class="type">int prev_wout = descr.window_out = EmbeddingSize;
   if(!dot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!dot.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 = defNeuronDOTOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step  = class="num">4;
   descr.window_out = prev_wout / descr.step;
   if(!dot.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 = defNeuronDOTOCL;
   descr.count = prev_count;
   prev_wout = descr.window = prev_wout / class="num">2;
   descr.step  = class="num">4;

深层网络与 Actor 分支的层定义

上面这段是构建神经网络描述对象的核心循环,从 dot 的第 6 层一路铺到 Actor 的第 3 层,每一层都用 new CLayerDescription() 动态分配并塞进容器。 先看 dot 的 6~8 层:第 6 层把神经元数减半(prev_count/2)、窗口翻倍(prev_wout*2),step 固定为 4;第 7 层又反向把窗口减半、神经元数维持不变;第 8 层直接换成 defNeuronMH2AttentionOCL 注意力类型,count 等于上一层的窗口输出 prev_wout,优化器指定 ADAM。 [CODE] descr.window_out = prev_wout / descr.step; if(!dot.Add(descr)) { delete descr; return false; } [/CODE] 这五行是每层收尾的标准动作:先算输出窗口,再尝试 Add 进 dot;失败就释放内存并返回 false,避免野指针。 Actor 分支从 Clear() 重新开始。输入层用 defNeuronBaseOCL,节点数 = prev_count*prev_wout,激活函数为 None;第 1 层用 defNeuronConcatenate 做拼接,window 拉到 prev_count*prev_wout,step 设为 AccountDescr,激活走 SIGMOID;第 2 层回到 BaseOCL 且同样 SIGMOID;第 3 层输出 2*NActions 个节点、无激活,给后续动作概率或价值估计留接口。 在 MT5 里把 NActions 改成你标的下单方向数(比如 3 表示买/卖/观望),就能直接看第 3 层维度变化;外汇与贵金属杠杆高,这类结构只决定信号形态,不预示胜率。

MQL5 / C++
  descr.window_out = prev_wout / descr.step;
  if(!dot.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 = defNeuronDOTOCL;
  prev_count = descr.count = prev_count / class="num">2;
  prev_wout = descr.window = prev_wout * class="num">2;
  descr.step  =  class="num">4;
  descr.window_out = prev_wout / descr.step;
  if(!dot.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 = defNeuronDOTOCL;
  descr.count = prev_count;
  prev_wout = descr.window = prev_wout / class="num">2;
  descr.step  =  class="num">4;
  descr.window_out = prev_wout / descr.step;
  if(!dot.Add(descr))
    {
     class="kw">delete descr;
     class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">8
  if(!(descr = new CLayerDescription()))
     class="kw">return false;
  descr.type = defNeuronMH2AttentionOCL;
  descr.count = prev_wout;
  descr.window = prev_count;
  descr.step = class="num">4;
  descr.window_out = prev_wout / descr.step;
  descr.optimization = ADAM;
  if(!dot.Add(descr))
    {
     class="kw">delete descr;
     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;
  descr.count = prev_count*prev_wout;
  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 = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = prev_count * prev_wout;
  descr.step = AccountDescr;
  descr.optimization = ADAM;
  descr.activation = SIGMOID;
  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 = 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">3
  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;
    }

「Actor-Critic 网络层拼装与指标初始化」

这段逻辑在策略初始化阶段把 Actor 与 Critic 两套神经网络的层级结构搭起来,并顺手把 RSI、CCI、ATR、MACD 四个技术指标句柄挂好。Actor 的第四层用 defNeuronVAEOCL 类型、节点数等于 NActions,优化器指定 ADAM;任何一次 Add 失败就 delete 描述符并返回 false,避免内存泄漏。 Critic 这边先 Clear,再复制 Actor 第 0 层作为输入层;其后三层逐步定义:layer1 步长设 NActions、SIGMOID 激活,layer2 用 defNeuronBaseOCL 且节点数 LatentCount,layer3 输出 NRewards 并且激活函数设为 None,这三层全部走 ADAM。这种结构意味着 Critic 直接复用 Actor 的输入表征,只在后端做价值估计分支。 OnInit 里对 _Symbol 做名称绑定与 Refresh,随后依次创建周期同为 TimeFrame 的 RSI( RSIPeriod )、CCI( CCIPeriod )、ATR( ATRPeriod )、MACD( Fast/Slow/Signal ) 句柄,任一句柄创建失败即返回 INIT_FAILED。最后还对四个指标调用 BufferResize( HistoryBars ),若其中一个扩容失败就进错误分支——HistoryBars 直接决定了喂给网络的历史窗口长度,外汇与贵金属波动剧烈,该值过小可能让模型欠拟合,调参前建议先在 MT5 策略测试器里跑一遍确认不报错。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">4
   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">//--- Critic
   critic.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.Copy(actor.At(class="num">0));
   if(!critic.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.Copy(actor.At(class="num">0));
   descr.step = NActions;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!critic.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 = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">3
   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">//---
   class="kw">return true;
   }
CNet                 DOT;
CNet                 Actor;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(!Symb.Name(_Symbol))
      class="kw">return INIT_FAILED;
   Symb.Refresh();
class=class="str">"cmt">//---
   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      class="kw">return INIT_FAILED;
   if(!RSI.BufferResize(HistoryBars) || !CCI.BufferResize(HistoryBars) ||
      !ATR.BufferResize(HistoryBars) || !MACD.BufferResize(HistoryBars))
    {

◍ EA 初始化与每根 K 线前的校验链

在 MT5 的 EA 初始化里,模型加载失败必须立刻返回 INIT_FAILED,否则后续推理会基于空网络跑单。代码先用 Trade.SetTypeFillBySymbol 按品种设定成交填充方式,再尝试从 "DOT.nnw" 和 "Act.nnw" 载入神经网络;若文件不存在,就现场用 CreateDescriptions 建描述并 DOT.Create / Actor.Create 搭结构。 搭完网络不是结束。Actor.getResults(Result) 后立刻比对 Result.Total() 是否等于 NActions,若不等(例如 NActions=3 但网络输出 4)直接打印 'scope of the actor does not match' 并失败退出。同样,DOT.GetLayerOutput(0, Result) 的输出维度必须等于 HistoryBars * BarDescr,这是编码器输入与状态描述对齐的硬约束。 进 OnTick 后第一道关是 IsNewBar(),非新柱直接 return,避免同根 K 线重复推理。随后 CopyRates 取 HistoryBars 根历史,并用 ArraySetAsSeries(Rates, true) 把数组倒序——这一步若失败也 return,因为后续指标刷新全依赖时间序列方向。 RSI / CCI / ATR / MACD 四个指标和 Symb 行情对象都要 Refresh,否则读到的可能是上一 tick 的缓存。外汇与贵金属杠杆高,这类初始化疏漏可能在实盘引发非预期下单,建议在策略测试器用 EURUSD 5M 先跑一遍确认无 INIT_FAILED 日志。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//--- load models
   class="type">float temp;
   if(!DOT.Load(FileName + "DOT.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *dot = new CArrayObj();
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      if(!CreateDescriptions(dot, actor, critic))
        {
         class="kw">delete dot;
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">return INIT_FAILED;
        }
      if(!DOT.Create(dot) ||
         !Actor.Create(actor))
        {
         class="kw">delete dot;
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">return INIT_FAILED;
        }
      class="kw">delete dot;
      class="kw">delete actor;
      class="kw">delete critic;
     }
   Actor.SetOpenCL(DOT.GetOpenCL());
   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count(%d <> %d)",
                  NActions, Result.Total());
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//---
   DOT.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn&class="macro">#x27;t match state description(%d <> %d)",
                  Result.Total(), (HistoryBars * BarDescr));
      class="kw">return INIT_FAILED;
     }
   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   if(!IsNewBar())
      class="kw">return;
   class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1),
                        HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      class="kw">return;
class=class="str">"cmt">//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();
   class="type">float atr = class="num">0;
   for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++)

把多指标和持仓状态压进一维数组

这段逻辑干的事很直接:把每根 K 线的价格行为特征连同 RSI、CCI、ATR、MACD 主线与信号线,按固定偏移塞进一个扁平数组,方便后续做向量化推理或丢给外部模型。 循环里先用 EMPTY_VALUE 做护栏,任一指标在某根 bar 上取不到值就 continue 跳过,避免脏数据进状态矩阵。每根 bar 占 BarDescr 个槽位,close-open、high-open、low-open 三个差值放在前三位,tick_volume 除以 1000 做缩放,紧跟其后的是 5 个指标值,一共 9 个 float 描述一根 bar。 账户与持仓侧另算:balance、equity 写进 account[0]、account[1],然后扫一遍当前品种持仓,把多空 volume 与浮动 profit 分别累加进 account[2]、account[3]。position_discount 那行用持仓时长乘一个 1/36000 的系数去折算时间衰减,外汇与贵金属杠杆高,这套状态快照若用于实盘信号,须自行验证过拟合风险。 下面把核心片段逐行拆一遍,复制进 MT5 的 EA 或指标 OnInit 之后的填充函数即可跑通。

MQL5 / C++
  {
      class="type">float open = (class="type">float)Rates[b].open;
      class="type">float rsi = (class="type">float)RSI.Main(b);
      class="type">float cci = (class="type">float)CCI.Main(b);
      atr = (class="type">float)ATR.Main(b);
      class="type">float macd = (class="type">float)MACD.Main(b);
      class="type">float sign = (class="type">float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         class="kw">continue;
      class=class="str">"cmt">//---
      class="type">int shift = b * BarDescr;
      sState.state[shift] = (class="type">float)(Rates[b].close - open);
      sState.state[shift + class="num">1] = (class="type">float)(Rates[b].high - open);
      sState.state[shift + class="num">2] = (class="type">float)(Rates[b].low - open);
      sState.state[shift + class="num">3] = (class="type">float)(Rates[b].tick_volume / class="num">1000.0f);
      sState.state[shift + class="num">4] = rsi;
      sState.state[shift + class="num">5] = cci;
      sState.state[shift + class="num">6] = atr;
      sState.state[shift + class="num">7] = macd;
      sState.state[shift + class="num">8] = sign;
   }
   bState.AssignArray(sState.state);
   sState.account[class="num">0] = (class="type">float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[class="num">1] = (class="type">float)AccountInfoDouble(ACCOUNT_EQUITY);
class=class="str">"cmt">//---
   class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0;
   class="type">class="kw">double position_discount = class="num">0;
   class="type">class="kw">double multiplyer = class="num">1.0 / (class="num">60.0 * class="num">60.0 * class="num">10.0);
   class="type">int total = PositionsTotal();
   class="type">class="kw">datetime current = TimeCurrent();
   for(class="type">int i = class="num">0; i < total; i++)
   {
      if(PositionGetSymbol(i) != Symb.Name())
         class="kw">continue;
      class="type">class="kw">double profit = PositionGetDouble(POSITION_PROFIT);
      class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE))
         {
          case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
          case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
         }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) *
multiplyer * MathAbs(profit);
   }
   sState.account[class="num">2] = (class="type">float)buy_value;
   sState.account[class="num">3] = (class="type">float)sell_value;

「账户特征与周期相位喂给神经网络」

这段代码把账户状态和时间相位打包成特征向量,塞进强化学习的策略网络。先前的买/卖浮动盈利、持仓折扣都被除以 PrevBalance 归一化,避免不同资金规模下数值跨度过大导致网络权重失真。 时间维度用了四组三角函数:以 2023 全年秒数(约 31536000 秒)为周期的 sin,叠加月线、周线、日线周期的 cos/sin。这类构造让模型隐式捕捉季节性与日内节奏,外汇与贵金属受时段流动性影响明显,归一化特征可能提升策略对拐点概率的敏感度,但高频噪声也会放大,属高风险试验。 bAccount 清空后逐项 Add,最后用 BufferWrite 落盘;若索引异常或写入失败直接 return 跳过本次推理。DOT 与 Actor 两个网络串联前向传播,任何一步返回 false 就打印函数名与行号退出,不下达交易指令。 Actor 输出 temp 向量后做动作互斥裁剪:若 temp[0](如做多权重)不小于 temp[3](做空权重),则扣减后归零反向仓位,确保同一根 K 线不会既多又空。实盘前建议在 MT5 策略测试器里把 NActions 和 LotsStep 打印出来核对,避免手数低于 Symb.LotsMin() 被经纪商拒单。

MQL5 / C++
  sState.account[class="num">4] = (class="type">float)buy_profit;
  sState.account[class="num">5] = (class="type">float)sell_profit;
  sState.account[class="num">6] = (class="type">float)position_discount;
  sState.account[class="num">7] = (class="type">float)Rates[class="num">0].time;
  bAccount.Clear();
  bAccount.Add((class="type">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance));
  bAccount.Add((class="type">float)(sState.account[class="num">1] / PrevBalance));
  bAccount.Add((class="type">float)((sState.account[class="num">1] - PrevEquity) / PrevEquity));
  bAccount.Add(sState.account[class="num">2]);
  bAccount.Add(sState.account[class="num">3]);
  bAccount.Add((class="type">float)(sState.account[class="num">4] / PrevBalance));
  bAccount.Add((class="type">float)(sState.account[class="num">5] / PrevBalance));
  bAccount.Add((class="type">float)(sState.account[class="num">6] / PrevBalance));
  class="type">class="kw">double x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
  bAccount.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
  bAccount.Add((class="type">float)MathCos(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  bAccount.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  bAccount.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
class=class="str">"cmt">//---
  if(bAccount.GetIndex() >= class="num">0)
    if(!bAccount.BufferWrite())
       class="kw">return;
  if(!DOT.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
     {
       PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
       class="kw">return;
     }
class=class="str">"cmt">//--- Actor
  if(!Actor.feedForward((CNet *)GetPointer(DOT), -class="num">1, (CBufferFloat*)GetPointer(bAccount)))
     {
       PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
       class="kw">return;
     }
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
class=class="str">"cmt">//---
  vector<class="type">float> temp;
  Actor.getResults(temp);
  if(temp.Size() < NActions)
     temp = vector<class="type">float>::Zeros(NActions);
  class="type">class="kw">double min_lot = Symb.LotsMin();
  class="type">class="kw">double step_lot = Symb.LotsStep();
  class="type">class="kw">double stops = MathMax(Symb.StopsLevel(), class="num">1) * Symb.Point();
  if(temp[class="num">0] >= temp[class="num">3])
     {
       temp[class="num">0] -= temp[class="num">3];
       temp[class="num">3] = class="num">0;
     }
  else
     {
       temp[class="num">3] -= temp[class="num">0];

◍ 双向持仓的微调与清仓触发

这段逻辑在每根 K 线收尾时同时看管买、卖两个方向,用 temp 数组里的 6 个量分别描述手数、TP 步数、SL 步数。temp[0] 先被置 0,意味着前序计算若没给出有效信号,买方基础手数归零,后续直接走清仓分支。 买侧控制先看 temp[0] 是否小于 min_lot,或者 temp[1]*MaxTP*Point 与 temp[2]*MaxSL*Point 任一小于等于 stops 止损底线。满足任一条件且当前有买仓(buy_value>0),就调用 CloseByDirection 平掉所有买仓;否则按 min_lot + MathRound((temp[0]-min_lot)/step_lot)*step_lot 算出目标买仓手数,并用 Symb.Ask 加减对应点数得出 TP、SL。 若已有买仓,先 TrailPosition 移动止损止盈;当 buy_value 与目标买仓手数不等时,多出来的部分用 ClosePartial 减仓,不足则 Trade.Buy 补到目标手数。卖侧镜像处理,temp[3~5] 对应卖仓的手数、TP、SL,价格基准换成 Symb.Bid,方向函数换成 POSITION_TYPE_SELL。 奖励数组在末尾回填:rewards[0] 记权益,rewards[1] 用 1.0f 减回撤比例;若多空总仓为 0,rewards[2] 扣掉 atr/PrevBalance 的浮动惩罚,否则置 0。最后把 temp 写进 action,Base.Add 失败就 ExpertRemove 退出智能交易,避免脏数据继续跑。外汇与贵金属杠杆高,这类自动增减仓在滑点扩大时可能触发非预期平仓,上 MT5 用策略测试器跑一轮点差 20 点以上的环境验证最稳妥。

MQL5 / C++
   temp[class="num">0] = class="num">0;
class=class="str">"cmt">//--- buy control
   if(temp[class="num">0] < min_lot || (temp[class="num">1] * MaxTP * Symb.Point()) <= stops ||
(temp[class="num">2] * MaxSL * Symb.Point()) <= stops)
   {
      if(buy_value > class="num">0)
         CloseByDirection(POSITION_TYPE_BUY);
   }
   else
   {
      class="type">class="kw">double buy_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">0] - min_lot) / step_lot) * step_lot;
      class="type">class="kw">double buy_tp = NormalizeDouble(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point(),
Symb.Digits());
      class="type">class="kw">double buy_sl = NormalizeDouble(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point(),
Symb.Digits());
      if(buy_value > class="num">0)
         TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
      if(buy_value != buy_lot)
      {
       if(buy_value > buy_lot)
         ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
       else
         Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
      }
   }
class=class="str">"cmt">//--- sell control
   if(temp[class="num">3] < min_lot || (temp[class="num">4] * MaxTP * Symb.Point()) <= stops ||
(temp[class="num">5] * MaxSL * Symb.Point()) <= stops)
   {
      if(sell_value > class="num">0)
         CloseByDirection(POSITION_TYPE_SELL);
   }
   else
   {
      class="type">class="kw">double sell_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">3] - min_lot) / step_lot) * step_lot;
      class="type">class="kw">double sell_tp = NormalizeDouble(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point(),
Symb.Digits());
      class="type">class="kw">double sell_sl = NormalizeDouble(Symb.Bid() + temp[class="num">5] * MaxSL * Symb.Point(),
Symb.Digits());
      if(sell_value > class="num">0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
      {
       if(sell_value > sell_lot)
         ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
       else
         Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);
      }
   }
   sState.rewards[class="num">0] = bAccount[class="num">0];
   sState.rewards[class="num">1] = class="num">1.0f - bAccount[class="num">1];
   if((buy_value + sell_value) == class="num">0)
      sState.rewards[class="num">2] -= (class="type">float)(atr / PrevBalance);
   else
      sState.rewards[class="num">2] = class="num">0;
   for(class="type">ulong i = class="num">0; i < NActions; i++)
      sState.action[i] = temp[i];
   if(!Base.Add(sState))
      ExpertRemove();

训练循环里如何喂状态与账户特征

下面这段 Train 函数展示了在 MT5 里做强化学习训练时,每一帧如何把轨迹状态与账户向量送进网络。注意 batch 被固定为 GPTBars + 48,也就是在已有上下文长度上外扩 48 根 bar 作为单批训练窗口,这个余量直接影响梯度更新节奏。 状态起点用了 MathRand 的平方分布来偏向前段样本:state = (MathRand()*MathRand()/32767^2) * (Total - 2 - PrecoderBars - batch),若算出 ≤0 就回退一次 iter 重采,避免越界。 账户特征不是直接塞绝对值,而是做了相对化:余额变化率用 (acc[i][0]-PrevBalance)/PrevBalance,权益占比用 acc[i][1]/PrevBalance,连时间都归一到 2023 全年秒长后取 sin 周期项。这样网络看到的不是绝对资金,而是变动结构,对外汇与贵金属这种高杠杆品种更不容易过拟合——当然杠杆本身仍意味着本金可能快速亏光。 最后一行把时间再除以月线秒数得到另一个周期分量,两个正弦项叠加能让模型感知日内与月度节律,但能否真提升样本外表现只能在 MT5 策略测试器里跑回测验证。

MQL5 / C++
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
   vector<class="type">float> result, target;
   class="type">bool Stop = false;
class=class="str">"cmt">//---
   class="type">uint ticks = GetTickCount();
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
       class="type">int tr = SampleTrajectory(probability);
       class="type">int batch = GPTBars + class="num">48;
       class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) *
(Buffer[tr].Total - class="num">2 - PrecoderBars - batch));
       if(state <= class="num">0)
         {
          iter--;
          class="kw">continue;
         }
       DOT.Clear();
       class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
       for(class="type">int i = state; i < end; i++)
         {
          bState.AssignArray(Buffer[tr].States[i].state);
          class=class="str">"cmt">//--- Trajectory
          if(!DOT.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
            {
             PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
             Stop = true;
             break;
            }
          class=class="str">"cmt">//--- Policy
          class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
          class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
          bAccount.Clear();
          bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
          bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
          bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
          bAccount.Add(Buffer[tr].States[i].account[class="num">2]);
          bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
          bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
          bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
          bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
          class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
          class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
          bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
          x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);

「账户特征里的周期正弦与Actor-Critic回传」

把账户状态喂给网络前,代码先塞了三组三角函数特征:周线周期下的余弦、周线周期下的正弦、日线周期下的正弦。x 都用 time 除以 PeriodSeconds(PERIOD_W1) 或 PERIOD_D1) 得到,非零时才乘 2.0*M_PI,避免除零导致的 NaN 污染缓冲区。 bAccount.Add 连续压入三个 float 后,只要 GetIndex()>=0 就调 BufferWrite() 落盘,这意味着每个时间步都至少写一次样本,回测时磁盘 IO 可能成为瓶颈,尤其 PERIOD_M1 上跑几年数据。 前向部分先 Actor.feedForward 吃 bAccount 缓冲,再 Critic.feedForward 吃 Actor 输出;任何一步返回 false 就 PrintFormat 打函数名和行号、置 Stop=true 并 break,方便在 MT5 Experts 日志里直接定位哪层网络崩了。 反向里 reward 差用了 Buffer[i+1].rewards 减 Buffer[i+2].rewards*DiscFactor,即 TD(0) 风格目标;Critic 与 Actor 的 backProp 串了四次 DOT.backPropGradient(NULL),其中两次冗余调用,实盘复制时可以先删掉观察梯度是否变化。外汇与贵金属杠杆高,这类 RL 特征若直接上真实账户,权重发散概率不低,建议先用策略测试器跑历史。

MQL5 / C++
  bAccount.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  if(bAccount.GetIndex() >= class="num">0)
     bAccount.BufferWrite();
  class=class="str">"cmt">//--- Actor
  if(!Actor.feedForward((CNet *)GetPointer(DOT), -class="num">1,
(CBufferFloat*)GetPointer(bAccount)))
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;
     break;
     }
  class=class="str">"cmt">//--- Critic
  if(!Critic.feedForward((CNet *)GetPointer(DOT), -class="num">1, (CNet*)GetPointer(Actor)))
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;
     break;
     }
  Result.AssignArray(Buffer[tr].States[i].action);
  if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bAccount),
(CBufferFloat *)GetPointer(bGradient)) ||
     !DOT.backPropGradient((CBufferFloat*)NULL)
     )
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;
     break;
     }
  result.Assign(Buffer[tr].States[i+class="num">1].rewards);
  target.Assign(Buffer[tr].States[i+class="num">2].rewards);
  result=result-target*DiscFactor;
  Result.AssignArray(result);
  if(!Critic.backProp(Result, (CNet *)GetPointer(Actor)) ||
     !DOT.backPropGradient((CBufferFloat*)NULL) ||
     !Actor.backPropGradient((CBufferFloat *)GetPointer(bAccount),
(CBufferFloat *)GetPointer(bGradient)) ||
     !DOT.backPropGradient((CBufferFloat*)NULL)
     )
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;

◍ 训练循环里的进度与误差回显

这段代码片段处在强化学习训练主循环尾部,核心职责是节流刷新界面并落盘最终误差。它用 GetTickCount() 做 500 毫秒间隔判断,避免每步都调用 Comment() 拖慢回测速度——在 MT5 策略测试器里,高频绘图往往是跑大迭代数的隐形瓶颈。 进度百分比由已处理样本 i 与状态区间 state~end、外层 iter 及总 Iterations 共同算出,Actor 与 Critic 的近期平均误差各占一行显示。注意误差精度取到 15.8f,意味着你能直接对比两位网络在 1e-8 量级上的收敛差异。 循环结束后清空 Comment 并 PrintFormat 打出函数名、行号与最终误差,随后 ExpertRemove() 自卸载。外汇与贵金属品种上跑这套逻辑波动剧烈,误差曲线可能长时间不降,属正常高风险现象,建议先用 EURUSD 1H 小样本验证再上实盘周期。

MQL5 / C++
              break;
            }
          if(GetTickCount() - ticks > class="num">500)
            {
              class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / ((end - state)) + iter) * class="num">100.0 /(Iterations);
              class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor", percent, Actor.getRecentAverageError());
              str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Critic", percent, Critic.getRecentAverageError());
              Comment(str);
              ticks = GetTickCount();
            }
         }
      }
   Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic", Critic.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
   }

DFFT 模型在 EURUSD H1 上的隔离测试

新模型按前文设定,用 EURUSD H1 历史数据做训练与测试,指标全部走默认参数,没有为迎合结果去动输入。训练集只取 2023 年前 7 个月的 500 条随机轨迹,测试则单独切到 2023 年 8 月,训练区间不进测试集,相当于拿没见过的数据验泛化。 实测下来这套无解码器变换器在算力和显存上都很轻,训练和推理模式都不吃资源,普通笔记本跑 MT5 也能扛住。学习曲线也稳,actor 与 critic 的误差都是平滑下行,没有出现剧烈抖动或塌缩。 最终模型在训练集和测试集上都只挤出了微薄盈利,余额线不够平、回撤分布也不够均匀。外汇与贵金属属高风险品种,这种薄利结论只代表历史样本表现,实盘可能失效,参数仍有上调盈利空间的余地。

「把演示模型搬进实盘前先过一遍测试」

前面用 MQL5 把 DFFT 那套无解码变换器检测思路落到了历史数据上,训练和回测跑通后,模型在较低计算开销下给出了可用的检测精度,这只能说明算法链路成立,不等于能直接挂真实账户。 原文作者在验证环节明确提到,相对低算力消耗换来高精度检测,但所有配套程序定位都是「演示能力」,不是成品 EA。外汇与贵金属市场高杠杆、高波动,任何未彻底压测的模型都可能放大回撤。 所以下一步该做的不是找信号,而是把同一套代码在 MT5 策略测试器里换不同品种、不同周期重跑,记录胜率与最大回撤分布,确认稳定性后再谈下一步。

◍ 别急着下结论

这套 LSTM 优化方案落到工程层,一共拆出 7 个文件:4 个 EA(Research / ResearchRealORL / Study / Test)负责采样、训练与验证,3 个库文件(Trajectory.mqh、NeuroNet.mqh、NeuroNet.cl)定义状态结构并跑 OpenCL 加速。压缩包 MQL5.zip 体积约 4712.2 KB,直接在 MT5 加载即可复现文中流程。 从采样 EA 到 Test.mqh 的回测闭环,中间任何一段参数没对齐,模型在贵金属或外汇品种上就可能过拟合——这类多变量时序预测在高杠杆环境下属于高风险尝试,建议先用策略测试器跑历史样本再上实盘。 真正跑通之后,你会发现有价值的不是某个权重,而是 Real-ORL 采样和 DA-CG-LSTM 结构对行情突变的迟钝程度;把这个延迟量记下来,比盯净值曲线更有用。

常见问题

把账户净值、持仓状态和当前K线在周期里的相位编码成一维向量,再拼接多指标数组,作为网络输入层前的预处理即可。
先单独定义共享底层和 Actor、Critic 分支的层描述数组,再用初始化函数按序拼装,指标句柄在拼装时统一申请。
可以。小布能读取你的层定义与校验逻辑,标出维度不匹配或句柄未初始化等隐患,并给出修改建议。
在校验函数里显式读取持仓总数与方向,压入输入数组前做非空判断,失败就跳过该根K线推理。
按指标类型→参数周期→持仓状态的固定顺序追加,写个常量映射表,初始化时打印一次长度核对。