神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)·综合运用
🎞️

神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)·综合运用

(3/3)·静态环境状态漏掉了价格变动的动态概率,这篇用 FAQ 聚合查询补上最后一块拼图

偏理论 第 3/3 篇
把历史数据堆进状态向量就当环境描述完整,是很多强化学习交易模型的隐性天花板。价格走的不是定格照片,而是连续帧里的位移与加速度。借用视频目标检测里的时态聚合,才有机会让模型读到走势的动势。

「EA 初始化与逐根 K 线取数骨架」

MT5 智能交易系统在 OnInit 里优先尝试从本地加载 DOT.nnw 与 Act.nnw 两个神经网络权重文件;若文件缺失或加载失败,则当场用 CreateDescriptions 构造 dot / decoder / actor / critic 四套网络描述,并依次 DOT.Create、Decoder.Create、Actor.Create,任一步返回 false 就释放全部 CArrayObj 指针并 return INIT_FAILED。加载成功后把 DOT 内部的 OpenCL 上下文透传给 Decoder 与 Actor,这一步决定后续矩阵运算是否在显卡上跑。 行情侧靠 OnTick 驱动,但先用 IsNewBar 拦掉同根 K 线内的重复触发;随后 CopyRates 按 TimeFrame 拉取 HistoryBars 根历史数据进 Rates 数组,并 ArraySetAsSeries(Rates, true) 把索引 0 对齐到最新一根。RSI、CCI、ATR、MACD 四个指标对象各自 Refresh,再循环把 open、rsi、cci、atr、macd、signal 逐个读入 float 变量——这里 atr 在循环外声明,意味着最后一根本根的 ATR 值会留到循环外继续使用。 实盘接这套逻辑时,先把 HistoryBars 设小(比如 50)观一眼显存占用与 tick 延迟;外汇与贵金属杠杆高、滑点突变频繁,神经网络推理只是概率辅助,仓位仍须自担风险。

MQL5 / C++
class=class="str">"cmt">//| Expert initialization function                                                                 | //+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//--- load models
   class="type">float temp;
class=class="str">"cmt">//---
   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 *decoder = new CArrayObj();
       CArrayObj *actor = new CArrayObj();
       CArrayObj *critic = new CArrayObj();
       if(!CreateDescriptions(dot, decoder, actor, critic))
         {
          class="kw">delete dot;
          class="kw">delete decoder;
          class="kw">delete actor;
          class="kw">delete critic;
          class="kw">return INIT_FAILED;
         }
       if(!DOT.Create(dot) ||
           !Decoder.Create(decoder) ||
           !Actor.Create(actor))
         {
          class="kw">delete dot;
          class="kw">delete decoder;
          class="kw">delete actor;
          class="kw">delete critic;
          class="kw">return INIT_FAILED;
         }
       class="kw">delete dot;
       class="kw">delete decoder;
       class="kw">delete actor;
       class="kw">delete critic;
     }
class=class="str">"cmt">//---
   Decoder.SetOpenCL(DOT.GetOpenCL());
   Actor.SetOpenCL(DOT.GetOpenCL());
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                                                 | //+------------------------------------------------------------------+
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++)
     {
       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);

把指标与持仓压进状态数组

这段逻辑干的事很直接:先过滤掉任一指标取不到值的 K 线(rsi/cci/atr/macd/sign 任一为 EMPTY_VALUE 就跳过),避免脏数据混进训练样本。随后按 BarDescr 步长把每根 bar 的 close-high-low 相对开盘价的偏移、tick_volume/1000、以及 5 个指标值共 9 个浮点塞进 sState.state。

账户侧用 AccountInfoDouble 抓 balance 与 equity 写进 account[0]、[1];持仓遍历只认当前品种,分别累加多空 volume 与 floating profit 到 account[2]~[5]。position_discount 那行有意思:用 (current - 开仓时间) * multiplyer *profit做时间衰减惩罚,multiplyer = 1/(60*60*10),相当于持仓秒数乘 1/360000 再乘利润绝对值,老仓对状态的负向权重更高。

最后 bAccount 只塞了两个归一化值:余额较 PrevBalance 的收益率、权益对 PrevBalance 的比值。外汇与贵金属杠杆高,这类状态拼接若用于 RL 或信号模型,过拟合和滑点吞噬概率都不小,上 MT5 跑前先手算几根 bar 的 shift 偏移确认没写串。

