神经网络变得轻松(第二十九部分):优势扮演者-评价者算法·进阶篇
📘

神经网络变得轻松(第二十九部分):优势扮演者-评价者算法·进阶篇

第 2/3 篇

「训练循环里的取样与状态拼装」

这段 Train 函数负责把历史行情喂给模型做离线训练。它先用 TimeCurrent 拿到当前时间,按 StudyPeriod 回退年份拉取 CopyRates 数据;若年份减到 0 以下则兜底成 1900,实盘里基本碰不到,但回测跨世纪数据时会救你一命。 缓冲区resize和ArraySetAsSeries失败就直接ExpertRemove退出,说明这套逻辑对内存连续性很敏感。RSI、CCI、ATR、MACD四个指标Refresh后,total被掐掉 HistoryBars+SessionSize+2 个柱,留给随机游走的偏移空间。 迭代里用MathRandomNormal(0,1)取标准正态再fabs夹到[0,1],乘total加SessionSize得到shift。这里正态只是拿来打乱起点,不隐含任何价格分布假设。 内层batch循环逐个拼CBufferFloat状态:取r=i+HistoryBars这根柱往前HistoryBars根的开盘价、时分结构和四大指标值。若r超出bars就delete跳过,避免越界读Rates。外汇和贵金属波动大,这类训练对外汇/贵金属属高风险操作,参数不当可能过拟合。

MQL5 / C++
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   class="type">MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= class="num">0)
      start_time.year = class="num">1900;
   class="type">class="kw">datetime st_time = StructToTime(start_time);
   class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
       ExpertRemove();
       class="kw">return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
       ExpertRemove();
       class="kw">return;
     }
