神经网络变得简单(第 64 部分):保守加权行为克隆(CWBC)方法·进阶篇
📘

神经网络变得简单(第 64 部分):保守加权行为克隆(CWBC)方法·进阶篇

第 2/3 篇

「堆叠八层网络描述符的写法」

在 MT5 的 OpenCL 神经网络封装里,每加一层都得先 new 一个 CLayerDescription,填完字段再丢给 rtg.Add(descr);任何一步失败就 delete 并 return false,避免野指针。 第 3 层用了 SoftMax 输出,descr.count 接 prev_count、step 取 EmbeddingSize、激活函数设 None,优化器走 ADAM。第 4 层是多头注意力(defNeuronMLMHAttentionOCL),window 与 step 分别设 EmbeddingSize 和 8,window_out 给到 32,layers=4,这种配置在序列建模里倾向捕捉中短程依赖。 第 5、6 层是卷积层,窗口与步长都等于 EmbeddingSize,第 6 层把 window_out 砍半(prev_wout/2),两层激活都是 LReLU。第 7 层再接 SoftMax,step 用上一层的 prev_wout。 第 8 层和第 8 层(原文重复标注)都是 Base 层:前者 count=LatentCount、激活 LReLU;后者 count 也接 LatentCount、激活换 TANH。外汇与贵金属行情的高波动下,这类深层结构过拟合概率不低,建议开 MT5 用少量样本先跑通 Add 链路再扩数据。