MQL5 / C++
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;
           class="kw">break;
        case POSITION_TYPE_SELL:
           sell_value += PositionGetDouble(POSITION_VOLUME);
           sell_profit += profit;
           class="kw">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;
 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));

◍ 把账户状态塞进网络的喂食逻辑

这段逻辑做的是把实时账户与周期相位编码成一维特征向量,喂给后续的自编码器与策略网络。前 6 个特征值分别是权益环比变动率、账户三项原始字段,以及自由保证金、结余、净值相对前一周期余额的占比,类型全部强转 float 以压低显存开销。 时间项用了四个不同周期的三角函数:以 2023 全年秒数为分母算年相位正弦,再分别用月线、周线、日线周期秒数算余弦或正弦。这样网络能隐式感知‘现在处于年/月/周/日循环的哪个相位’,对外汇和贵金属这种带周期节律的市场可能有帮助,但高频噪声也会被一并吃进去。 特征拼满后先跑 bAccount.BufferWrite() 落盘,失败直接 return;随后 DOT 前馈、Decoder 解码、Actor 出动作,三层任意一层 feedForward 返回 false 就打印函数名加行号并退出,方便在 MT5 Experts 日志里定位断点。 出向量后做动作互斥裁剪:temp[0] 与 temp[3] 分别代表多空强度,谁大就减掉小的,保证同周期不双向开仓。下方还取了 LotsMin、LotsStep 与 StopsLevel*Point 作为下单硬约束,外汇贵金属杠杆高,这类边界值不校验就容易报‘无效交易量’错单。

MQL5 / C++
  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));
  if(bAccount.GetIndex() >= class="num">0)
    if(!bAccount.BufferWrite())
       class="kw">return;
class=class="str">"cmt">//---
  if(!DOT.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
    {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      class="kw">return;
    }
  if(!Decoder.feedForward((CNet*)GetPointer(DOT), LatentLayer,(CNet*)GetPointer(DOT)))
    {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      class="kw">return;
    }
class=class="str">"cmt">//--- Actor
  if(!Actor.feedForward((CNet *)GetPointer(Decoder), -class="num">1, (CBufferFloat*)GetPointer(bAccount)))
    {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      class="kw">return;
    }
class=class="str">"cmt">//---
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
  vector<class="type">float> temp;
  Actor.getResults(temp);
  if(temp.Size() < NActions)
    temp = vector<class="type">float>::Zeros(NActions);
class=class="str">"cmt">//---
  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];
      temp[class="num">0] = class="num">0;
    }
class=class="str">"cmt">//--- buy control

「双向持仓的加减量控与奖励回填」

这段逻辑把买、卖两条线拆开管,但判定骨架完全一致:先用 temp 数组的前三个元素(买向)和后三个元素(卖向)检查手数下限、止盈止损距离是否撞到 stops 硬下限。只要任一条件不满足,就直接把该方向已有仓位清掉,不再参与本轮调仓。 以买向为例,temp[0] 是模型给的目标手数,min_lot 是经纪商最小交易单位,step_lot 是手数步长。代码用 MathRound((temp[0]-min_lot)/step_lot)*step_lot 把目标手数吸附到合规网格上,避免下单被 MT5 拒。买向 TP 挂在 Ask 上方 temp[1]*MaxTP*Point 处,SL 挂在 Ask 下方 temp[2]*MaxSL*Point 处,并用 NormalizeDouble 按品种 Digits 截断报价精度。 若当前 buy_value 大于算出的 buy_lot,说明模型要减仓,调 TrailPosition 改挂止损止盈后由 ClosePartial 平掉多出部分;若小于,则 Trade.Buy 补齐差额。卖向对称处理,TP/Bid 用减、SL/Bid 用加。外汇与贵金属杠杆高,stops 设太小会被这段代码强制清仓,回测时建议先打印 stops 与 Point 乘积确认不会误杀。 清完仓位后,sState.rewards 回填三项:账户权益、1 减回撤比例、以及无持仓时的 atr/PrevBalance 惩罚项。最后把 temp 整组写进 action 并 Base.Add,添加失败直接 ExpertRemove 退出智能交易,防止脏数据继续跑。

MQL5 / C++
  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">class="kw">ulong i = class="num">0; i < NActions; i++)
      sState.action[i] = temp[i];
  if(!Base.Add(sState))
      ExpertRemove();
}

