神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器·进阶篇
🧠

神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器·进阶篇

(2/3)· 从机器人导航到 MT5 交易策略,看控制转换器如何把长周期规划切成可训练片段

新手友好 第 2/3 篇
很多交易者一听到决策转换器就默认它只能跑机器人导航,其实把长周期控制拆成子任务片段的思路,直接能套到随机行情里的仓位节奏上。本篇接着把这套层次化控制逻辑落到可训练的局部策略,少走弯路。

「强化学习代理如何落地下单与风控」

这段逻辑把智能体输出的动作向量直接翻译成 MT5 的下单指令,同时卡死券商的最小手数和止损距离。temp 数组里 0~2 位管 buy、3~5 位管 sell,先做一次多空手数互斥抵消,避免同根品种双向加仓把保证金打爆。 sState.account[7] 存当前 K 线时间,rewards[0] 用 (余额-前余额)/前余额 算单步收益率,rewards[1] 则是权益/前余额-1。若 (CurrentBar-StartBar)<MaxSteps 且 CurrentBar<StartBar,直接复用已加载状态里的动作;否则由 SampleAction(NActions) 随机采样,这意味着回测早期策略倾向探索而非死守旧权重。 下单前先取 min_lot、step_lot,并用 MathMax(Symb.StopsLevel(),1)*Point() 得出 stops 底线。buy 控制块里,只要 temp[0]<min_lot、或 TP/SL 距离不超过 stops,就平掉已有买仓;否则把 agent 手数对齐到 step_lot 整数倍,用 Ask±动作*MaxTP/MaxSL*Point 算 TP/SL,已有买仓走 TrailPosition 移动止损,手数不符则部分平仓或 Trade.Buy 补仓。外汇与贵金属杠杆高,这类自动调仓在滑点扩大时可能触发连环平仓,务必在策略测试器用真实点差验证。 卖侧对称处理:temp[3] 不达标或止损空间不足就清 sell,否则对齐手数后由 Bid 反推 sell_tp/sell_sl。注意原文 sell_lot 那行多写了一个分号,MT5 编译器能过但复制到自己 EA 里建议删掉,免得后续重构时误判语法块边界。

MQL5 / C++
  sState.account[class="num">7] = (class="type">float)Rates[class="num">0].time;
class=class="str">"cmt">//---
  sState.rewards[class="num">0] = class="type">float((sState.account[class="num">0] - PrevBalance) / PrevBalance);
  sState.rewards[class="num">1] = class="type">float(sState.account[class="num">1] / PrevBalance - class="num">1.0);
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
  vector<class="type">float> temp = vector<class="type">float>::Zeros(NActions);
  if((CurrentBar - StartBar) < MaxSteps)
    if(CurrentBar < StartBar)
      temp.Assign(Loaded.States[CurrentBar].action);
    else
      temp = SampleAction(NActions);
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">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());

回测帧里怎么筛掉劣质轨迹

强化学习式 EA 在 MT5 里跑优化时,每帧回传的轨迹会先过 OnTesterPass 这道闸。FrameNext 把本次 pass 编号、名称、id 和轨迹数组吐出来,程序先比对名称是不是当前 EA 自身,id 小于等于 0 的直接跳过,避免脏帧污染缓冲。 缓冲上限由 MaxReplayBuffer 控,一旦 Buffer 大小超过它,就触发淘汰:遍历现有轨迹,找末态 account[1](即最终净值类指标)最小的那条,记下 min_tr。新帧里每条轨迹若末态收益 min 小于等于旧最差,才允许顶替,等于用「最差优先淘汰」保住高收益样本。 这套机制直接决定你策略里 replay buffer 的质量。开 MT5 把 MaxReplayBuffer 从默认调小到 200 左右,观察优化器是不是更快收敛到高 equity 参数区;外汇与贵金属杠杆品种波动剧烈,回测优不代表实盘稳,高回撤概率始终在。 别把缓冲当越大越好 很多新手以为 replay buffer 无脑拉满能学得更全,实际上旧劣轨迹占着坑会让策略偏向保守解。按上面代码逻辑,buffer 满后只换「更差」的进来,越大反而稀释了优质样本权重。

MQL5 / C++
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[];
   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;
            for(class="type">int i = class="num">0; i < total; i++)
              {
               class="type">float prof = Buffer[i].States[Buffer[i].Total - class="num">1].account[class="num">1];
               if(prof < min)
                 {
                  min = MathMin(prof, min);
                  min_tr = i;
                 }
              }
            class="type">float prof = array[a].States[array[a].Total - class="num">1].account[class="num">1];
            if(min <= prof)

◍ 用代码搭出强化学习的工作网络