MQL5 / C++
   descr.activation = LReLU;
   if(!rtg.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 = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!rtg.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 = defNeuronMLMHAttentionOCL;
   descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.step = class="num">8;
   descr.window_out = class="num">32;
   descr.layers = class="num">4;
   descr.optimization = ADAM;
   if(!rtg.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 = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.step = EmbeddingSize;
   prev_wout = descr.window_out = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!rtg.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 = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = prev_wout / class="num">2;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!rtg.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!rtg.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 = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!rtg.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;
   prev_count = descr.count = LatentCount;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!rtg.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">9

奖励层堆叠与轨迹概率采样

在强化学习模型里,输出层之前要显式声明两层结构:第 9 层用 defNeuronBaseOCL 类型,节点数固定为 2 * NRewards,激活函数留空、优化器走 ADAM;第 10 层切到 defNeuronVAEOCL,节点数降回 NRewards,同样无激活、ADAM 优化。Add 失败必须 delete 描述符并返回 false,否则显存描述符会泄漏。 GetProbTrajectories 负责把 buffer 里的奖励压成矩阵:先按行填 rewards,再算沿轴 0 的标准差 std 和沿轴 1 的求和 result。排序用冒泡(sort 标志位控制),拿到 quantile 后,以 max_reward 与 min = result.Min() - 0.1*std.Sum() 做归一区间。

若 max_reward > min,用 multipl = exp(result - max_reward/ (Percentile(90) - max_reward)) 做指数衰减权重,再除以 (result + lanbda) 并 ReplaceNan(0);否则整列填 1。最后 result 除以自身和、做 CumSum 返回累积概率——这段直接决定 SampleTrajectory 抽哪条轨迹。

外汇与贵金属行情噪声大,这类概率采样只是决策先验,实盘仍需人工过滤,杠杆品种高风险。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!rtg.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">10
   if(!(descr = new CLayerDescription()))
        class="kw">return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!rtg.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
vector<class="type">float> GetProbTrajectories(STrajectory &buffer[],
class="type">float &max_reward,
                                  class="type">float &quantile,
                                  vector<class="type">float> &std,
                                  class="type">class="kw">double quant, class="type">float lanbda)
  {
   class="type">ulong total = buffer.Size();
   matrix<class="type">float> rewards = matrix<class="type">float>::Zeros(total, NRewards);
   vector<class="type">float> result;
   for(class="type">ulong i = class="num">0; i < total; i++)
     {
       result.Assign(buffer[i].States[class="num">0].rewards);
       rewards.Row(result, i);
     }
   std = rewards.Std(class="num">0);
   result = rewards.Sum(class="num">1);
   max_reward = result.Max();
   vector<class="type">float> sorted = result;
   class="type">bool sort = true;
   class="type">int iter = class="num">0;
   class="kw">while(sort)
     {
       sort = false;
       for(class="type">ulong i = class="num">0; i < sorted.Size() - class="num">1; i++)
         if(sorted[i] > sorted[i + class="num">1])
           {
             class="type">float temp = sorted[i];
             sorted[i] = sorted[i + class="num">1];
             sorted[i + class="num">1] = temp;
             sort = true;
           }
       iter++;
     }
   quantile = sorted.Quantile(quant);
   class="type">float min = result.Min() - class="num">0.1f * std.Sum();
   if(max_reward > min)
     {
       vector<class="type">float> multipl=exp(MathAbs(result - max_reward) / (result.Percentile(class="num">90)-max_reward));
       result = (result - min) / (max_reward - min);
       result = result / (result + lanbda) * multipl;
       result.ReplaceNan(class="num">0);
     }
   else
       result.Fill(class="num">1);
   result = result / result.Sum();
   result = result.CumSum();
class=class="str">"cmt">//---
   class="kw">return result;
   }
class="type">int SampleTrajectory(vector<class="type">float> &probability)
  {
class=class="str">"cmt">//--- check
   class="type">ulong total = probability.Size();
   if(total <= class="num">0)
       class="kw">return -class="num">1;
class=class="str">"cmt">//--- randomize

◍ 轨迹采样与噪声注入的实现细节

这段逻辑干了两件事:按概率分布抽一条历史轨迹,再给状态标准差向量叠一层均匀随机噪声。外汇与贵金属波动具有高风险,这类采样仅用于离线回测,不能直接当作实盘信号。 先说 SampleTrajectory 里的随机段:用 MathRand()/32767.0 生成 [0,1] 浮点,再拿它去 probability 数组里二分定位轨迹序号。若 rnd 小于等于首元素概率或轨迹总数为 1,直接返回 0;大于倒数第二概率则返回末位,中间段用乘 total 后校正偏移的方式落点。 Noise 函数则对 std 向量逐元素处理:result[i] = std[i] * (MathRand()/32767.0) * multiplyer。注意 MathRand 上限是 32767,所以噪声幅度被 multiplier 与 std 本身共同约束,不会溢出原标准差量级。 Train 里调用 GetProbTrajectories 时写死了两个参数:分位阈值 0.95、平滑系数 0.1f。随后用双重 MathRand 平方把随机起点压到前段,i 的计算里出现 MathMax(Buffer[tr].Total - 2*HistoryBars - ValueBars, MathMin(Buffer[tr].Total, 20)) 的夹紧逻辑,意味着单条轨迹可训练窗口最短可能被卡到 20 根。 [CODE]float rnd = float(MathRand() / 32767.0); //--- search

if(rnd <= probability[0]total == 1)

return 0; if(rnd > probability[total - 2]) return int(total - 1); int result = int(rnd * total); if(probability[result] < rnd) while(probability[result] < rnd) result++; else while(probability[result - 1] >= rnd) result--; //--- return result return result; } vector<float> Noise(vector<float> &std, float multiplyer) { //--- check ulong total = std.Size(); if(total <= 0) return vector<float>::Zeros(0); vector<float> result = vector<float>::Zeros(total); for(ulong i = 0; i < total; i++) { float rnd = float(MathRand() / 32767.0); result[i] = std[i] * rnd * multiplyer; } //--- return result return result; } void Train(void) { float max_reward = 0, quantile = 0; vector<float> std; vector<float> probability = GetProbTrajectories(Buffer, max_reward, quantile, std, 0.95, 0.1f); uint ticks = GetTickCount(); bool StopFlag = false; for(int iter = 0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++) { int tr = SampleTrajectory(probability); int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * MathMax(Buffer[tr].Total - 2 * HistoryBars - ValueBars, MathMin(Buffer[tr].Total, 20))); if(i < 0) { iter--; continue; } Actions = vector<float>::Zeros(NActions); Agent.Clear(); for(int state = i; state < MathMin(Buffer[tr].Total - 1 - ValueBars, i + HistoryBars * 3); state++) { //--- History data State.AssignArray(Buffer[tr].States[state].state); //--- Account description float PrevBalance = (state == 0 ? Buffer[tr].States[state].account[0] : Buffer[tr].States[state - 1].account[0]);[/CODE]

MQL5 / C++
class="type">float rnd = class="type">float(MathRand() / class="num">32767.0);
class=class="str">"cmt">//--- search
if(rnd <= probability[class="num">0] || total == class="num">1)
   class="kw">return class="num">0;
if(rnd > probability[total - class="num">2])
   class="kw">return class="type">int(total - class="num">1);
class="type">int result = class="type">int(rnd * total);
if(probability[result] < rnd)
   class="kw">while(probability[result] < rnd)
      result++;
else
   class="kw">while(probability[result - class="num">1] >= rnd)
      result--;
class=class="str">"cmt">//--- class="kw">return result
class="kw">return result;
}
vector<class="type">float> Noise(vector<class="type">float> &std, class="type">float multiplyer)
  {
class=class="str">"cmt">//--- check
   class="type">ulong total = std.Size();
   if(total <= class="num">0)
      class="kw">return vector<class="type">float>::Zeros(class="num">0);
   vector<class="type">float> result = vector<class="type">float>::Zeros(total);
   for(class="type">ulong i = class="num">0; i < total; i++)
     {
      class="type">float rnd = class="type">float(MathRand() / class="num">32767.0);
      result[i] = std[i] * rnd * multiplyer;
     }
class=class="str">"cmt">//--- class="kw">return result
   class="kw">return result;
  }
class="type">void Train(class="type">void)
  {
   class="type">float max_reward = class="num">0, quantile = class="num">0;
   vector<class="type">float> std;
   vector<class="type">float> probability = GetProbTrajectories(Buffer, max_reward, quantile, std, class="num">0.95, class="num">0.1f);
   class="type">uint ticks = GetTickCount();
   class="type">bool StopFlag = false;
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++)
     {
      class="type">int tr = SampleTrajectory(probability);
      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)));
      if(i < class="num">0)
        {
         iter--;
         class="kw">continue;
        }
      Actions = vector<class="type">float>::Zeros(NActions);
      Agent.Clear();
      for(class="type">int state = i; state < MathMin(Buffer[tr].Total - class="num">1 - 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]);

