神经网络变得简单(第 59 部分):控制二分法(DoC)·进阶篇
📘

神经网络变得简单(第 59 部分):控制二分法(DoC)·进阶篇

第 2/3 篇

◍ 卷积层之后的潜变量堆叠

在第四层卷积处理完价格窗口后,网络继续向上堆叠四层潜变量与一层分布式回报输出。第四层把窗口输出直接当步长,window 与 step 都等于前层 window_out,并固定输出 4 个特征,优化器统一用 ADAM、激活走 LReLU。 第五到七层都是 defNeuronBaseOCL 基础全连接,节点数由 LatentCount 控制;第六层把激活换成 TANH 且把 prev_count 同步成 LatentCount,形成编码瓶颈,其余层保持 LReLU。 第八层切到 defNeuronFQF 分位数回报头,count 取 NRewards、window_out 写死 32,这意味着模型在内部把未来奖励拆成 32 个分位来估计,而非单点期望。 OnTick 里只在 IsNewBar 为真时跑逻辑:用 CopyRates 拉 NBarInPattern 根 K 线,并把 Rates 设成时间序列;随后 RSI、CCI、ATR、MACD 与品种对象全部 Refresh,保证指标和报价是最新快照。 历史数据循环里逐根取出 open 与 RSI.Main(b)、CCI.Main(b) 转 float,说明特征拼接发生在 Bar 粒度而非 Tick 粒度,外汇与贵金属品种下这种重算频率仍可能因点差跳变产生过拟合,需自行在 MT5 回测验证。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   descr.window_out = class="num">4;
   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">5
   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">6
   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">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   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 = defNeuronFQF;
   descr.count = NRewards;
   descr.window_out = class="num">32;
   descr.optimization = ADAM;
   if(!rtg.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
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">//---
   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);

「把账户持仓压进状态数组」

这段逻辑紧接上一节的指标采样,把当前账户余额、净值和持仓多空分布一并写入状态结构,方便后续给模型或监控模块喂原始特征。 先取两个最基础的账户量:ACCOUNT_BALANCE 和 ACCOUNT_EQUITY,直接落进 sState.account[0] 与 [1]。这两个值实时反映资金面,外汇和贵金属杠杆高,净值回撤往往比余额更敏感。 接着遍历 PositionsTotal(),只筛本符号持仓。多单 volume 与 profit 分别累加进 buy_value / buy_profit,空单同理进 sell_value / sell_profit,最终写进 account[2]~[5]。 position_discount 的计算值得留意:它用持仓利润减去「持仓时长 × 1/(60*60*10) × 利润绝对值」。乘数是 1/36000,相当于把每毫秒的衰减摊薄得很小,倾向用于惩罚长期浮亏仓。开 MT5 把这段粘进 EA,改 multiplyer 分母看 discount 曲线怎么变,就能验证它对持仓时间的惩罚强度。

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

把账户状态压成神经网络输入向量

这段逻辑干的事,是把当前账户与行情的时间特征拼装成一个特征向量 bState,喂给两层网络(RTG 与 Agent)做推理。注意第 7 个账户字段存的是 Rates[0].time 的 float 值,也就是当前 K 线时间戳,后续时间标签全靠它换算。 时间编码用了 4 个不同周期的正弦/余弦:年用 2023→2024 的秒数做分母,月/周/日分别用 PERIOD_MN1、PERIOD_W1、PERIOD_D1 的 PeriodSeconds 做分母,乘上 2π 后取三角函数。这样网络能隐式学到季节与周期节律,外汇和贵金属受周期影响明显,但高频噪声也可能被放大,属高风险信号。 推理结束后,代码把 Agent 输出 temp 做多空互斥裁剪:若 temp[0](买量)≥ temp[3](卖量),卖量归零;反之买量归零。随后用 Symb.LotsMin()、LotsStep()、StopsLevel() 约束真实下单手数,买量不足 min_lot 或 TP/SL 到点差 stops 以内就平掉已有买仓。 开 MT5 把这段塞进 EA 的 OnTick,重点看 bState.AddArray(AgentResult) 那行——上一次网络动作会作为本次状态的一部分回灌,这是策略产生记忆的关键,改 PERIOD 分母能直接改变周期敏感度。

MQL5 / C++
  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);
  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
    {
     temp[class="num">3] -= temp[class="num">0];
     temp[class="num">0] = class="num">0;
    }
  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;

◍ 买卖双向仓位的挂单与平仓逻辑

这段控制块把买入和卖出两条线分开处理,核心是根据模型输出的 temp 数组计算手数、止盈止损,并决定加仓、减仓还是清仓。外汇与贵金属杠杆高,实盘跑之前务必在策略测试器里用历史数据验证这套仓位变更逻辑。 买入侧先用 Symb.NormalizePrice 把 Ask 加减 temp[1]/temp[2] 乘 MaxTP/MaxSL 再乘 Point 后的价格规范化,避免报价精度越界。若 buy_value 大于 0 就移动止盈止损;当模型目标手数 buy_lot 与当前持仓不等时,多则平部分、少则补单。 卖出侧先用 min_lot 与 stops 门槛过滤:若 temp[3] 小于最小手数,或止盈止损距离不足 stops,就直接按方向清仓。否则照买侧镜像算 sell_lot、sell_tp、sell_sl,并做移动与差额处理。 末尾把状态写回经验库:用 BarDescr*(NBarInPattern-1) 取偏移,空仓时按 atr/PrevBalance 记惩罚,否则奖励置 0;若 Base.Add 失败直接 ExpertRemove 退出。OnInit 里则先 LoadTotalBase,失败打错误码并返回 INIT_FAILED,模型权重随后以 float temp 载入。