上面这段截取自一个自组织聚类缓冲的收尾逻辑:当命中最小三角套利差 min_tr 时,把 array[a] 写回 Buffer 并打印替换记录;否则按 id 扩容 Buffer 再拷贝。这套缓冲管理是后续喂给神经网络的样本池基础,外汇与贵金属行情的高波动会让样本分布快速偏移,实操中建议跑 MT5 观察 Buffer 扩容频率。 #define WorkerInput 512 定义了工人网络输入维度,CreateWorkerDescriptions 函数则逐层堆出网络结构:输入层 512 节点用 ADAM 优化、无激活;隐层 1 用 LatentCount 个 LReLU 节点;隐层 2 同宽但切到 TANH;隐层 3 扩到 NActions*EmbeddingSize 且线性输出;判别层 4 用 SoftMax 并按 NActions 步进。 逐层 Add 失败都会 delete 描述符并返 false,说明这套构建对内存失败零容忍。你在调 LatentCount 时若设得过小(比如低于 32),隐层 2 的 TANH 可能把梯度压死,策略收敛概率会明显下降。

MQL5 / C++
{
            Buffer[min_tr] = array[a];
            PrintFormat("Replace %.2f to %.2f -> bars %d", min, prof, array[a].Total);
            }
          }
        }
      else
        {
         if(ArrayResize(Buffer, total + (class="type">int)id, class="num">10) < class="num">0)
            class="kw">return;
         ArrayCopy(Buffer, array, total, class="num">0, (class="type">int)id);
         }
      }
   }
class="macro">#define                 WorkerInput              class="num">512
class="type">bool CreateWorkerDescriptions(CArrayObj *worker, CArrayObj *descriminator)
   {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!worker)
      {
      worker = new CArrayObj();
      if(!worker)
         class="kw">return false;
      }
class=class="str">"cmt">//--- Worker
   worker.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = WorkerInput;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!worker.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 = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!worker.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;
   prev_count = descr.count = LatentCount;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!worker.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;
   prev_count = descr.count = NActions * EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!worker.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 = defNeuronSoftMaxOCL;
   descr.count = EmbeddingSize;
   descr.step = NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!descriminator.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">5

「判别器网络的四层堆叠与训练入口」

在 MT5 用 OpenCL 跑强化学习式策略时,判别器(descriminator)的层结构直接决定策略梯度的偏置方向。上面这段初始化把输入层到输出层依次压进 CArrayObj,任何一层 Add 失败就 delete 描述符并回 false,避免显存泄漏。 输入层节点数等于 NActions,激活函数设 None,仅做透传;随后 layer1 用 LReLU、节点数 LatentCount,layer2 切到 TANH、节点数仍为 LatentCount,layer3 与 layer4 都回到 WorkerInput 宽,其中 layer4 用 defNeuronSoftMaxOCL 做多类概率归一。 Train() 函数开头先取 GetTickCount() 存进 ticks,并置 StopFlag=false,这是后续回测循环里判断是否提前终止的硬开关。外汇与贵金属杠杆品种下,这类自研网络过拟合概率偏高,实盘前务必用历史 Tick 跑通再上模拟盘。 把 LatentCount 从默认调小 30% 往往能让判别器在 EURUSD 的 M15 上收敛更快,但收益曲线平滑度会下降,属于可验证的调参现象。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NActions;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!worker.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- Descriminator
   if(!descriminator)
     {
      descriminator = new CArrayObj();
      if(!descriminator)
        class="kw">return false;
     }
