神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器·综合运用
📘

神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器·综合运用

第 3/3 篇

「回放缓冲里的价值网络训练回路」

这段逻辑跑在 MT5 智能交易的训练分支里,核心是拿历史状态序列喂给前馈网络做价值逼近,并用折扣因子回推目标收益。外汇与贵金属行情的高波动会让 reward 序列噪声放大,训练误差可能偏高,需在实盘前用历史数据验证收敛性。 [CODE]片段里先判断下标 i<0 时让 iter 自减并 continue,注意 check++ 写在 continue 之后永远不会执行,这是个死代码;随后 check 归零,把 Buffer[tr].States[i].state 拷给 State 并拼接其后 ValueBars-1 个状态形成输入向量。 喂完 getPointer(State) 做 feedForward,若失败就置 StopFlag 并 break;接着取 target 与 result 两个 reward 向量,用 target = target - result*MathPow(DiscFactor,ValueBars) 做折扣修正,再 backProp 更新权重。每 500 毫秒用 Comment 打出迭代进度与平均误差,例如 'Value 12.34% -> Error 0.00123456' 这类数值,可直接在 MT5 终端看。 训练收尾会 PrintFormat 输出最终平均误差并调用 ExpertRemove() 卸载 EA,避免占资源。CreateDescriptions 函数则负责清掉旧 agent 描述、准备输入层,是网络结构初始化的入口。

MQL5 / C++
if(i < class="num">0)
  {
   iter--;
   class="kw">continue;
   check++;
   if(check >= total_tr)
     break;
  }
check = class="num">0;
class=class="str">"cmt">//--- History data
State.AssignArray(Buffer[tr].States[i].state);
for(class="type">int state = class="num">1; state < ValueBars; state++)
   State.AddArray(Buffer[tr].States[i + state].state);
class=class="str">"cmt">//--- Study
if(!Value.feedForward(GetPointer(State)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   StopFlag = true;
   break;
  }
vector<class="type">float> target, result;
target.Assign(Buffer[tr].States[i + ValueBars - class="num">1].rewards);
result.Assign(Buffer[tr].States[i + class="num">2 * ValueBars - class="num">1].rewards);
target = target - result*MathPow(DiscFactor,ValueBars);
Value.getResults(result);
Result.AssignArray(CAGrad(target - result) + result);
if(!Value.backProp(Result, (CBufferFloat *)NULL, (CBufferFloat *)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   StopFlag = true;
   break;
  }
class=class="str">"cmt">//---
if(GetTickCount() - ticks > class="num">500)
  {
   class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Value",
iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Value.getRecentAverageError());
   Comment(str);
   ticks = GetTickCount();
  }
 }
 Comment("");
class=class="str">"cmt">//---
 PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Value", Value.getRecentAverageError());
 ExpertRemove();
class=class="str">"cmt">//---
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateDescriptions(CArrayObj *agent)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!agent)
   {
    agent = new CArrayObj();
    if(!agent)
      class="kw">return false;
   }
class=class="str">"cmt">//--- Agent
  agent.Clear();
class=class="str">"cmt">//--- Input layer

逐层堆出深度强化网络骨架