MQL5 / C++
  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];
  if(!Base.Add(sState))
      ExpertRemove();
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ResetLastError();
   if(!LoadTotalBase())
    {
      PrintFormat("Error of load study data: %d", GetLastError());
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//--- load models
   class="type">float temp;

「模型初始化失败的几个硬卡点」

在 MT5 里跑强化学习类 EA,初始化阶段最容易卡在神经网络维度对不上。下面这段逻辑先尝试加载 Agent 和 RTG 两个网络,若文件不存在就现场建描述数组并 Create,任何一步失败直接 return INIT_FAILED,不会带病启动。 加载或创建完后,代码用 getResults 和 GetLayerOutput(0,...) 反复校验输出节点数:Agent 的动作输出必须等于 NActions,RTG 的奖励输出必须等于 NRewards;Agent 输入层维度需等于 NRewards + BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions,RTG 输入层则不含 NRewards 那一项。任何一项 Total() 不匹配就打印具体差值并失败退出。 RTG.SetUpdateTarget(1000000) 把目标网络更新步数设到百万级,随后用 EventChartCustom 发一个自定义事件 'Init' 触发图表端训练;若事件创建失败(GetLastError 非 0)同样 INIT_FAILED。外汇与贵金属市场高杠杆、滑点无常,这类 EA 若初始化维度算错,实盘可能直接不交易或乱下单,务必在策略测试器里先跑通再上真金。 OnDeinit 里只做了一件事:Agent.Save(FileName+"Act.nnw",0,0,0,TimeCurrent(),true) 把最新权重落盘。注意 RTG 权重没在这里存,若你改了训练频率要自查是否漏存,否则重启 EA 会重新初始化 RTG。

MQL5 / C++
  if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !RTG.Load(FileName + "RTG.nnw", dtStudied, true))
     {
       Print("Init new models");
       CArrayObj *agent = new CArrayObj();
       CArrayObj *rtg = new CArrayObj();
       if(!CreateDescriptions(agent,rtg))
         {
          class="kw">delete agent;
          class="kw">delete rtg;
          class="kw">return INIT_FAILED;
         }
       if(!Agent.Create(agent) ||
          !RTG.Create(rtg))
         {
          class="kw">delete agent;
          class="kw">delete rtg;
          class="kw">return INIT_FAILED;
         }
       class="kw">delete agent;
       class="kw">delete rtg;
     }
class=class="str">"cmt">//---
   Agent.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the agent does not match the actions count(%d <> %d)", NActions, Result.Total());
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//---
   Agent.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions))
     {
      PrintFormat("Input size of Agent doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(),
(NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions));
      class="kw">return INIT_FAILED;
     }
   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">//---
   RTG.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions))
     {
      PrintFormat("Input size of RTG doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(),
(BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions));
      class="kw">return INIT_FAILED;
     }
   RTG.SetUpdateTarget(class="num">1000000);
   if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      class="kw">return INIT_FAILED;
     }
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">//---
   Agent.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);

训练循环里的状态拼装细节

训练函数 Train 的核心是嵌套循环:外层按 Iterations 上限跑随机轨迹,内层把单条历史缓冲拆成连续状态喂给智能体。MathRand 两次相乘再除以 32767 平方,是为了把随机起点压到更靠前的区间,避免样本总被截断在行情尾部。 状态向量 State 的拼装值得盯一眼:除价格类特征外,还塞了余额变化率、权益相对余额比、浮动盈亏占比,以及用 2023→2024 全年秒数归一化的时间正弦项。这种时间编码让模型对跨年 seasonal 偏移有概率上的感知,但具体泛化能力要在 MT5 里跑不同年份缓冲才看得出。 内层循环上限用 MathMin(Buffer[tr].Total - 2, i + HistoryBars * 3),意味着单幕最长只取历史窗的 3 倍步数。若 i 算出来为负就回退一次 iter 重抽,保证不越界。外汇与贵金属品种用这套训练,杠杆与滑点会让权益项剧烈跳动,属高风险验证,参数乱调可能过拟合到样本内噪声。 想复现就直接把这段贴进 EA 的 Train 调用链,重点改 HistoryBars 和 Iterations 看样本利用率变化。

MQL5 / C++
  RTG.Save(FileName + "RTG.nnw", TimeCurrent(), true);
  class="kw">delete Result;
}
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);
  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 = (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;
       }
     Actions = vector<class="type">float>::Zeros(NActions);
     Agent.Clear();
     RTG.Clear();
     for(class="type">int state = i; state < MathMin(Buffer[tr].Total - class="num">2,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]);
        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);

常见问题

用固定偏移量分两段存储:前段放多单持仓与挂单,后段放空单,索引不交叉即可避免覆盖。
会。建议先按时间窗降维再拼接账户向量,否则状态维度过高容易导致模型初始化失败。
可以。小布能按你给的仓位规则拼装状态向量并模拟买卖双向挂单与平仓,省去手动核对。
常卡在状态数组长度写死、卷积输出未展平、账户向量补齐零位不一致这三处。
不用每帧重算全量,只在成交或挂单变更时更新持仓段,其余帧直接复用缓存拼接。