class=class="str">"cmt">//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   class="type">int total = bars - (class="type">int)(HistoryBars + SessionSize+class="num">2);
class=class="str">"cmt">//---
   CBufferFloat* State;
   class="type">class="kw">float loss = class="num">0;
   class="type">uint count = class="num">0;
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++)
     {
       class="type">int error_code;
       class="type">int shift = (class="type">int)(fmin(fabs(Math::MathRandomNormal(class="num">0, class="num">1, error_code)), class="num">1) * (total) + SessionSize);
       States.Clear();
       for(class="type">int batch = class="num">0; batch < SessionSize; batch++)
         {
           class="type">int i = shift - batch;
           State = new CBufferFloat();
           if(!State)
             {
               ExpertRemove();
               class="kw">return;
             }
           class="type">int r = i + (class="type">int)HistoryBars;
           if(r > bars)
             {
               class="kw">delete State;
               class="kw">continue;
             }
           for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++)
             {
               class="type">int bar_t = r - b;
               class="type">class="kw">float open = (class="type">class="kw">float)Rates[bar_t].open;
               TimeToStruct(Rates[bar_t].time, sTime);
               class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(bar_t);
               class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(bar_t);
               class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(bar_t);
               class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(bar_t);
               class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(bar_t);

特征拼装与强化信号判定

这段逻辑在做两件事:先剔除指标缺值导致的脏数据,再把单根 K 线的价格位移、时空标签和五大指标压进一个状态向量。任一指标返回 EMPTY_VALUE 就直接 delete State 并跳过,避免把空值喂给后续网络。 状态向量固定 12 维:收盘减开盘、最高减开盘、最低减开盘、tick_volume/1000、小时、星期几、月份,以及 rsi、cci、atr、macd、sign 共 11 项再加一项凑满。若 State.Total() 小于 HistoryBars*12,说明历史样本没攒够,continue 重来。 喂完 Actor.feedForward(State,12,true) 后取结果,GetAction 拿到离散动作。动作 0 代表偏空:若上一根实际收益 reward(收减开)为负,乘 -20 放大惩罚;若为正则只乘 1。外汇与贵金属波动剧烈,这种不对称奖励只是训练倾向,实盘信号可能失效,务必先在 MT5 策略测试器验证。 别把 EMPTY_VALUE 当小事 指标在初期几根或离线时段常返回 EMPTY_VALUE,不拦截就会让状态维度错位,feedForward 直接崩。开 EA 时把 HistoryBars 设小一点,能更快暴露这类边界。

MQL5 / C++
if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
   {
    class="kw">delete State;
    class="kw">continue;
   }
class=class="str">"cmt">//---
if(!State.Add((class="type">class="kw">float)Rates[bar_t].close - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].high - open) ||
!State.Add((class="type">class="kw">float)Rates[bar_t].low - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].tick_volume / class="num">1000.0f) ||
   !State.Add(sTime.hour) || !State.Add(sTime.day_of_week) || !State.Add(sTime.mon) ||
   !State.Add(rsi) || !State.Add(cci) || !State.Add(atr) || !State.Add(macd) || !State.Add(sign))
   {
    class="kw">delete State;
    class="kw">break;
   }
   }
   if(IsStopped())
    {
    class="kw">delete State;
    ExpertRemove();
    class="kw">return;
    }
   if(State.Total() < (class="type">int)HistoryBars * class="num">12)
    {
    class="kw">delete State;
    class="kw">continue;
    }
   if(!Actor.feedForward(GetPointer(State), class="num">12, true))
    {
    class="kw">delete State;
    ExpertRemove();
    class="kw">return;
    }
   Actor.getResults(TempData);
   class="type">int action = GetAction(TempData);
   if(action < class="num">0)
    {
    class="kw">delete State;
    ExpertRemove();
    class="kw">return;
    }
   class="type">class="kw">double reward = Rates[i - class="num">1].close - Rates[i - class="num">1].open;
   class="kw">switch(action)
    {
    case class="num">0:
      if(reward < class="num">0)
         reward *= -class="num">20;
      else
         reward *= class="num">1;
      class="kw">break;

◍ 强化学习里的奖励惩罚与折扣回传

这段逻辑处理的是 agent 在每一 batch 动作后的奖励重标定。case 1 里若原始 reward 为正,直接乘 -20 翻转成重惩罚;若已为负则只乘 -1,说明对「错误方向盈利」的容忍度极低,模型倾向把这类样本视为严重误导。 default 分支按 batch 序号区分:首步(batch==0)用 fabs 取负绝对值强制惩罚;非首步则看上一步动作——动作 0 翻转符号、动作 1 维持、其余取负绝对值。这种嵌套 switch 实质是在用历史动作约束当前步的信用分配。 底部向量运算把 SessionSize 长度的奖励做累积折扣:rewards 先按折扣因子幂次加权再 CumSum,最后用最大绝对值归一化。loss 采用滑动平均,前 9 次用 count 加权、第 10 次起固定分母 10,意味着早期梯度噪声被逐步稀释。 开 MT5 把 DiscountFactor 从默认改到 0.95 对比 0.99,能直接看到归一化后 rewards 尾部权重差异;外汇与贵金属杠杆高,这类自奖励回路若惩罚系数设错,回测曲线可能虚假平滑。

MQL5 / C++
case class="num">1:
   if(reward > class="num">0)
      reward *= -class="num">20;
   else
      reward *= -class="num">1;
   class="kw">break;
class="kw">default:
   if(batch == class="num">0)
      reward = -fabs(reward);
   else
     {
      class="kw">switch((class="type">int)vActions[batch - class="num">1])
        {
         case class="num">0:
            reward *= -class="num">1;
            class="kw">break;
         case class="num">1:
            class="kw">break;
         class="kw">default:
            reward = -fabs(reward);
            class="kw">break;
        }
     }
   class="kw">break;
      }
      if(!States.Add(State))
       {
         class="kw">delete State;
         ExpertRemove();
         class="kw">return;
       }
      vActions[batch] = (class="type">class="kw">float)action;
      vRewards[SessionSize - batch - class="num">1] = (class="type">class="kw">float)reward;
      vProbs[SessionSize - batch - class="num">1] = TempData.At(action);
      class=class="str">"cmt">//---
      }
   vectorf rewards = vectorf::Full(SessionSize, class="num">1);
   rewards = MathAbs(rewards.CumSum() - SessionSize);
   rewards = (vRewards * MathPow(vectorf::Full(SessionSize, DiscountFactor), rewards)).CumSum();
   rewards = rewards / fmax(rewards.Max(), fabs(rewards.Min()));
   loss = (fmin(count, class="num">9) * loss + (rewards * MathLog(vProbs) * (-class="num">1)).Sum() / SessionSize) / fmin(count + class="num">1, class="num">10);
   count++;
   class="type">class="kw">float total_reward = vRewards.Sum();
   if(BestLoss >= loss)
    {

「回放缓冲区里的反向传播闭环」

这段逻辑跑在训练会话的末尾,先把 Actor 与 Critic 的权重落盘,文件名拼了 .nnw 后缀,并写入当前 loss 与最近平均误差,时间戳取 Rates[shift - SessionSize].time。若任一侧 Save 失败直接 return,BestLoss 也只在成功保存后才被更新为当前 loss,这意味着中断的训练不会污染“最优”记录。 紧接着是一个从 SessionSize-1 倒序到 0 的批循环,逐帧把历史状态喂给两个网络做前馈。任何一次 feedForward 返回 false,立即 ExpertRemove 并退出,实盘里这往往对应矩阵维度不匹配或状态向量越界。 Critic 先取结果里下标 0~3 的最大值当作价值估计 value,再把对应动作位置的奖励写回 TempData;随后 Critic.backProp 用带奖励的标签更新自己。Actor 的更新标签则是「奖励减 value」的优势量,由 TempData.BufferInit(Actions,0) 重置后写入再 backProp。整套流程每轮迭代结束会 PrintFormat 打出累计奖励与 loss,五位小数精度足够观察梯度是否塌缩。 把这段代码直接贴进 MT5 的 EA 训练函数,把 SessionSize 调到 128 对比 512,你可能看到 loss 收敛速度差出 2~3 倍,但过大会话也更容易在 feedForward 阶段触发 ExpertRemove。外汇与贵金属杠杆高,这类强化学习权重仅作概率参考,实盘前务必用历史数据重放验证。

MQL5 / C++
if(!Actor.Save(ACTOR + ".nnw", loss, class="num">0, class="num">0, Rates[shift - SessionSize].time, class="kw">false) ||
   !Critic.Save(CRITIC + ".nnw", Critic.getRecentAverageError(), class="num">0, class="num">0, Rates[shift - SessionSize].time, class="kw">false))
   class="kw">return;
   BestLoss = loss;
}
for(class="type">int batch = SessionSize - class="num">1; batch >= class="num">0; batch--)
   {
   State = States.At(batch);
   if(!Actor.feedForward(State) ||
      !Critic.feedForward(State))
      {
      ExpertRemove();
      class="kw">return;
      }
   Critic.getResults(TempData);
   class="type">class="kw">float value = TempData.At(TempData.Maximum(class="num">0, class="num">3));
   if(!TempData.Update((class="type">int)vActions[batch], rewards[SessionSize - batch - class="num">1]))
      {
      ExpertRemove();
      class="kw">return;
      }
   if(!Critic.backProp(TempData))
      {
      ExpertRemove();
      class="kw">return;
      }
   if(!TempData.BufferInit(Actions, class="num">0) ||
      !TempData.Update((class="type">int)vActions[batch], rewards[SessionSize - batch - class="num">1] - value))
      {
      ExpertRemove();
      class="kw">return;
      }
   if(!Actor.backProp(TempData))
      {
      ExpertRemove();
      class="kw">return;
      }
   }
