神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)·综合运用
📘

神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)·综合运用

第 3/3 篇

「EA 初始化里的神经网络装载与校验」

在 MT5 智能交易的 OnInit 里,RTG 这个类对象先尝试从外部文件加载已训练好的网络权重。代码调用 RTG.Load(FileName + "RTG.nnw", temp, temp, temp, dtStudied, true),若返回失败则现场用 CArrayObj 构造特征描述并 RTG.Create,任一步返回 false 都会 delete 临时对象并给出 INIT_FAILED,避免空网络跑实盘。 装载完成后,COpenCL *opcl = Agent.GetOpenCL() 把异构计算句柄同时注入 Planner、FutureEmbedding 和 RTG。这意味着你的显卡若不支持 OpenCL 或驱动异常,EA 初始化阶段就可能直接卡死,开 MT5 的「专家属性—依赖项」可核对 OpenCL 设备状态。 紧接着 RTG.getResults(Result) 拉出输出维度,并硬性比对 Result.Total() 是否等于 NRewards。若不一致,PrintFormat 会打出 'The scope of the RTG does not match the rewards count (%d <> %d)' 且返回 INIT_FAILED——这是典型的维度错配防护,改 NRewards 参数前必须先确认 RTG.nnw 的输出节点数。 推演阶段,Agent.feedForward(GetPointer(State), 1, false, Result) 做策略前向,RTG.feedForward 则以 Account 数组为输入做 Return-To-Go 预测;任一处 feedForward 返回 false 就置 StopFlag 并 break。训练侧用 CAGrad(Actions - result) + result 构造梯度目标,再 Agent.backProp 联动 FutureEmbedding 反传,外汇与贵金属杠杆高,这类自研 RL 模块过拟合后实盘回撤可能超预期,建议先在策略测试器跑 3 个月 tick 级回测再上真仓。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(!RTG.Load(FileName + "RTG.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *rtg = new CArrayObj();
      if(!CreateValueDescriptions(rtg))
        {
         class="kw">delete rtg;
         class="kw">return INIT_FAILED;
        }
      if(!RTG.Create(rtg))
        {
         class="kw">delete rtg;
         class="kw">return INIT_FAILED;
        }
      class="kw">delete rtg;
class=class="str">"cmt">//---
     }
class=class="str">"cmt">//---
   COpenCL *opcl = Agent.GetOpenCL();
   Planner.SetOpenCL(opcl);
   FutureEmbedding.SetOpenCL(opcl);
   RTG.SetOpenCL(opcl);
class=class="str">"cmt">//---
   RTG.getResults(Result);
   if(Result.Total() != NRewards)
     {
      PrintFormat("The scope of the RTG does not match the rewards count(%d <> %d)", NRewards, Result.Total());
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }

      class=class="str">"cmt">//--- Policy Feed Forward
      if(!Agent.feedForward(GetPointer(State), class="num">1, false, Result))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         StopFlag = true;
         break;
        }
      class=class="str">"cmt">//--- Return-To-Go
      Account.AssignArray(Buffer[tr].States[state + class="num">1].account);
      if(!RTG.feedForward(GetPointer(Account), class="num">1, false, Result))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         StopFlag = true;
         break;
        }

      class=class="str">"cmt">//--- Policy study
      Actions.Assign(Buffer[tr].States[state].action);
      vector<class="type">class="kw">float> result;
      Agent.getResults(result);
      Result.AssignArray(CAGrad(Actions - result) + result);
      if(!Agent.backProp(Result, GetPointer(FutureEmbedding)) ||
         !FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
        {

回放梯度与指标缓冲的初始化落点

强化学习回放阶段,代码先把下一状态的奖励序列读进 target 向量,再减去当前向后偏移 ValueBars 根 K 线的折扣奖励,折扣因子由 MathPow(DiscFactor, ValueBars) 算出。若 RTG 或 FutureEmbedding 的反向传播任意一步返回 false,立即打印函数名与行号、置 StopFlag 并 break,训练循环就此中断。 初始化函数 OnInit 里依次创建 RSI、CCI、ATR、MACD 四个指标句柄,周期统一取 TimeFrame 参数;任一 Create 失败直接返回 INIT_FAILED。四个指标缓冲都被 Resize 到 NBarInPattern 长度,若 Resize 不全成功也会在终端打印 __FUNCTION__ 与 __LINE__ 后失败退出。 Agent 与 Planner 两个神经网络模型分别从 FileName+"Act.nnw" 与 "Pln.nnw" 载入,载入失败会输出 "Can't load pretrained model"。载入后若 Actor 输出维度 Result.Total() 不等于 NActions,终端会报 "The scope of the Actor does not match the actions count",此时动作空间与网络输出不对齐,EA 无法继续。开 MT5 把 NActions 与 NBarInPattern 调成一致再编译,是验证这套逻辑的最快路径。外汇与贵金属品种波动剧烈,此类模型 EA 实盘前务必在策略测试器跑足历史样本,亏损概率不低。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
StopFlag = true;
break;
		 }
		 class=class="str">"cmt">//--- Return To Go study
		 vector<class="type">class="kw">float> target;
		 target.Assign(Buffer[tr].States[state + class="num">1].rewards);
		 result.Assign(Buffer[tr].States[state + ValueBars].rewards);
		 target = target - result * MathPow(DiscFactor, ValueBars);
		 Result.AssignArray(target);
		 if(!RTG.backProp(Result, GetPointer(FutureEmbedding)) ||
			!FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
			 {
			 PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
			 StopFlag = true;
			 break;
			 }
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(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">class="kw">float temp;
   if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Planner.Load(FileName + "Pln.nnw", temp, temp, temp, dtStudied, true))
     {
      PrintFormat("Can&class="macro">#x27;t load pretrained model");
      class="kw">return INIT_FAILED;
     }
   Planner.SetOpenCL(Agent.GetOpenCL());
   Agent.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the Actor does not match the actions count(%d <> %d)", NActions, Result.Total());

◍ Actor 维度校验与每根 K 线的状态拼装

初始化阶段先让 Agent 取第 0 层输出做维度核对:若 Result.Total() 不等于 BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions,直接 return INIT_FAILED。这一步能拦掉绝大多数因神经网络输入层定义和状态描述不一致导致的静默崩溃,开 MT5 跑 EA 前建议先打印一次该值确认。 OnTick 里用 IsNewBar() 过滤,只在新柱拉数据;CopyRates 取 NBarInPattern 根历史柱,并 ArraySetAsSeries(Rates, true) 把数组倒序,使 Rates[0] 对应最新一根。随后 RSI/CCI/ATR/MACD 与 Symb 全部 Refresh,避免用到上一 tick 的缓存。 状态向量按每根柱 9 个字段铺:close-open、high-open、low-open、tick_volume/1000、rsi、cci、atr、macd、signal,循环里遇任一指标为 EMPTY_VALUE 就 continue 跳过该柱。以 NBarInPattern=30 为例,仅历史部分就占 270 个 float,维度错一处 Planner.feedForward 就会喂崩。 账户侧只先写 account[0] = AccountInfoDouble(ACCOUNT_BALANCE),余额作为后续奖励函数的基准输入;外汇与贵金属杠杆高,状态里含账户净值时回测需警惕过拟合。

MQL5 / C++
  class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   Agent.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions))
     {
       PrintFormat("Input size of Actor doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(),
(BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions));
       class="kw">return INIT_FAILED;
     }
   Agent.Clear();
   AgentResult = vector<class="type">class="kw">float>::Zeros(NActions);
   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), 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">class="kw">float atr = class="num">0;
   for(class="type">int b = class="num">0; b < NBarInPattern; b++)
     {
       class="type">class="kw">float open = (class="type">class="kw">float)Rates[b].open;
       class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(b);
       class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(b);
       atr = (class="type">class="kw">float)ATR.Main(b);
       class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(b);
       class="type">class="kw">float sign = (class="type">class="kw">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">class="kw">float)(Rates[b].close - open);
       sState.state[shift + class="num">1] = (class="type">class="kw">float)(Rates[b].high - open);
       sState.state[shift + class="num">2] = (class="type">class="kw">float)(Rates[b].low - open);
       sState.state[shift + class="num">3] = (class="type">class="kw">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);
   if(!Planner.feedForward(GetPointer(bState), class="num">1, false))
      class="kw">return;
class=class="str">"cmt">//--- Account description
   sState.account[class="num">0] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_BALANCE);

「把账户状态压成特征向量」

这段逻辑干的事很直接:把当前账户的权益、多空持仓量、多空浮盈,以及一笔带时间衰减的持仓折扣,全部塞进一个定长数组 sState.account,再按比例折算成模型能吃的浮点特征 bState。外汇和贵金属杠杆高,权益回撤和持仓折扣的微小变动都可能在分钟级放大,跑之前先在策略测试器里确认 PrevBalance / PrevEquity 不是零值。

position_discount 那行是重点:profit 减去「持仓秒数 × (1/36000) ×profit」。也就是说持仓越久,这笔利润的折算权重越低,倾向把长期漂着的单子视为拖累。multiplyer = 1.0/(60*60*10) 即 2.78e-5,开 MT5 把这组常数改大改小,能直接观察特征对训练样本时间敏感度的变化。

时间标签用了三组三角函数:以年、月、周周期为基频取 sin/cos,把绝对时间映射成循环坐标,避免模型把 2024.01.01 当成比 2023.01.01「大」的孤立数。D'2024.01.01'-D'2023.01.01' 在 MQL5 里是 365 天的秒数,换成别的时间窗要同步改分母。 别把正态当圣经 这些特征没做标准化,buy_value 和权益占比量纲差几个数量级。直接喂给浅层网络可能让梯度被大值主导,建议在 bState.Add 之前先过一道自己的 min-max 或 z-score。

MQL5 / C++
  sState.account[class="num">1] = (class="type">class="kw">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">class="kw">float)buy_value;
  sState.account[class="num">3] = (class="type">class="kw">float)sell_value;
  sState.account[class="num">4] = (class="type">class="kw">float)buy_profit;
  sState.account[class="num">5] = (class="type">class="kw">float)sell_profit;
  sState.account[class="num">6] = (class="type">class="kw">float)position_discount;
  sState.account[class="num">7] = (class="type">class="kw">float)Rates[class="num">0].time;
class=class="str">"cmt">//---
  bState.Add((class="type">class="kw">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance));
  bState.Add((class="type">class="kw">float)(sState.account[class="num">1] / PrevBalance));
  bState.Add((class="type">class="kw">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">class="kw">float)(sState.account[class="num">4] / PrevBalance));
  bState.Add((class="type">class="kw">float)(sState.account[class="num">5] / PrevBalance));
  bState.Add((class="type">class="kw">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">class="kw">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">class="kw">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">class="kw">float)MathSin(class="num">2.0 * M_PI * x));

用神经网络输出接管多空手数

这段逻辑把日内时间戳折算成正弦相位送进状态向量,再让 Agent 前向推理出下一步的多空配置。x 用当前柱 time 除以 D1 周期秒数,MathSin(2πx) 把时间 cyclical 化,避免模型把‘周一’和‘周五’当成线性距离。 前向跑完之后,temp 向量前 4 位分别是 buy_lot、buy_tp、buy_sl、sell_lot 的隐含量。代码里先对 temp[0] 和 temp[3] 做互斥削减:谁大减谁小,另一个置零,保证同一时刻不会多空两边都给正手数。 delta 用 AgentResult 和 temp 的绝对差求和来量化‘策略意图漂移’,这个值越大说明本根 K 线 Agent 改主意越狠。外汇与贵金属杠杆高,这种漂移若直接成交可能频繁换手,实盘前应在 MT5 策略测试器打印 delta 分布。 买侧控制里,temp[0] 小于 min_lot、或 TP/SL 折算后点数不超过 stops(StopsLevel 与 Point 的乘积,至少 1 点)就平多;否则按 step_lot 圆整手数,用 Ask±MaxTP/MaxSL*Point 算挂价。卖侧对称处理,Bid 减 TP 出 sell_tp。

MQL5 / C++
  x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  bState.Add((class="type">class="kw">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
  Planner.getResults(Result);
class=class="str">"cmt">//---
  if(!Agent.feedForward(GetPointer(bState), class="num">1, false, Result))
     class="kw">return;
class=class="str">"cmt">//---
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
  vector<class="type">class="kw">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
    {
     temp[class="num">3] -= temp[class="num">0];
     temp[class="num">0] = class="num">0;
    }
  class="type">class="kw">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());

◍ 空单止损与奖励状态的收口逻辑

这段代码片段处理空单的尾随止损与部分平仓,并把当前柱状态写入强化学习样本。先按 Bid 加 temp[5]*MaxSL*Point 归一化出空单止损价,若 sell_value 大于 0 就调用 TrailPosition 推损;仓位不等时,多出来的部分用 ClosePartial 平掉,不足则按差价 Trade.Sell 补仓。 奖励数组的填法很直接:shift 取 BarDescr*(NBarInPattern-1),rewards[0] 存 bState[shift],rewards[1] 取下一偏移减 1.0f。当多空仓位合计为 0,rewards[2] 扣掉 atr/PrevBalance 作为空仓成本;否则置 0。 样本提交靠 Base.Add(sState),失败就 ExpertRemove 结束专家。注意外汇与贵金属杠杆高,atr 占比微小也可能在极端波动下放大回撤,MT5 里把 MaxSL 和 NBarInPattern 调小可直观看样本节奏变化。

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

「EURUSD H1 上的离线训练与八月回测」

方法实现完后,先用 2023 年前 7 个月的 EURUSD H1 历史做训练,再拿 2023 年 8 月同品种同周期数据测性能。训练集直接复用上一篇文章里做好的文件,我拷了一份改名 "PDT.bd",然后挂上预训练 EA。 所有训练 EA 都跑在在线图表上,但学习过程完全离线,不触发任何真实或模拟下单。预训练很吃资源,我的机器连续跑了一天多,这阶段只能等。 作者提到的在线学习思路,我在策略测试器里用训练/测试交替的短周期下游训练来逼近,不过一开始还是拿旧训练集把模型“预热”起来。后续优调连续做了几十轮下游训练加测试迭代,同样耗时。 回测结果不算漂亮:8 月新数据上模型共交易 18 笔,胜率仅 39%。最大盈利 11.26,接近最大亏损 4.76 的 3 倍;平均盈利 5.15 对平均亏损 3.19,盈利系数 1.03。两个历史段里平均获胜交易规模都压过最大亏损,但盈利占比都低于 40%,把优势抵消了。 模型能把经验迁移到没见过的数据,这点由训练集和后续区间测试结果可比性佐证。外汇和贵金属这类高波动品种,离线训出来的轻量模型容易过拟合,真要上 MT5 验证建议先自己跑一遍 8 月片段看曲线。 方法有明显潜力,但想抬升胜率得接着拆结果、调超参,且训练成本不低,不适合追求快速迭代的人。

PDT 落地的算力与数据现实

PDT 把未来状态知识和对应回报绑在一起做无监督预训练,下游微调时直接挑预测奖励最大的机会,决策倾向更优。但它比 DT、ODT 更吃资源:同样的数据集下,预训练耗时和算力开销明显更高,小账户跑本地 MT5 接 Python 训练大概率卡在显存和时长上。 方法作者实测过,行为多样性和策略一致性之间的权衡最优值随数据集变,没有统一参数;我们自己的复跑也确认这点——想训出能用的 agent,未标记数据虽好收集,但清洗和训练的资源门槛仍在。外汇和贵金属波动高、滑点狠,这类模型信号只作概率参考,实盘前务必小资金验证。 后续若想改未来状态编码,可接第 58 篇 DT 基础和第 62 篇层次化 DT 的思路做对照实验,别闷头调 PDT 原结构。

◍ 记住这一条就够了

这套 LSTM 优化方案在工程层拆成了七个文件:四个 EA 负责样本收集、预训练、优调和测试,三个类库管状态定义、网络构建和 OpenCL 加速。实际落地时你只要把 MQL5.zip(476 KB)解压进 MT5 的 MQL5 目录,先跑 Faza1.mq5 攒够轨迹数据,再依次走 Pretrain 和 FineTune,中间别跳步。 外汇与贵金属市场高杠杆、高波动,任何模型输出都只是概率倾向,不能直接当下单依据;用 Test.mq5 回测时重点看不同品种上的过拟合程度,而不是某一次曲线多漂亮。 真要省事,就让小布盯盘替你把 Trajectory 里的状态字段和 NeuroNet 的层宽对齐一遍,很多跑不通的报错其实就卡在结构声明和内核参数没对上。

常见问题

在初始化里做维度与权重校验,打印特征数和层数;若与预设不一致立即返回 INIT_FAILED,避免后续用空模型跑单。
在 OnInit 里按历史深度预填指标缓冲,回放梯度落到每根收盘 K 线,保证训练样本与实盘状态对齐。
可以。小布能读取你的特征拼装与多空手数输出,标记维度不匹配或止损收口异常,省去手动逐行核对。
未归一化的权益曲线和杠杆倍数最易带偏;建议做 z-score 或区间缩放,再拼进状态向量。
每根 K 线收口能保留过程奖励,避免仅看终点导致 PDT 预训练漏掉中间风险信号。