神经网络变得简单(第 65 部分):距离加权监督学习(DWSL)·进阶篇
📘

神经网络变得简单(第 65 部分):距离加权监督学习(DWSL)·进阶篇

第 2/3 篇

◍ Critic 与卷积网络的层定义拆解

在 MT5 里搭强化学习交易智能体,critic 网络的第一层用 defNeuronBaseOCL 类型,神经元数直接取 LatentCount,激活函数设 None、优化器走 ADAM,这一步只是把潜在向量原样接进网络。 紧接着的 layer 1 换成 defNeuronConcatenate,window 绑 prev_count(也就是 LatentCount)、step 绑 NActions,相当于把动作空间按步长拼到潜在向量后面;后面三层都是 defNeuronBaseOCL 配 LReLU,直到 layer 4 把输出数压到 NRewards 且激活回 None,给出原始奖励打分。 卷积侧先清掉旧描述,输入层神经元数算作 (HistoryBars * BarDescr) + AccountDescr,把K线描述和账户状态一并喂入;layer 1 只留 HistoryBars * BarDescr 个基元神经元不做激活,layer 2 用 defNeuronSoftMaxOCL、count 为 HistoryBars、step 为 BarDescr,对每根K线做软最大化权重。 照这段代码在 EA 的 CNet 派生类里复刻,把 LatentCount、NActions、NRewards、HistoryBars、BarDescr、AccountDescr 这几个宏先定好,编译后能在观测面板看到各层维度是否对得上。外汇与贵金属杠杆高,这类网络只是概率意义上的策略近似,实盘前务必用历史数据校验。

MQL5 / C++
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = LatentCount;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = prev_count;
  descr.step = NActions;
  descr.optimization = ADAM;
  descr.activation = LReLU;
  if(!critic.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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronBaseOCL;
  descr.count = NRewards;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!critic.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Convolution
  convolution.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = (HistoryBars * BarDescr) + AccountDescr;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!convolution.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 = defNeuronBaseOCL;
  descr.count = HistoryBars * BarDescr;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!convolution.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 = defNeuronSoftMaxOCL;
  descr.count = HistoryBars;
  descr.step = BarDescr;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!convolution.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }

「卷积堆叠与奖励分位的代码落地」