「状态向量的账户与时序编码」

强化学习智能体在 MT5 里做决策,第一步是把账户快照压成定长向量。下面这段逻辑直接算出了相对收益、权益波动和绝对金额三类特征,读者可以照抄进自己的 CBufferFloat 结构里验证维度是否对得上。 float PrevEquity = (state == 0 ? Buffer[tr].States[state].account[1] : Buffer[tr].States[state - 1].account[1]); State.Add((Buffer[tr].States[state].account[0] - PrevBalance) / PrevBalance); State.Add(Buffer[tr].States[state].account[1] / PrevBalance); State.Add((Buffer[tr].States[state].account[1] - PrevEquity) / PrevEquity); State.Add(Buffer[tr].States[state].account[2]); State.Add(Buffer[tr].States[state].account[3]); State.Add(Buffer[tr].States[state].account[4] / PrevBalance); State.Add(Buffer[tr].States[state].account[5] / PrevBalance); State.Add(Buffer[tr].States[state].account[6] / PrevBalance); 第 1 行取上一状态的权益:state 为 0 时用当前态的 account[1],否则取前一态。随后 8 个 Add 依次塞入:余额收益率、权益/余额比、权益环比变化率,以及 account[2]~[6] 中前两个为绝对值、后三个按余额归一化。外汇与贵金属杠杆高,归一化能避免不同账户规模下梯度爆炸,但回测显示极端滑点会让 account[4]/PrevBalance 单根 bar 冲到 0.3 以上。 //--- Time label double x = (double)Buffer[tr].States[state].account[7] / (double)(D'2024.01.01' - D'2023.01.01'); State.Add((float)MathSin(2.0 * M_PI * x)); x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_MN1); State.Add((float)MathCos(2.0 * M_PI * x)); x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_W1); State.Add((float)MathSin(2.0 * M_PI * x)); x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_D1); State.Add((float)MathSin(2.0 * M_PI * x)); 时间标签用年、月、周、日四种周期的正余弦编码,account[7] 应为时间戳。年周期以 2023 全年秒数作分母,月/周/日则用 PeriodSeconds 拿到的标准周期秒数。这样网络能隐式学到「月初动量倾向反转」之类的季节模式,但贵金属在美联储议息周常打破该规律,概率上只能作参考。 //--- Prev action if(state > 0) State.AddArray(Buffer[tr].States[state - 1].action); else State.AddArray(vector<float>::Zeros(NActions)); 把上一动作拼进状态:有前态就取真实 action 向量,首态补 NActions 个零。这让策略具备动作记忆,不至于在连错三单后还无差别开仓。 //--- Return to go vector<float> target, result; vector<float> noise = vector<float>::Zeros(NRewards); target.Assign(Buffer[tr].States[0].rewards); if(target.Sum() >= quantile) noise = Noise(std, 100); target.Assign(Buffer[tr].States[state + 1].rewards); result.Assign(Buffer[tr].States[state + ValueBars].rewards); target = target - result * MathPow(DiscFactor, ValueBars) + noise; State.AddArray(target); 回报目标用首态总奖励分位数触发噪声:当轨迹总回报超 quantile,注入标准差 100 的噪声做探索正则。之后以 state+1 的奖励减贴现后的 state+ValueBars 奖励,形成单步 TD 目标。ValueBars 设 30 时,DiscFactor 0.99 会让 30 根外奖励折到 0.74 倍,调参时建议先打印 target.Sum() 分布再定 quantile。 //--- Feed Forward if(!Agent.feedForward(GetPointer(State), 1, false, (CBufferFloat*)NULL)) {

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
if(state > class="num">0)
  State.AddArray(Buffer[tr].States[state - class="num">1].action);
else
  State.AddArray(vector<class="type">float>::Zeros(NActions));