EA 初始化时四套神经网络的装载逻辑

在 MT5 的 EA 初始化阶段,先声明了四个 CNet 实例:DOT、Decoder、Actor、Critic,分别对应编码、解码、策略与价值网络。OnInit 里第一步调用 ResetLastError 清错误码,随后 LoadTotalBase 负责把历史学习数据读入内存,若返回 false 则直接打印错误号并 INIT_FAILED,EA 起不来。 紧接着尝试从磁盘加载四个 .nnw 模型文件(DOT.nnw、Dec.nnw、Act.nnw、Crt.nnw)。只要其中任意一个 Load 失败,代码会现建四组 CArrayObj 描述对象,调用 CreateDescriptions 按 NActions、HistoryBars、BarDescr 等宏生成网络结构,再逐网 Create;任一步失败同样回退 INIT_FAILED,并手动 delete 防内存泄漏。 模型就绪后,OpenCL 上下文从 DOT 取出并共享给其余三网,Actor.getResults(Result) 校验输出维度必须等于 NActions,否则报“scope of the actor does not match”。DOT.GetLayerOutput(0, Result) 再核对输入尺寸 == HistoryBars * BarDescr,不符则提示编码器输入与状态描述不匹配。最后 bGradient 缓冲区按 AccountDescr 与 NForecast 的最大值初始化,外汇与贵金属品种下这套装载若报错,EA 无法跑,属高风险调试环节,建议逐文件核对 .nnw 是否和当前宏定义一致。

