神经网络变得简单(第 60 部分):在线决策转换器(ODT)·进阶篇
🧠

神经网络变得简单(第 60 部分):在线决策转换器(ODT)·进阶篇

(2/3)·经典决策转换器跑久了就亏,本文拆解 ODT 如何用在线微调补上这条裂缝

含代码示例 第 2/3 篇

接上篇,我们继续深挖决策转换器的实战短板。离线训好的模型在测试段开头能盈利,越往后越容易连续无盈利交易,把前面利润吐光。定期重训太笨重,在线优调才是更顺手的解法。

EA 初始化时的指标与模型装载

这段初始化逻辑把 MACD、RSI、CCI、ATR 四个指标连同两组神经网络模型一次性挂到当前图表。MACD 默认参数沿用了经典组合:快线 12、慢线 26、信号线 9,价格源取收盘价,改这几个数就能直接换周期风格。 输入组里还有两个容易被忽略的开关:StudyIters=5 控制学习迭代次数,StudyPeriod=120 规定两次学习之间隔 120 根 K 线。做外汇或贵金属回测时,这两个值会明显改变样本外表现,属于高波动品种里必须手动压一压的变量。 OnInit 里先调 LoadTotalBase,再逐个 Create 指标句柄,任何一步失败就返回 INIT_FAILED,MT5 会直接在日志报哪一行断的。模型部分从 FileName 指向的 Act.nnw 与 RTG.nnw 读权重,若文件缺失,代码会新建空数组对象而不是崩溃——这点对调试很有用。

MQL5 / C++
input group                "---- MACD ----"
input class="type">int                FastPeriod   =  class="num">12;          class=class="str">"cmt">//Fast
input class="type">int                SlowPeriod   =  class="num">26;          class=class="str">"cmt">//Slow
input class="type">int                SignalPeriod=   class="num">9;          class=class="str">"cmt">//Signal
input ENUM_APPLIED_PRICE  MACDPrice    =  PRICE_CLOSE; class=class="str">"cmt">//Applied price
class=class="str">"cmt">//---
input class="type">int                StudyIters   =   class="num">5;          class=class="str">"cmt">//Iterations to Study
input class="type">int                StudyPeriod =  class="num">120;          class=class="str">"cmt">//Bars between Studies
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   LoadTotalBase();
   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(NBarInPattern) || !CCI.BufferResize(NBarInPattern) ||
       !ATR.BufferResize(NBarInPattern) || !MACD.BufferResize(NBarInPattern))
     {
       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(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
       !RTG.Load(FileName + "RTG.nnw", dtStudied, true) ||
       !AgentStudy.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
       !RTGStudy.Load(FileName + "RTG.nnw", dtStudied, true))
     {
       PrintFormat("Can&class="macro">#x27;t load pretrained models");
       CArrayObj *agent = new CArrayObj();
       CArrayObj *rtg = new CArrayObj();

◍ 初始化失败点与模型落盘的细节

强化学习 EA 在 OnInit 里先建描述对象,若 CreateDescriptions 返回 false 会立刻 delete 掉 agent 与 rtg 并打印 "Can't create description of models",直接 return INIT_FAILED。这一步卡住说明状态空间的描述生成有问题,常见原因是 NBarInPattern 或 BarDescr 配置和传入数据对不上。 紧接着的 Agent.Create / RTG.Create / AgentStudy.Create / RTGStudy.Create 任一失败也会走同一套清理逻辑并返回 INIT_FAILED。注意这里 delete 的是临时 agent、rtg 指针,真正参与推理的 study 对象在后面才用。 初始化尾段有两次维度校验:Result.Total() 必须等于 NActions,否则报 "The scope of the actor does not match the actions count";GetLayerOutput(0, Result) 后 Total 必须等于 NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions,差一个就 INIT_FAILED。这组加法就是你的状态向量长度,调参时改任一常量都要同步核对。 OnDeinit 里会把训练好的网络存成 Act.nnw 与 RTG.nnw,并调用 SaveTotalBase 落回经验池。回测中断或换品种前,确认这两个文件已生成,否则下次加载是冷启动。OnTick 开头只用 IsNewBar 过滤,非新 K 线直接 return,说明决策频率绑定图表周期,想提高响应就把 EA 挂到更低周期但注意外汇杠杆风险偏高。

MQL5 / C++
if(!CreateDescriptions(agent, rtg))
  {
   class="kw">delete agent;
   class="kw">delete rtg;
   PrintFormat("Can&class="macro">#x27;t create description of models");
   class="kw">return INIT_FAILED;
  }
if(!Agent.Create(agent) ||
   !RTG.Create(rtg) ||
   !AgentStudy.Create(agent) ||
   !RTGStudy.Create(rtg))
  {
   class="kw">delete agent;
   class="kw">delete rtg;
   PrintFormat("Can&class="macro">#x27;t create models");
   class="kw">return INIT_FAILED;
  }
 class="kw">delete agent;
 class="kw">delete rtg;
 class=class="str">"cmt">//---
 }
class=class="str">"cmt">//---
  Agent.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;
   }
  AgentResult = vector<class="type">float>::Zeros(NActions);