在 MT5 里搭一个面向价格行为的智能体,最先落地的不是策略逻辑,而是网络层的物理结构。下面这段构造代码把输入到隐层的七层骨架一次钉死,每层都用 CLayerDescription 描述类型、宽度与优化器,任何一层 Add 失败就直接回 false 并释放描述符。 输入层用 defNeuronBaseOCL,节点数由 BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions + NRewards 拼出来,激活留 None、优化器选 ADAM。第二层接 BatchNorm(defNeuronBatchNormOCL),batch 写死 1000,用来稳初始梯度。 第三层 Embedding 把 HistoryBars 作节点数,用 ArrayCopy 把五类窗口长度拷进 descr.windows,window_out 设为 EmbeddingSize。第四层多头稀疏注意力(defNeuronMLMHSparseAttentionOCL)把上一层节点数乘以 5,window_out=32、layers=4、step=8,概率采样走 Sparse——这一步直接决定显存和计算量的量级。 后面三层是卷积加两个全连接:卷积层 window_out=16、激活 LReLU;第五层 LatentCount 宽、LReLU;第六层同样宽、TANH 收口。把这段代码原样丢进你的 EA 初始化里,改 LatentCount 或 step 就能在 MT5 策略测试器里看到推理耗时和权重大小的明显漂移。外汇与贵金属杠杆高,这类模型过拟合后实盘回撤可能非常剧烈,参数先小批量验证。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions + NRewards);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.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 = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.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 = defNeuronEmbeddingOCL;
   prev_count = descr.count = HistoryBars;
     {
       class="type">int temp[] = {BarDescr * NBarInPattern, AccountDescr, TimeDescription, NActions, NRewards};
       ArrayCopy(descr.windows, temp);
     }
   class="type">int prev_wout = descr.window_out = EmbeddingSize;
   if(!agent.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 = defNeuronMLMHSparseAttentionOCL;
   prev_count = descr.count = prev_count * class="num">5;
   descr.window = EmbeddingSize;
   descr.step = class="num">8;
   descr.window_out = class="num">32;
   descr.layers = class="num">4;
   descr.probability = Sparse;
   descr.optimization = ADAM;
   if(!agent.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 = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.step = EmbeddingSize;
   descr.window_out = class="num">16;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!agent.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 = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!agent.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;
   prev_count = descr.count = LatentCount;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7

◍ 网络末层堆叠与训练循环的样本抽取

在智能体架构里,第 7 层之后连续挂了三层描述符:先是一个 LReLU 激活、ADAM 优化的潜变量层(节点数 = LatentCount),再接一个无激活的线性层(节点数 = WorkerInput),最后用 SoftMax 输出层把动作概率归一化到 WorkerInput 维。三层都走 Add() 注册,任何一步失败就 delete descr 并回 false,保证显存不漏。 训练函数 Train() 里先取 Buffer 总样本数 total_tr,用 GetTickCount() 记墙钟时间。主循环按 Iterations 上限跑,同时受 IsStopped() 与 StopFlag 拦截,任一为真就停。 样本抽取不是顺序的:tr 用 MathRand()/32767.0 随机挑一条序列,i 再用两次随机平方把起点压到靠后区段,下限保护是 MathMin(Buffer[tr].Total, 20+ValueBars)。若 i<0 就 iter-- 并重抽,避免越界。 每个 state 从 i 扫到 i+HistoryBars*3 与 Buffer[tr].Total-2-ValueBars 的较小值,把 Buffer[tr].States[state].state 灌进 State,账户余额用上一帧的 account[0] 做 PrevBalance。外汇与贵金属行情下用这类 RL 代理复盘,杠杆风险极高,实盘前务必在 MT5 策略测试器跑通再谈。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!agent.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 = defNeuronBaseOCL;
   descr.count = WorkerInput;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">9
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = WorkerInput;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Train function                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Train(class="type">void)
  {
   class="type">int total_tr = ArraySize(Buffer);
   class="type">uint ticks = GetTickCount();
   class="type">float err=class="num">0;
   class="type">int err_count=class="num">0;
class=class="str">"cmt">//---
   class="type">bool StopFlag = false;
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++)
     {
      class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1));
      class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * MathMax(Buffer[tr].Total - class="num">2 *
HistoryBars-ValueBars,MathMin(Buffer[tr].Total,class="num">20+ValueBars)));
      if(i < class="num">0)
        {
         iter--;
         class="kw">continue;
        }
      Actions = vector<class="type">float>::Zeros(NActions);
      for(class="type">int state = i; state < MathMin(Buffer[tr].Total - class="num">2 - ValueBars,i + HistoryBars * class="num">3); state++)
        {
         class=class="str">"cmt">//--- History data
         State.AssignArray(Buffer[tr].States[state].state);
         class=class="str">"cmt">//--- Account description
         class="type">float PrevBalance = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">0] : Buffer[tr].States[state - class="num">1].account[class="num">0]);

「把账户状态压成强化学习特征向量」

这段逻辑干的事很直接:把某一笔训练轨迹里的账户快照,转成一组可喂给神经网络的浮点特征。前几行先算权益增量和余额占比——比如用 (account[0]-PrevBalance)/PrevBalance 表达本金收益率,用 account[1]/PrevBalance 把权益归一化,避免不同账户规模干扰策略泛化。 时间维度被编码成周期正弦/余弦。代码里拿 account[7] 当时间戳,分别除以 2023 全年秒数、月线秒数、周线秒数、日线秒数,再乘 2π 取三角函数值。这样网络能隐式学到‘月初/周末/跨年’这类节奏,对外汇和贵金属这种受周期驱动的市场可能有用,但杠杆品种波动剧烈,信号失效也快。 Return-To-Go 那段值得盯:rtg 取下一状态的奖励序列,再减去往前 ValueBars 步奖励乘折扣因子 DiscFactor 的幂,等于在做多步信用分配。最后 Agent.feedForward 和 Worker.feedForward 串起来做策略前向,一旦返回失败就置 StopFlag 退出循环——你开 MT5 跑这套时,若日志频繁打印函数名加行号,基本就是网络前向崩了。

MQL5 / C++
class="type">float PrevEquity = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">1] : Buffer[tr].States[state - class="num">1].account[class="num">1]);
State.Add((Buffer[tr].States[state].account[class="num">0] - PrevBalance) / PrevBalance);
State.Add(Buffer[tr].States[state].account[class="num">1] / PrevBalance);
State.Add((Buffer[tr].States[state].account[class="num">1] - PrevEquity) / PrevEquity);
State.Add(Buffer[tr].States[state].account[class="num">2]);
State.Add(Buffer[tr].States[state].account[class="num">3]);
State.Add(Buffer[tr].States[state].account[class="num">4] / PrevBalance);
State.Add(Buffer[tr].States[state].account[class="num">5] / PrevBalance);
State.Add(Buffer[tr].States[state].account[class="num">6] / PrevBalance);
class=class="str">"cmt">//--- Time label
class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (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;);
State.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
State.Add((class="type">float)MathCos(class="num">2.0 * M_PI * x));
x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
State.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
State.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
class=class="str">"cmt">//--- Prev action
State.AddArray(Actions);
class=class="str">"cmt">//--- Return-To-Go
vector<class="type">float> rtg;
rtg.Assign(Buffer[tr].States[state+class="num">1].rewards);
Actions.Assign(Buffer[tr].States[state+ValueBars].rewards);
rtg=rtg-Actions*MathPow(DiscFactor,ValueBars);
State.AddArray(rtg);
class=class="str">"cmt">//--- Policy Feed Forward
if(!Agent.feedForward(GetPointer(State), class="num">1, false, (CBufferFloat *)NULL) ||
   !Worker.feedForward((CNet *)GetPointer(Agent),-class="num">1,(CBufferFloat *)NULL))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   StopFlag = true;
   break;
   }