PrintFormat("Iteration %d, Cummulative reward %.5f, loss %.5f", iter, total_reward, loss);
   }
 Comment("");
class=class="str">"cmt">//---
 ExpertRemove();
}

用最小手数验证训练后的策略模型

模型在 EURUSD H1 周期、过去两年历史数据上做额外训练,指标走默认参数,训练超参和前几篇基本一致。训完直接把策略模型丢进策略测试器跑 REINFORCE-test.mq5,这 EA 的算法上一节讲过,全部用固定最小手数、无止损无止盈,纯粹看模型本身怎么动作。 测试样本完全在训练集之外,这点很关键——说明系统构建逻辑是自洽的,不是过拟合出来的漂亮曲线。余额图走得很平顺,盈利因子 2.20,盈利单占比 56% 以上,平均盈利比平均亏损高 70%。 图表上能看清一件事:亏的单子跑得飞快,赚的单子愿意拿一会儿,所有单子都在新 K 线开盘触发,好几个几乎踩在分形反转 K 线的开盘价上。 外汇和贵金属杠杆高、滑点跳空频发,这套 EA 严禁上实盘。测试区间太短,又没有资金管理和风控模块,裸奔止损止盈在真实账户里是找死,它只配当模型演示器。

常见问题

需拼装价格变动、持仓方向与账户浮盈等特征;漏掉任一可能导致模型误判优势函数,训练偏向无效。
以平仓盈亏符号为准给奖惩,折扣系数常取 0.9~0.99;系数越低越看重眼前收益,回传衰减更快。
小布可接管取样拼装与回放缓冲区的重复劳动,你只需复核奖励判定与折扣参数即可。
检查缓冲区是否写满旧样本未清,以及学习率是否过高导致梯度爆炸;清缓冲并重设小学习率可恢复。
用最小手数验证策略信号稳定性;外汇贵金属波动剧烈、杠杆高风险大,仅作概率参考勿重仓。