这段构建逻辑把第 3 到第 7 层直接堆成一条卷积链:第 3 层窗口取 BarDescr、步长相同,输出窗口砍半;第 4、5 层继续用上一层输出窗口 prev_wout 当窗和步长,每层再除 2,到第 5 层强制 window_out=2。 所有卷积层统一挂 LReLU 和 ADAM,任何一次 Add 失败就 delete 描述符并回 false,这种写法在 MT5 终端里若显 'Inconsistent embedding size' 说明 embedding 维度和状态矩阵列数对不上。 末层用 defNeuronSoftMaxOCL 把 prev_count*prev_wout 展平做概率输出,再接一个 defNeuronBaseOCL 全连接层压到 EmbeddingSize 维,返回 true 才算网络描述合法。 GetTargets 里 k = states * percentile / 100 直接算出分位样本数,比如 states=1000、percentile=70 时 k=700,后续可凭此切高奖励轨迹,外汇与贵金属行情下这类筛选只反映历史概率、实盘仍属高风险。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronConvOCL;
  prev_count = descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.step = BarDescr;
  prev_wout = descr.window_out = BarDescr / class="num">2;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!convolution.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;
  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.activation = LReLU;
  descr.optimization = ADAM;
  if(!convolution.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 = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = class="num">2;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!convolution.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 = defNeuronSoftMaxOCL;
  descr.count = prev_count * prev_wout;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!convolution.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 = defNeuronBaseOCL;
  descr.count = EmbeddingSize;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!convolution.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="kw">struct STarget
  {
   vector<class="type">float>      rewards;
   vector<class="type">float>      actions;
  };
STarget GetTargets(class="type">int percentile,
                   vector<class="type">float> &embedding,
                   matrix<class="type">float> &state_embedding,
                   matrix<class="type">float> &rewards,
                   matrix<class="type">float> &actions
)
  {
   STarget result;
   if(embedding.Size() != state_embedding.Cols())
     {
       PrintFormat("%s -> %d Inconsistent embedding size", __FUNCTION__, __LINE__);
       class="kw">return result;
     }
   class="type">ulong size = embedding.Size();
   class="type">ulong states = state_embedding.Rows();
   class="type">ulong k = class="type">ulong(states * percentile / class="num">100);

近邻回放里的距离权重与嵌入降维

这段逻辑干的是从经验池里挑 k 个近邻状态,再合成一套可用于策略更新的奖励与动作参考。先建一个 states×size 的零矩阵 temp,逐列算当前嵌入 embedding 与历史 state_embedding 的绝对差,用 temp.Max() 取出最大距离 alpha 做数值压扁,避免后面 exp 溢出。 距离向量 dist 走的是 log-sum-exp 技巧:MathLog(MathExp(temp/(-alpha)).Sum(1))*(-alpha),等效于减去最大值后的软最大值,数值稳定。dist.Percentile(percentile) 给出截断阈值 max,只有距离不超过该分位数的历史样本才进 k 近邻集合,k 不满就继续扫。 挑中的样本把 rewards、actions、state_embedding 按行搬进 k_rewards / k_actions / k_embedding,最后一行塞入当前 embedding。min_dist 取负后过 AF_SOFTMAX 得到 sf,result.rewards 就是 sf 对 k_rewards 的加权和——距离越近权重越大。 k_embedding 做 SVD 分解,奇异值和除以嵌入矩阵二范数与维度折中项,写进倒数第二个奖励位,当作潜状态复杂度惩罚;倒数第一位是 EntropyLatentState(Actor),逼着策略别过早塌缩。外汇与贵金属波动大,这套近邻合成在滑点扩开时可能失真,上 MT5 用 EURUSD 的 M15 跑一遍看 k=16、percentile=90 的回放分布再调。

MQL5 / C++
  matrix<class="type">float> temp = matrix<class="type">float>::Zeros(states, size);
  for(class="type">ulong i = class="num">0; i < size; i++)
      temp.Col(MathAbs(state_embedding.Col(i) - embedding[i]), i);
  class="type">float alpha=temp.Max();
  vector<class="type">float> dist = MathLog(MathExp(temp/(-alpha)).Sum(class="num">1))*(-alpha);
  vector<class="type">float> min_dist = vector<class="type">float>::Zeros(k);
  matrix<class="type">float> k_rewards = matrix<class="type">float>::Zeros(k, NRewards);
  matrix<class="type">float> k_actions = matrix<class="type">float>::Zeros(k, NActions);
  matrix<class="type">float> k_embedding = matrix<class="type">float>::Zeros(k + class="num">1, size);
  matrix<class="type">float> U, V;
  vector<class="type">float> S;
  class="type">float max = dist.Percentile(percentile);
  class="type">float min = dist.Min();
  for(class="type">ulong i = class="num">0, cur = class="num">0; (i < states && cur < k); i++)
     {
       if(max < dist[i])
         class="kw">continue;
       min_dist[cur] = dist[i];
       k_rewards.Row(rewards.Row(i), cur);
       k_actions.Row(actions.Row(i), cur);
       k_embedding.Row(state_embedding.Row(i), cur);
       cur++;
     }
  k_embedding.Row(embedding, k);
  vector<class="type">float> sf;
  (min_dist*(-class="num">1)).Activation(sf, AF_SOFTMAX);
  result.rewards = sf.MatMul(k_rewards);
  k_embedding.SVD(U, V, S);
  result.rewards[NRewards - class="num">2] = S.Sum() / (MathSqrt(MathPow(k_embedding, class="num">2.0f).Sum() * MathMax(k + class="num">1, size)));
  result.rewards[NRewards - class="num">1] = EntropyLatentState(Actor);
  vector<class="type">float> act_sf;
  alpha=MathAbs(k_rewards).Max();  
  dist = MathLog(MathExp(k_rewards/(-alpha)).Sum(class="num">1))*(-alpha);
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), HistoryBars, 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="type">float atr = class="num">0;
  for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; 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)

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