class=class="str">"cmt">//--- Policy study
Actions.Assign(Buffer[tr].States[state].action);
Worker.getResults(rtg);
if(err_count==class="num">0)
   err=rtg.Loss(Actions,LOSS_MSE);

训练循环与每根新K线的特征拼装

强化学习智能体的离线训练落在这段循环里:误差用滑动平均更新,err_count 未满 1000 时持续累加,每轮把梯度写回 Worker 与 Agent。若 backProp 任意一步失败,立即打印函数名与行号、置 StopFlag 并 break,避免脏权重继续传播。 训练过程并非逐 tick 刷屏,而是用 GetTickCount() 做节流——距上次输出超过 500 毫秒才通过 Comment 打印进度,格式为「Agent 完成百分比 -> Error 十五位浮点」。这样在 MT5 策略测试器里跑长时间训练不会因频繁绘图拖慢回测。 实盘侧靠 OnTick 驱动,但首行 IsNewBar() 过滤掉同根 K 线内的冗余 tick,只在换柱时 CopyRates 取 History 长度的历史。RSI、CCI、ATR、MACD 四个指标与 Symb 行情对象都显式 Refresh,否则可能读到上一 tick 的缓存值。 特征向量 bState 按根回溯 ValueBars-1 到 0:每根塞入 close-open、high-open、low-open 三个位移,以及 tick_volume/1000.0、rsi、cci,再加上循环外已取的 macd 与 signal。任一指标返回 EMPTY_VALUE 就 continue 跳过该根,保证送进网络的样本无空洞。外汇与贵金属波动受杠杆放大,直接接实盘前务必先在历史数据上验证该特征拼接是否与你标的的周期匹配。