MQL5 / C++
CNet                    Decoder;
CNet                    Actor;
CNet                    Critic;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      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) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true)
     )
     {
      CArrayObj *dot = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      if(!CreateDescriptions(dot, decoder, actor, critic))
        {
         class="kw">delete dot;
         class="kw">delete decoder;
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">return INIT_FAILED;
        }
      if(!DOT.Create(dot) ||
         !Decoder.Create(decoder) ||
         !Actor.Create(actor) ||
         !Critic.Create(critic))
        {
         class="kw">delete dot;
         class="kw">delete decoder;
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">return INIT_FAILED;
        }
      class="kw">delete dot;
      class="kw">delete decoder;
      class="kw">delete actor;
      class="kw">delete critic;
     }
   OpenCL = DOT.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);
   Actor.SetOpenCL(OpenCL);
   Critic.SetOpenCL(OpenCL);
   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;
     }
   if(!bGradient.BufferInit(MathMax(AccountDescr, NForecast), class="num">0) ||
      !bGradient.BufferCreate(OpenCL))
     {

◍ 初始化失败与离线模型落盘的分支处理

EA 在 OnInit 里若缓冲区创建失败,会先 PrintFormat 打出错误码再 return INIT_FAILED;同样,EventChartCustom 发送自定义事件失败也走同一条路。这两处返回失败前不会触发 OnDeinit 的模型保存逻辑,因为 reason 会被置为 REASON_INITFAILED。 OnDeinit 中只在 reason 既不是 REASON_INITFAILED 也不是 REASON_RECOMPILE 时才把四个网络(Actor / DOT / Decoder / Critic)以 TimeCurrent() 时间戳写入对应 .nnw 文件。也就是说,重编译导致的卸载不会覆盖你盘面上的训练权重,这个细节能避免来回改代码把模型冲掉。 Train 函数里每轮迭代先用 GetProbTrajectories 取阈值 0.9 的概率轨迹,再用双重 MathRand() 平方做非线性采样挑 state,batch 固定为 GPTBars+48。若算出的 state<=0 就 iter-- 并 continue,相当于丢弃这次无效抽样。外汇与贵金属杠杆高,这类自研训练 EA 在实盘前务必用策略测试器跑足样本,过拟合概率不低。

MQL5 / C++
  PrintFormat("Error of create buffers: %d", GetLastError());
  class="kw">return INIT_FAILED;
   }
  if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"))
   {
    PrintFormat("Error of create study event: %d", GetLastError());
    class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
   {
    Actor.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
    DOT.Save(FileName + "DOT.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
    Decoder.Save(FileName + "Dec.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
    Critic.Save(FileName + "Crt.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
   }
  class="kw">delete Result;
  class="kw">delete OpenCL;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Train function                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
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);

「把账户状态塞进策略网络的喂数据写法」

这段逻辑干了两件事:先跑轨迹网络(DOT + Decoder 的前向传播),再把账户历史归一化成一组特征向量喂给后续 Actor。任何一步 feedForward 失败就直接 Stop 并 break,训练循环立刻停,不会带着脏状态往下走。 账户特征里最值得盯的是那几个比值:余额变化率用 (account[0]-PrevBalance)/PrevBalance,权益相对余额用 account[1]/PrevBalance,回撤率用 (account[1]-PrevEquity)/PrevEquity。这些量纲统一到「相对于前一刻余额」之后,网络才不容易被绝对资金量带偏。外汇和贵金属杠杆高,这类归一化能降低爆仓样本对梯度的一次性冲击,但模型仍可能过拟合极端行情。 时间编码那段把时间戳分别除以年、月、周、日周期再套 sin/cos,等于手工注入周期先验。其中年周期基准写死为 D'2024.01.01' - D'2023.01.01'(约 31536000 秒),月用 PERIOD_MN1、周用 PERIOD_W1、日用 PERIOD_D1 的 PeriodSeconds()。开 MT5 把这段贴进 EA 训练循环,改一下年基准或去掉周线分量,能直接看出策略对周期敏感的程度上不下得来。

MQL5 / C++
class=class="str">"cmt">//--- Trajectory
   if(!DOT.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
      {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         class="kw">break;
      }
   if(!Decoder.feedForward((CNet*)GetPointer(DOT), LatentLayer, (CNet*)GetPointer(DOT)))
      {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         class="kw">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);
   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

Actor-Critic 反向传播与折扣回报计算

这段训练循环把 Actor 与 Critic 串起来做前向和反向传播。前向阶段若 Actor 或 Critic 的 feedForward 返回 false,立即打印函数名与行号、置 Stop=true 并 break,避免脏梯度污染后续权重。 反向传播先用 Buffer[tr].States[i].action 给 Result 赋值,再走 Actor.backProp 与 Decoder/DOT 的梯度回传;任意一步失败同样触发 Stop 退出。注意 DOT.backPropGradient 连续三次调用,中间两次传入 LatentLayer 做隐层梯度累积,最后一次无参收尾。 回报侧用相邻两步 rewards 算时序差分:result = Buffer[i+1].rewards - Buffer[i+2].rewards * DiscFactor,DiscFactor 即折扣因子,控制未来奖励衰减强度。该值设 0.9 与设 0.99 会让策略更新方向出现明显分歧,建议直接在 MT5 里改参看 equity 曲线形变。 Critic 的反向以 Actor 为基准网络,随后 Actor 再以 bAccount、bGradient 做梯度回传;任一层 backProp 失败都会打印「__FUNCTION__ -> __LINE__」并终止本轮,方便你定位是哪一层网络维度不匹配。

MQL5 / C++
if(!Actor.feedForward((CNet *)GetPointer(Decoder), -class="num">1, (CBufferFloat*)GetPointer(bAccount)))
  {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    class="kw">break;
  }
class=class="str">"cmt">//--- Critic
if(!Critic.feedForward((CNet *)GetPointer(Decoder), -class="num">1, (CNet*)GetPointer(Actor)))
  {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    class="kw">break;
  }
Result.AssignArray(Buffer[tr].States[i].action);
if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)) ||
  !Decoder.backPropGradient((CNet *)GetPointer(DOT), -class="num">1, -class="num">1, false) ||
  !DOT.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer) ||
  !DOT.backPropGradient((CBufferFloat*)NULL)
  )
  {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    class="kw">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)) ||
  !Decoder.backPropGradient((CNet *)GetPointer(DOT), -class="num">1, -class="num">1, false) ||
  !DOT.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer) ||
  !DOT.backPropGradient((CBufferFloat*)NULL) ||
  !Actor.backPropGradient((CBufferFloat *)GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient), -class="num">1, false) ||
  !Decoder.backPropGradient((CNet *)GetPointer(DOT), -class="num">1, -class="num">1, false) ||
  !DOT.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer) ||
  !DOT.backPropGradient((CBufferFloat*)NULL)
  )
  {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;

◍ 训练循环里的进度与误差打印

这段代码片段处在强化学习训练主循环收尾处,核心是用定时刷新把 Actor / Critic 的进度百分比和近期平均误差推到图表上。 每过 500 毫秒(GetTickCount 差值 > 500)才更新一次 Comment,避免每 tick 刷屏拖慢 MT5 回测或实盘帧率;percent 由当前迭代 i、状态偏移 state、终点 end 与总轮数 Iterations 算出,范围 0~100。 循环结束后清空 Comment,并用 PrintFormat 把 Actor、Critic 的最终平均误差按 10.7f 精度打到日志,随后调用 ExpertRemove 让 EA 自卸载——适合跑完固定轮数就退出的训练型脚本。 外汇与贵金属行情具有高杠杆高风险,这类自训练 EA 仅建议在策略测试器内验证,实盘加载前务必确认误差收敛且过拟合可控。

MQL5 / C++
              class="kw">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("");
   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">//---
   }