class=class="str">"cmt">//---
  Agent.GetLayerOutput(class="num">0, Result);
  if(Result.Total() != (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions))
   {
    PrintFormat("Input size of Actor doesn&class="macro">#x27;t match state description(%d <> %d)",
Result.Total(), (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions));
    class="kw">return INIT_FAILED;
   }
  Agent.Clear();
  RTG.Clear();
  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 deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  AgentStudy.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
  RTGStudy.Save(FileName + "RTG.nnw", TimeCurrent(), true);
  class="kw">delete Result;
  class="type">int total = ArraySize(Buffer);
  printf("Saving %d", MathMin(total + class="num">1, MaxReplayBuffer));
  SaveTotalBase();
  Print("Saved");
  }
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=class="str">"cmt">//---

「把多指标状态压进一维特征数组」

这段逻辑做的是把一段 K 线窗口里的价格行为与指标读数,统一塞进一个定长浮点数组,方便后续直接喂给模型或比对。先通过 CopyRates 拉取从倒数第 1 根 bar 起、共 NBarInPattern 根的历史数据,并强制把 Rates 设为序列排列(下标 0 为最新),若设置失败直接 return,避免后面错位。 随后逐个 Refresh 指标句柄(RSI/CCI/ATR/MACD/符号),保证读到的是当前帧数据。循环里每根 bar 占 BarDescr 个槽位,依次写入 close-open、high-open、low-open、tick_volume/1000、以及 rsi/cci/atr/macd/signal 九个值;任一指标返回 EMPTY_VALUE 就跳过该 bar,不污染特征。 账户层只抓了两个量:balance 与 equity,写进 account[0]、account[1]。持仓统计部分则遍历 PositionsTotal,按品种过滤后把多单 volume 累加进 buy_value、利润进 buy_profit,空单同理,multiplier 用 1/(60*60*10) 做时间衰减系数,可用来折算持仓时长权重。外汇与贵金属杠杆高,这类特征若用于实盘信号,须先在策略测试器跑历史样本验证分布稳定性。 复制下面代码到 MT5 的 EA 头里,把 NBarInPattern 调到你能覆盖的波段长度(例如 30),即可在观察器里看 sState.state 每根 bar 的九维切片。