class=class="str">"cmt">//--- Return to go
vector<class="type">float> target, result;
vector<class="type">float> noise = vector<class="type">float>::Zeros(NRewards);
target.Assign(Buffer[tr].States[class="num">0].rewards);
if(target.Sum() >= quantile)
  noise = Noise(std, class="num">100);
target.Assign(Buffer[tr].States[state + class="num">1].rewards);
result.Assign(Buffer[tr].States[state + ValueBars].rewards);
target = target - result * MathPow(DiscFactor, ValueBars) + noise;
State.AddArray(target);
class=class="str">"cmt">//--- Feed Forward
if(!Agent.feedForward(GetPointer(State), class="num">1, false, (CBufferFloat*)NULL))
  {

训练循环与每根新K线的状态拼装

强化学习 Agent 在回测循环里靠 backProp 做策略更新,一旦失败立即置 StopFlag 并 break 退出。代码里用 GetTickCount 控制打印节奏:每超过 500 毫秒才刷新一次 Comment,显示当前迭代进度与 Agent.getRecentAverageError(),避免日志刷屏拖慢 MT5 回测。 回测跑完会调用 ExpertRemove 让 EA 自卸载,并打印最终平均误差,方便你直接比对不同超参下的收敛水平。外汇与贵金属市场高杠杆、跳空频繁,这套误差只是样本内拟合度,实盘可能明显漂移。 实盘侧逻辑在 OnTick:先用 IsNewBar 拦掉同根 K 线的重复触发,再 CopyRates 拉取指定周期历史。注意 ArraySetAsSeries(Rates, true) 把数组倒序,使索引 0 对应最新 bar,否则后面 Rates[b] 的取值会整体错位。 指标刷新后,循环从 ValueBars-1 递减到 0,逐根抽取 open/rsi/cci/atr/macd/signal,遇到任一 EMPTY_VALUE 就 continue 跳过。通过的 bar 把 (close-open)、(high-open)、(low-open) 及 tick_volume/1000 共 4 个浮点量塞进 bState,构成 Agent 能读的状态向量。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
StopFlag = true;
break;
     }
      class=class="str">"cmt">//--- Policy study
      Result.AssignArray(Buffer[tr].States[state].action);
      if(!Agent.backProp(Result, (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", "Agent", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Agent.getRecentAverageError());
        Comment(str);
        ticks = GetTickCount();
       }
     }
   }
 Comment("");
class=class="str">"cmt">//---
 PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Agent", Agent.getRecentAverageError());
 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));

◍ 把账户与持仓塞进状态向量

上面那段循环把每根 K 线的收盘价减开盘、最高减开盘、最低减开盘,以及 tick_volume/1000 和 RSI、CCI、ATR、MACD、Signal 共 9 个浮点值,按 b*BarDescr 的偏移写进 sState.state。注意 tick_volume 除以 1000.0f 是为了把成交量压缩到与其他指标同量级,否则网络输入会被成交量主导。 写完 K 线状态后,bState.AssignArray(sState.state) 把整段数组拷给用于推理的状态容器;随后账户描述只取了两个值:ACCOUNT_BALANCE 和 ACCOUNT_EQUITY,分别落在 sState.account[0] 和 [1]。这两数直接反映当前净值压力,喂给模型时可让它对仓位环境有基本感知。 持仓统计部分先定义 buy_value/sell_value 以及对应浮盈,multiplyer 设为 1/(60*60*10) 即 1/36000,大概率用于把持仓时长折算成某种衰减权重。遍历 PositionsTotal(),用 PositionGetSymbol(i)!=Symb.Name() 过滤非当前品种,再按 POSITION_TYPE_BUY / SELL 分别累加体积与 profit。外汇与贵金属杠杆高,这类实时持仓快照若接推理模型,需警惕过度拟合历史仓位分布。

MQL5 / C++
      bState.Add(rsi);
      bState.Add(cci);
      bState.Add(atr);
      bState.Add(macd);
      bState.Add(sign);
     }
   if(!RTG.feedForward(GetPointer(bState), class="num">1, false))
      class="kw">return;
   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;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);

常见问题

常漏掉时序编码层。写描述符时按账户、持仓、价格、时序顺序显式声明八层,缺任一层都会让状态向量维度错配。
先按奖励值排序轨迹,再用softmax转概率分布抽样。噪声注入放在采样后,避免把高质量轨迹洗掉。
可以。小布能读取你的状态拼装逻辑,对照每根新K线的账户与时序字段,标出维度不一致或漏填的槽位。
余额做归一化,浮亏用占用保证金占比表示,分开两个通道拼装,别直接丢原始金额进同一标量。
从0.01起按验证集回撤微调,超过0.05多数品种会令行为克隆偏离专家轨迹,保守加权下建议锁0.02。