「用 EURUSD 历史切片验证聚合模型」

模型跑通后必须拿真实历史去压一遍。我在 H1 周期下取 EURUSD 做训练与测试分离:用 2023 年前 7 个月数据训练,8 月数据留作测试,输入特征与奖励结构和前文保持一致。 若你手头没有之前的经验回放文件,先把 EA ...\Experts\FAQ\Research.mq5 丢进策略测试器,选训练区间做慢速优化,用随机验算灌满缓冲区;有旧文件则直接重命名为 “FAQ.bd” 续用。指标参数训练与测试必须完全一致,我准备本文时全部用了默认值。 训练阶段用 ...\Experts\FAQ\Study.mq5 在实时图表挂机,它只读书训练、不下单,所以账户余额零风险。我习惯迭代式训练:每轮用当前策略替掉随机策略去补数据,相当于围绕已有政策做在线探索,让后续训练拿到真奖励而非插值。 外汇与贵金属属高风险品种,以下仅为历史回测现象。2023 年 8 月测试集上 EA 共成交 87 笔,45 笔盈利,胜率 51.72%;盈利因子 1.61,恢复因子 1.65,最大及平均盈利均大于亏损对应值。这组合说明该聚合模型在样本外月份未明显坍塌,但换周期或品种需重测。

即插即用模块的实盘验证边界

FAQ 提出的查询聚合模块本质是即插即用结构,可挂进多数基于变换器的对象检测器,用来处理视频与时间序列类任务。动态版还能按源数据自适应生成初始化并调权重。 我们在 MQL5 里落地了这套思路,用真实历史数据训模型,再切到训练集之外的时间段回测。结果倾向支持方法有效,但训练和测试窗口都很短,样本不足以推出稳健结论。 外汇与贵金属行情属高风险环境,这类实验程序仅作方法演示,不能直接当实盘信号源。真要验证,自己开 MT5 拉长样本区间跑一遍最实在。

◍ 文中所附的程序清单

这套 LSTM 多元时间序列预测方案落地到 MT5,靠的是一组分工明确的程序文件,而不是单个 EA 包打天下。 研究阶段由 Research.mq5 与 ResearchRealORL.mq5 两个智能交易系统负责采集样本,前者跑常规窗口,后者用 Real-ORL 方法补样本;随后 Study.mq5 做模型训练,Test.mq5 做离线回测验证。 支撑逻辑的是三个类库:Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网接口,NeuroNet.cl 则是 OpenCL 核函数,决定 GPU 上矩阵运算怎么跑。 想自己复现,直接下载文末 MQL5.zip(约 973 KB)解压进 MetaEditor 即可;外汇与贵金属行情高波动,任何模型输出只代表概率倾向,实盘前务必用 Test.mq5 在对应品种历史数据上重跑一遍。

让小布替你跑这套
这类状态上下文聚合的离线回测与特征相关性诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到聚合权重随帧变化的曲线,你只需判断动势信号是否值得跟。

常见问题

朴素方法直接均化相邻帧随机初始化的查询,缺乏帧间语义约束;FAQ 用输入帧特征生成动态查询并学权重,缓解快速移动导致的性能下降。
论文作者指出这种方式难以训练且结果更弱,所以改用对随机查询加帧特征约束、依相邻帧调权重的动态聚合模块。
余弦相似只衡量方向重合度,高频噪声会扭曲权重;外汇贵金属本身高风险,这类聚合需配合滑窗与正则,效果倾向随品种波动结构变化。
目前小布内置的是离线诊断与可视化,不替你自动下单;你可以把聚合后的状态动势作为人工决策的输入参考。
把帧简单等同 K 线根数,忽略交易事件非均匀采样;相邻帧间隔应按交易时间重采样,否则聚合权重会被休市缺口误导。