MQL5 / C++
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), NBarInPattern, 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;
  for(class="type">int b = class="num">0; b < (class="type">int)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;

把持仓状态压进神经网络的输入向量

这段逻辑干的事很直接:先按 POSITION_TYPE_SELL 分支把空单手数和浮动盈亏分别累加进 sell_value、sell_profit,再算一笔 position_discount——用持仓利润减去「当前时间减开仓时间」乘系数乘利润绝对值,等于给长周期持仓打了个时间折扣。 sState.account 数组下标 2~7 依次落了买量、卖量、买利、卖利、折扣、当前 K 线时间;随后 bState 把账户回撤率((余额-前余额)/前余额)、权益占比、权益回撤、买卖量原值、买卖利占比、折扣占比共 8 个浮点塞进去,构成 RL 状态基底。 时间标签那段把 Rates[0].time 分别除以 2023 全年秒数、月线/周线/日线周期秒数,再跑 sin/cos(2πx),等于手工编码了年/月/周/日的周期相位,喂给网络当季节性特征。 真正下注前,代码用 RTG.feedForward 和 Agent.feedForward 两层前向传播拿结果,更新 PrevBalance/PrevEquity,再从 Agent 取 temp 向量。temp[0] 与 temp[3] 做互斥裁剪(谁大减谁、小的归零)后,才用 LotsMin、LotsStep、StopsLevel*Point 约束下单手数和止损距——外汇贵金属杠杆高,这种裁剪能压住同向重复开仓的敞口膨胀。

MQL5 / C++
      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;
  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
  if(!RTG.feedForward(GetPointer(bState)))
     class="kw">return;
  RTG.getResults(Result);
  bState.AddArray(Result);
class=class="str">"cmt">//---
  if(!Agent.feedForward(GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
     class="kw">return;
class=class="str">"cmt">//---
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
class=class="str">"cmt">//---
  vector<class="type">float> temp;
  Agent.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
     {

◍ 多空双向的仓位再平衡逻辑

这段控制块把 agent 算出的目标权重 temp[] 直接映射到实盘下单动作,买和卖各走一套对称判断。先看买侧:当 temp[0] 小于最小手数,或止盈/止损距离换算后小于等于 stops 阈值时,若有买仓就整仓平掉;否则按 step_lot 步进把目标手数对齐,并依 MaxTP、MaxSL 和点值算出 tp/sl 价格。 若已有买仓,先调用 TrailPosition 把止损止盈 trailing 到新位置;若当前买仓手数 buy_value 与目标 buy_lot 不一致,多出部分用 ClosePartial 减仓,不足部分用 Trade.Buy 补单。卖侧逻辑完全镜像,只是价格基于 Bid 且 sl 在 Bid 之上、tp 在 Bid 之下。 最后一段把状态写回强化学习结构:用 BarDescr*(NBarInPattern-1) 定位上一pattern 的末端 bar,把 bState 偏移量塞进 sState.rewards,并在无持仓时按 atr/PrevBalance 扣减奖励、有持仓则奖励清零。外汇与贵金属杠杆高,这类按权重自动加减仓的逻辑在滑点扩大时可能触发频繁往返交易,建议先在 MT5 策略测试器用点差 20 点以上的历史段跑一遍。

MQL5 / C++
   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
   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];

「训练循环里的样本剔除与随机抽帧」

上面这段落在 OnTick 末尾调用 Train(),但真正干活的是 Train(void) 里的内存整理与迭代采样。先看头部:当 Base.Total 超过 StudyPeriod 才把当前 Base 压入 Buffer 并做累计收益处理,否则不进样本池。 样本清理有一段容易被忽略:clear = Base.Total + StudyPeriod - Buffer_Size,若 clear > 0 就调 Base.ClearFirstN(clear) 丢掉最早的状态,避免数组无限膨胀。MT5 里若 Buffer_Size 设太小,老样本被清太快,回测可能偏向近期行情。 随后用 count 变量压缩 Buffer:凡是 Buffer[i].Total < StudyPeriod 的残样直接前移覆盖,最后 ArrayResizetotal_tr - count。这一步保证后续训练只跑满周期的样本。 迭代部分用 MathRand()/32767.0 先随机抽一条轨迹 tr,再用 MathRand()*MathRand()/32767^2 的偏置分布选起点 i,倾向落在轨迹中段而非首尾。若 i<0iter-- 重抽,不浪费迭代次数。外汇与贵金属波动跳变多,这种中段采样可能降低极端边界样本干扰,但仍是概率性改善,实盘前请在 MT5 策略测试器用真实点差验证。

MQL5 / C++
  if(!Base.Add(sState))
        ExpertRemove();
class=class="str">"cmt">//---
  if((Bars(_Symbol, TimeFrame) % StudyPeriod) == class="num">0)
     Train();
  }
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);
  if(Base.Total >= StudyPeriod)
    if(ArrayResize(Buffer, total_tr + class="num">1) == (total_tr + class="num">1))
      {
       Buffer[total_tr] = Base;
       Buffer[total_tr].CumRevards();
       total_tr++;
      }
  class="type">int clear = Base.Total + StudyPeriod - Buffer_Size;
  if(clear > class="num">0)
    Base.ClearFirstN(clear);
class=class="str">"cmt">//---
  class="type">int count = class="num">0;
  for(class="type">int i = class="num">0; i < (total_tr + count); i++)
   {
    if(Buffer[i + count].Total < StudyPeriod)
      {
       count++;
       i--;
       class="kw">continue;
      }
    if(count > class="num">0)
       Buffer[i] = Buffer[i + count];
   }
  if(count > class="num">0)
   {
    ArrayResize(Buffer, total_tr - count);
    total_tr = ArraySize(Buffer);
   }
  class="type">uint ticks = GetTickCount();
class=class="str">"cmt">//---
  class="type">bool StopFlag = false;
  for(class="type">int iter = class="num">0; (iter < StudyIters && !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,
                                                                            MathMin(Buffer[tr].Total, class="num">20)));
    if(i < class="num">0)
      {
       iter--;
       class="kw">continue;
      }
    vector<class="type">float> Actions = vector<class="type">float>::Zeros(NActions);
    AgentStudy.Clear();
    RTGStudy.Clear();
    for(class="type">int state = i; state < MathMin(Buffer[tr].Total - class="num">2, class="type">int(i + HistoryBars * class="num">1.5)); state++)
      {
       class=class="str">"cmt">//--- History data
       bState.AssignArray(Buffer[tr].States[state].state);
       class=class="str">"cmt">//--- Account description
交给小布盯盘看回撤拐点
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型收益曲线转弱的那一段,你再决定是否手动介入优调。

常见问题

经典 DT 只用离线轨迹训静态策略,ODT 在离线初训后接一段在线优调,用环境交互数据补状态和动作空间的覆盖盲区,策略倾向随盘面持续修正。
离线集回报偏低且只覆盖有限状态动作子空间,模型遇到分布外行情时预测动作可能失效,从而产生连续无盈利业务并回吐利润。
可以。文中提到操作期上下文长度可能短于训练期,K 作为超参数控制转换器可见的历史令牌步数,实盘可按延迟和资源权衡截断。
目前小布内置的是诊断与看盘辅助,ODT 的训练代码需你在 MT5 端自行部署;小布可帮你盯住策略回撤拐点,减少人工轮巡成本。
存在这种概率。外汇贵金属属高风险品种,在线步长与学习率需保守设,避免模型把短期噪声当成规律,实盘前建议用隔离样本回测。