MQL5 / C++
else
   err=(err*err_count + rtg.Loss(Actions,LOSS_MSE))/(err_count+class="num">1);
   if(err_count<class="num">1000)
      err_count++;
   Result.AssignArray(CAGrad(Actions - rtg) + rtg);
   if(!Worker.backProp(Result,NULL,NULL) ||
      !Agent.backPropGradient((CBufferFloat *)NULL, (CBufferFloat *)NULL))
      {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      StopFlag = true;
      break;
      }
   if(GetTickCount() - ticks > class="num">500)
      {
      class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Agent", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), err);
      Comment(str);
      ticks = GetTickCount();
      }
   }
   }
 Comment("");
class=class="str">"cmt">//---
 PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Agent", err);
 ExpertRemove();
class=class="str">"cmt">//---
 }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  if(!IsNewBar())
   class="kw">return;
class=class="str">"cmt">//---
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), History, 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=class="str">"cmt">//--- History data
  class="type">float atr = class="num">0;
  bState.Clear();
  for(class="type">int b = ValueBars-class="num">1; b >=class="num">0; 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);
   if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
      class="kw">continue;
   class=class="str">"cmt">//---
   bState.Add((class="type">float)(Rates[b].close - open));
   bState.Add((class="type">float)(Rates[b].high - open));
   bState.Add((class="type">float)(Rates[b].low - open));
   bState.Add((class="type">float)(Rates[b].tick_volume / class="num">1000.0f));
   bState.Add(rsi);
   bState.Add(cci);

◍ 把K线与账户状态压进神经网络张量

这段逻辑干的事很直接:把每根bar的几何信息与副图指标,按固定偏移塞进一个一维状态数组,再交给前馈网络做推理。NBarInPattern根bar,每根占BarDescr个浮点位,shift = b * BarDescr就是定位游标。 close-open、high-open、low-open三个差量保留价格相对开盘的位移结构;tick_volume除以1000压缩量级,避免成交量绝对数淹没其他特征。RSI、CCI、ATR、MACD主线与信号线各占一位,任一指标返回EMPTY_VALUE就跳过该bar,防止脏数据进网络。 账户侧只取了余额与净值两个double,写进account[0]和account[1]。持仓扫描则遍历PositionsTotal(),用PositionGetSymbol过滤当前品种,按POSITION_TYPE_BUY/SELL分别累加手数与浮动盈亏——buy_value、sell_value、buy_profit、sell_profit四个量就是给模型看的敞口快照。 multiplyer = 1.0/(60*60*10)这条看着像把时间维度归一化到某类衰减系数,具体用途要结合后续 reward 设计看。外汇与贵金属杠杆高,这类把实时持仓喂给模型的写法,回测与实盘差异可能很大,上MT5跑之前先确认滑点假设。

MQL5 / C++
   bState.Add(atr);
   bState.Add(macd);
   bState.Add(sign);
   }
   if(!Value.feedForward(GetPointer(bState), class="num">1, false))
      class="kw">return;
   for(class="type">int b = class="num">0; b < NBarInPattern; 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);
      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);
class=class="str">"cmt">//--- Account description
   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;

「把账户快照喂给神经网络的收尾动作」

这段逻辑处在每根 K 线收盘后的状态打包阶段:先把买/卖持仓价值、浮盈、持仓时间折扣写进 sState.account 的 2~6 号位,再用 0 号位净值减 PrevBalance 算出本根收益率,1 号位权益占 PrevBalance 比重、权益回撤率也一并入栈。 时间特征没有用裸时间戳,而是拿 Rates[0].time 分别除以 2023 全年秒数、月线/周线/日线周期秒数,再套 2π 正弦余弦。这样年周期用 sin、月用 cos、周和日用 sin,把季节性波动压进 [-1,1] 区间,避免量纲把网络梯度冲垮。 bState 最后把上一次 Agent 输出 AgentResult 和 Value 的 Result 数组原样追加,送进 Agent.feedForward 做推理、Worker 做动作映射;失败就直接 return,不更新 PrevBalance/PrevEquity。 推理完从 Worker 取 temp,按最小手数 LotsMin、步进 LotsStep、StopsLevel 点值算 stops;若 temp[0]>=temp[3] 则多单优先减空单信号,反之空单减多单,delta 用两向量差的绝对值求和衡量本次决策相对上根的变动幅度。外汇与贵金属杠杆品种下,这套信号若直接跟单,滑点和点差可能让 delta 很小的调整也产生不成比例回撤,属高风险操作。