class=class="str">"cmt">//---
   descriminator.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!descriminator.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 = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!descriminator.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;
   prev_count = descr.count = LatentCount;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!descriminator.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 = WorkerInput;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!descriminator.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 = defNeuronSoftMaxOCL;
   descr.count = WorkerInput;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!descriminator.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">//| Train function                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Train(class="type">void)
  {
   class="type">uint ticks = GetTickCount();
class=class="str">"cmt">//---
   class="type">bool StopFlag = false;

对抗训练循环与网络层描述构建

下面这段对抗式训练主循环,是生成器(Worker)与判别器(Descrimitator)交替前馈、反向传播的核心。循环受 Iterations 次数、IsStopped() 与 StopFlag 三重约束,任一 feedForward 或 backProp 失败都会置 StopFlag 并 break,避免 EA 在 MT5 中卡死。 随机扰动样本的落点用 MathRand()/32767.0 映射到 [0, WorkerInput-1],每次只对单个 pos 位写入 1.0f,相当于在输入缓冲里做稀疏噪声注入,逼判别器学更鲁棒的特征。 每过 500 毫秒(GetTickCount() 差值判定),用 Comment() 把当前迭代进度与判别器近期平均误差刷到图表左上角;误差数值由 Descrimitator.getRecentAverageError() 实时给出,开 MT5 跑这套能直接看到误差随迭代下降的曲线倾向。外汇与贵金属行情高波动,此类模型仅作特征提取辅助,实盘信号须自担高风险。 循环结束清掉 Comment 并打印最终判别器误差,随后调 ExpertRemove() 自卸载——说明这是离线训练型 EA,不是常驻策略。 CreateValueDescriptions() 负责搭网络价值层:先防御性 new CArrayObj,再 Clear() 旧描述;输入层 descr.count = ValueBars * BarDescr,类型 defNeuronBaseOCL、激活 None、优化 ADAM。prev_count 暂存该层宽度,供后续隐层参照扩维。

MQL5 / C++
for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++)
  {
  Data.BufferInit(WorkerInput, class="num">0);
  class="type">int pos = class="type">int(MathRand() / class="num">32767.0 * (WorkerInput - class="num">1));
  Data.Update(pos, class="num">1.0f);
  class=class="str">"cmt">//--- Study
  if(!Worker.feedForward(Data,class="num">1,false) ||
     !Descrimitator.feedForward(GetPointer(Worker),-class="num">1,(CBufferFloat *)NULL))
    {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     StopFlag = true;
     break;
    }
  if(!Descrimitator.backProp(Data,(CBufferFloat *)NULL, (CBufferFloat *)NULL) ||
     !Worker.backPropGradient((CBufferFloat *)NULL, (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", "Desciminator",
iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Descrimitator.getRecentAverageError());
     Comment(str);
     ticks = GetTickCount();
    }
  }
 Comment("");
class=class="str">"cmt">//---
 PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Descriminator", Descrimitator.getRecentAverageError());
 ExpertRemove();
class=class="str">"cmt">//---
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateValueDescriptions(CArrayObj *value)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!value)
    {
     value = new CArrayObj();
     if(!value)
       class="kw">return false;
    }
class=class="str">"cmt">//--- Value
  value.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = ValueBars * BarDescr;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!value.Add(descr))
    {
     class="kw">delete descr;

◍ 堆叠五层网络并启动随机采样训练

这段构建逻辑把价值网络拆成五层依次挂进容器,每一层都先 new 一个 CLayerDescription,失败立即 return false 并释放内存,避免悬空指针。第一层是批归一化(BatchNormOCL),batch 写死 1000、激活函数为 None、优化器 ADAM,用来稳住前面传进来的 prev_count 个输入。 第二层换成卷积(ConvOCL),输出维度锁在 window_out=4,激活用 LReLU,窗口与步长都等于 BarDescr,相当于把 K 线局部结构压成 4 通道特征。第三、四层是普通全连接(BaseOCL),隐层宽由 LatentCount 决定,前者 LReLU 后者 TANH,第四层把 prev_count 也刷成 LatentCount 方便后续复用。第五层输出 NRewards 个奖励值,无激活,直接给强化学习当 Q 值近似。 Train 函数里 total_tr 取 Buffer 大小,每轮用 MathRand()/32767.0 做均匀抽样本条轨迹,再用 MathRand()*MathRand()/32767^2 的偏左分布挑起始偏移,最大可用长度为 Buffer[tr].Total - 2*ValueBars,防止卷积窗口越界。Iterations 上限受 IsStopped() 与 StopFlag 双重拦截,实跑时若在 MT5 策略测试器里点停止会立刻断循环。 把 batch 从 1000 下调到 200~400 可能在小样本贵金属数据上收敛更快,但外汇品种高杠杆波动下过拟合概率会上升,动手改之前先单品种回测确认。

MQL5 / C++
  class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.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 = defNeuronConvOCL;
   descr.count = ValueBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   descr.window_out = class="num">4;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!value.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.optimization = ADAM;
   descr.activation = LReLU;
   if(!value.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;
   prev_count = descr.count = LatentCount;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!value.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 = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.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">//| 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">int check = class="num">0;
class=class="str">"cmt">//---
   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)) * (Buffer[tr].Total - class="num">2 * ValueBars));
把重复劳动交给小布
这些层次化模型的片段切分与局部策略诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到训练边界提示,你专注决策。

常见问题

控制转换器用概率路线图把长期规划拆成短距片段,再用局部控制器逐段执行,比单一 RL 策略更适配高维随机环境。
d 过大边会稀疏导致路径难连,过小则图过密训练成本飙升,需按品种波动特征调参。
从构建的 G 图中随机选边作为局面起点,以边终点为条件目标,用样本计划引导策略收敛。
目前小布内置的是片段诊断与边界提示,完整模型训练仍需在 MT5 端自行部署代码,小布负责替你盯盘口异动。
层次化把复杂问题拆为子任务,各自独立建模,面对动态行情时局部策略可单独更新而不重训全局。