这段逻辑紧接 K 线特征之后,负责把账户 equity、balance 以及当前持仓的双向敞口写进同一套 state 数组,供后续模型或决策模块直接读。 先填账户头两个槽位:account[0] 取 ACCOUNT_BALANCE,account[1] 取 ACCOUNT_EQUITY,都是转 float 存。注意这里没做归一化,裸金额进数组,后面 bAccount 才做相对处理。

持仓统计那段用 PositionsTotal() 遍历,非当前品种直接 continue 跳过。buy_value / sell_value 分别累加多空手数,buy_profit / sell_profit 累加各自浮盈。position_discount 这行最值得盯:用 (current - 开仓时间) * multiplyer *profit做时间衰减惩罚,multiplyer = 1/(60*60*10) 即每持仓 1 秒约扣 1/36000 的盈利权重,老仓对状态的“折扣”更大。

最后 bAccount 里第一维是 (balance - PrevBalance)/PrevBalance 的相对变动,第二维是 equity/PrevBalance,第三维是 equity 相对 PrevEquity 的变化率——这三个数能直接反映本周期账户斜率,接 ML 或阈值判断都够用。外汇贵金属杠杆高,equity 回撤速度可能远快于 balance,跑这段代码前先确认 PrevBalance 在初始化时正确赋值。

MQL5 / C++
      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);
   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;
            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">//---
   bAccount.Clear();
   bAccount.Add((class="type">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance));
   bAccount.Add((class="type">float)(sState.account[class="num">1] / PrevBalance));
   bAccount.Add((class="type">float)((sState.account[class="num">1] - PrevEquity) / PrevEquity));
   bAccount.Add(sState.account[class="num">2]);
   bAccount.Add(sState.account[class="num">3]);

「把账户状态塞进网络再算下单手数」

这段逻辑干了两件事:先把账户环比与多个时间周期的正弦/余弦特征写进特征容器 bAccount,再丢给 Actor 网络前向推理,用返回向量 temp 控制买卖手数与止损止盈。 特征构造里用 Rates[0].time 除以 PeriodSeconds(PERIOD_MN1/W1/D1) 得到当前柱在月、周、日周期里的归一化位置,再乘 2*M_PI 做 Sin/Cos,等于把‘时间相位’编码成连续周期量,喂给模型捕捉季节节律。 推理后 temp[0] 与 temp[3] 分别代表净多空权重,代码用互斥减法归零一方,保证同根信号不双向开仓。随后用 Symb.LotsMin()、LotsStep()、StopsLevel() 把模型输出换算成合规手数和距价:若 temp[0] 小于最小手数,或 TP/SL 折算点数不超过 stops 级别,就触发 CloseByDirection 平多,否则按 MathRound 对齐步长开/跟 Trailing。 外汇与贵金属杠杆高,这类基于模型输出的自动下单若 MaxTP/MaxSL 或 stops 设错,可能在几根 K 线内触发频繁平仓,建议先在 MT5 策略测试器用 2023 全年数据跑一遍观察相位特征贡献。

MQL5 / C++
  bAccount.Add((class="type">float)(sState.account[class="num">4] / PrevBalance));
  bAccount.Add((class="type">float)(sState.account[class="num">5] / PrevBalance));
  bAccount.Add((class="type">float)(sState.account[class="num">6] / PrevBalance));
  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;);
  bAccount.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);
  bAccount.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);
  bAccount.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);
  bAccount.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x));
  if(bAccount.GetIndex() >= class="num">0)
      if(!bAccount.BufferWrite())
         class="kw">return;