MQL5 / C++
      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;
class=class="str">"cmt">//---
  bState.Add((class="type">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance));
  bState.Add((class="type">float)(sState.account[class="num">1] / PrevBalance));
  bState.Add((class="type">float)((sState.account[class="num">1] - PrevEquity) / PrevEquity));
  bState.Add(sState.account[class="num">2]);
  bState.Add(sState.account[class="num">3]);
  bState.Add((class="type">float)(sState.account[class="num">4] / PrevBalance));
  bState.Add((class="type">float)(sState.account[class="num">5] / PrevBalance));
  bState.Add((class="type">float)(sState.account[class="num">6] / PrevBalance));
class=class="str">"cmt">//--- Time label
  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;);
  bState.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);
  bState.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);
  bState.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);
  bState.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
class=class="str">"cmt">//--- Prev action
  bState.AddArray(AgentResult);
class=class="str">"cmt">//--- Return to go
  Value.getResults(Result);
  bState.AddArray(Result);
  if(!Agent.feedForward(GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL) ||
     !Worker.feedForward((CNet *)GetPointer(Agent), -class="num">1, (CBufferFloat *)NULL))
     class="kw">return;
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
class=class="str">"cmt">//---
  vector<class="type">float> temp;
  Worker.getResults(temp);
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="type">float delta = MathAbs(AgentResult - temp).Sum();
  AgentResult = temp;
class=class="str">"cmt">//--- buy control

双边持仓的清算与加码判定

这段逻辑把多空两边分开处理,核心是先看 temp 数组里的手数、止盈止损步长是否越过硬下限,再决定平掉还是按模型输出调仓。temp[0] 是多单可用手数,temp[1]、temp[2] 分别是多单 TP、SL 对应的 MaxTP、MaxSL 倍数;只要手数低于 min_lot,或换算后的 TP/SL 距离不超过 stops 阈值,就直接触发 CloseByDirection 清空同方向仓位。 不满足条件时,多单目标手数用 min_lot 加步长取整算出来:buy_lot = min_lot + MathRound((temp[0]-min_lot)/step_lot)*step_lot,TP 挂在 Ask 上方 temp[1]*MaxTP*Point,SL 挂在 Ask 下方 temp[2]*MaxSL*Point,均经 NormalizePrice 对齐报价精度。已有多单就先 TrailPosition 移动止损止盈;若实际持仓 buy_value 与目标不符,多出部分 ClosePartial 减仓,不足部分 Trade.Buy 补单。 空单分支完全对称,temp[3~5] 对应卖单的手数、TP、SL 倍数,价格基准换成 Bid,SL 落在 Bid 上方、TP 在 Bid 下方。 尾部把状态写回强化学习样本:shift 按 BarDescr*(NBarInPattern-1) 取历史偏移,rewards[0]、rewards[1] 记录基准与相对变化;若当前无任何持仓,rewards[2] 扣掉 atr/PrevBalance 作为闲置惩罚,否则置 0。最后把 AgentResult 塞进 action 并 Base.Add,写入失败就 ExpertRemove 终止 EA。外汇与贵金属杠杆高,这类自动调仓在滑点扩大时可能频繁触发减仓,实盘前务必在 MT5 策略测试器用真实点差回测。

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 = Symb.NormalizePrice(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point());
      class="type">class="kw">double buy_sl = Symb.NormalizePrice(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point());
      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 = Symb.NormalizePrice(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point());
      class="type">class="kw">double sell_sl = Symb.NormalizePrice(Symb.Bid() + temp[class="num">5] * MaxSL * Symb.Point());
      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);
      }
    }
class=class="str">"cmt">//---
  class="type">int shift = BarDescr * (NBarInPattern - class="num">1);
  sState.rewards[class="num">0] = bState[shift];
  sState.rewards[class="num">1] = bState[shift + class="num">1] - class="num">1.0f;
  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] = AgentResult[i];
  if(!Base.Add(sState))
      ExpertRemove();
}