class=class="str">"cmt">//---
  if(!Actor.feedForward(GetPointer(bState), class="num">1, false, GetPointer(bAccount)))
      class="kw">return;
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
class=class="str">"cmt">//---
  vector<class="type">float> temp;
  Actor.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=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 = NormalizeDouble(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point(), Symb.Digits());
       class="type">class="kw">double buy_sl = NormalizeDouble(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point(), Symb.Digits());
       if(buy_value > class="num">0)
          TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
       if(buy_value != buy_lot)
          {
            if(buy_value > buy_lot)

卖仓与回测帧的收尾处理

空仓条件判定之后,卖单分支和买单走的是同一套逻辑镜像。当 temp[3] 小于 min_lot,或止盈、止损距离不足 stops 时,若已有卖仓市值 sell_value 大于 0,直接调用 CloseByDirection(POSITION_TYPE_SELL) 清空,不再补单。 否则按 min_lot 加步长取整算出 sell_lot,并用 Bid 减 MaxTP 点差、加 MaxSL 点差归一化出 sell_tp / sell_sl。若 sell_value 与 sell_lot 不等,多出部分走 ClosePartial 平局部,不足部分用 Trade.Sell 补至目标仓位——外汇与贵金属杠杆高,这类动态调仓在滑点行情中可能触发连续小额成交,回测和实盘偏差需自己跑 MT5 比对。 奖励数组在仓量为 0 时按 atr / PrevBalance 扣减第 3 项,否则置 0;随后把 temp 拷进 sState.action,调用 Base.Add 写缓冲,失败即 ExpertRemove 结束策略。 OnTesterPass 里用 FrameNext 遍历优化帧,只认当前程序名且 id 大于 0 的帧;缓冲超 MaxReplayBuffer 时扫一遍找最小轨迹准备覆盖,这是把多遍回测轨迹喂给后续强化学习模块的底層动作。

MQL5 / C++
   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 = NormalizeDouble(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point(), Symb.Digits());
      class="type">class="kw">double sell_sl = NormalizeDouble(Symb.Bid() + temp[class="num">5] * MaxSL * Symb.Point(), Symb.Digits());
      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);
      }
   }
   sState.rewards[class="num">0] = bAccount[class="num">0];
   sState.rewards[class="num">1] = class="num">1.0f - bAccount[class="num">1];
   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] = temp[i];
   sState.rewards[class="num">3] = class="num">0;
   sState.rewards[class="num">4] = class="num">0;
   if(!Base.Add(sState))
      ExpertRemove();
}
class="type">void OnTesterPass()
  {
class=class="str">"cmt">//---
   class="type">ulong pass;
   class="type">class="kw">string name;
   class="type">long id;
   class="type">class="kw">double value;
   STrajectory array[];
   class="kw">while(FrameNext(pass, name, id, value, array))
     {
      class="type">int total = ArraySize(Buffer);
      if(name != MQLInfoString(MQL_PROGRAM_NAME))
         class="kw">continue;
      if(id <= class="num">0)
         class="kw">continue;
      if(total >= MaxReplayBuffer)
      {
       for(class="type">int a = class="num">0; a < id; a++)
         {
          class="type">float min = FLT_MAX;
          class="type">int min_tr = class="num">0;

常见问题

Critic 负责评估动作价值,卷积层负责从价格图像中提取局部形态特征;先卷积堆叠再做全连接接入 Critic,代码里用层定义逐层声明即可。
用嵌入降维把状态压到低维再算欧氏距离,权重取距离倒数或分位截断;回测时近邻数设 32~64 之间比较稳,太大易平滑掉信号。
小布可接入品种页把账户权益、持仓比和特征向量变化做 AIGC 诊断,异常时直接标红提醒,省去自己盯多屏。
在网络输出动作后接一个资金占比映射层,用当前权益和风控系数算手数;卖仓与回测帧收尾时再按滑点校正一次。
层数增加放大了近邻回放的矩阵运算,卖仓逻辑又在每帧重算距离权重;可把回放缓存设上限并降低嵌入维数来提速。