◍ 训练完的模型拿八月数据验真

前面把数据收集和模型训练拆成独立 EA 跑,StudyWorker.mq5 自主采样不用现成样本,同时并行攒训练集。2023 年前 7 个月的历史区间捞样本相当累,即便把智能体动作采样界限压得很小,多数验算还是过不了余额正增长这一关,只好在优化模式里把每次验算的迭代次数也设为可调参数去挑计划界限。 局部政策模型训出来后,调度器和成本函数模型训练一起开,整体耗时明显压下来了。熬过这段繁琐流程,总算拿到一个在训练集外也能跑出盈利的模型。 拿 2023 年 8 月的历史数据做测试,盈利因子只有 1.13,盈利和无盈利仓位比例接近 1:1。也就是说没靠胜率吃饭,所有利润都来自平均盈利单大于平均亏损单——外汇与贵金属杠杆品种这种薄优势模型,实盘漂移风险偏高,MT5 里用策略测试器复跑这段区间最直观。

「并行训练省下的时间值得拿来验」

前面几节把控制转换器的架构和 MQL5 实现拆开了讲,落到工程层有个实在好处:把模型训练切到互不相干的独立 EA 里跑,能并行扛好几个任务。实测下来整体训练耗时明显被压短,这对想自己复现的人是直接可抄的点。 我们在训练集和测试集上都跑出过能盈利的模型,说明这条路线在概率上是有效的,可以拿去搭交易方案。但外汇和贵金属是高杠杆高风险品种,原文里那些程序只是演示算法逻辑,别直接挂真盘。 你开 MT5 建两个无关 EA 各跑一部分样本,对比串行的耗时差,比看结论更管用。

顺着链接能挖到的几篇相关硬货

这一节本身没有正文论述,只是把几篇相邻技术文章列成入口。对做价格行为量化的人来说,值得点开的是「神经网络变得简单(第 15 部分):使用 MQL5 进行数据聚类」,它直接给了一套在 MT5 里跑 K 均值聚类的可行路径,能把行情状态切成若干簇再做后续规则。 另外三篇偏机器人导航与强化学习理论:控制变压器的回波条件序列建模、决策转换器(DT)、以及离线强化学习中的乐观情绪问题。它们的方法论如果能迁移到择时建模,前提是你先搞清样本外过拟合的风险——外汇和贵金属杠杆高、跳空频繁,直接套离线 RL 乐观假设容易翻车。 真要动手,建议先开 MT5 把第 15 部分的数据聚类代码跑通,用 EURUSD 的 H1 波动特征做输入,看簇中心是否随波动率 regime 切换。这一步验证了,再回头啃 DT 那篇不迟。

◍ 画得少,看得清

整套 LSTM 优化方案落到工程层,就是八个文件各司其职:Faza1.mq5 负责样本收集,Study.mq5 做调度器训练,StudyWorker.mq5 训局部政策模型,StudyValue.mq5 训成本函数,Test.mq5 跑模型测试;Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网类,NeuroNet.cl 则是 OpenCL 端的算子库。 把这套 mql5.zip(579 KB)解进 MT5 的 MQL5 目录,先挂 Faza1 跑一段历史收集轨迹,再依次起 Study 系列做训练,最后用 Test 验泛化——外汇与贵金属行情高波动、杠杆风险大,离线训出的网络在实盘只具概率优势,不代表稳定盈利。 收尾不必堆功能:能看清哪段序列被网络记住、哪段被丢弃,比堆十层结构更有用。少画几根线,反而容易盯住策略真正的决策边界。

常见问题

用账户权益除以初始入金做比例缩放,持仓量除以最大允许手数,把输出限制在 0~1 区间,避免梯度爆炸。
不要按顺序,每根新K线拼装后随机打乱抽 batch,否则网络会过拟合近期样本,泛化变差。
可以,小布盯盘的 AIGC 已内置特征拼装流程,打开对应品种页就能直接看拼好的张量预览,不用自己写脚本。
更可能是每根K线特征拼装漏了账户状态维度,先核对输入向量长度,再逐步加大回放缓冲到 1 万条以上试。
决策转换器管宏观动作序列生成,深度强化骨架做细粒度价值评估,两者通过末层堆叠共享特征输出。