神经网络在交易中的应用:市场异常的自适应检测(终篇)·进阶篇
🧠

神经网络在交易中的应用:市场异常的自适应检测(终篇)·进阶篇

(2/3)· 接上篇理论,我们动手搭自适应瓶颈模块,看它如何实时调压缩级别揪出异常盘口

案例拆解 第 2/3 篇
多数人在波动骤增时还用固定窗口看盘,结果把异常当噪声过滤掉了。DADA 的自适应瓶颈正是为了解决这个问题,让压缩级别跟着行情走。先理解模块构造,后面训练才不会白跑。

「编码器后段的层定义与窗口配置」

下面这段 MT5 神经网络编码器从第 4 层到第 8 层的定义,展示了卷积与自适应批归一化层如何串接。第 4 层用 defNeuronConvOCL,窗口取 HistoryBars / Segments,步长等于窗口,输出维度锁在 EmbeddingSize,上一层输出 prev_wout 被记录为后续层的输入窗口。 第 6 层是重点:类型 defNeuronAdaBN,window_out 写死 256,step=3 对应 Top K 选择;内部用长度为 15 的临时数组 temp,循环填入 (i+1)*8,也就是 8,16,24…120,再拷进 descr.windows。batch 设 1e4、优化器 ADAM,激活函数为 None。 第 7、8 层继续卷积,count 乘上 BarDescr 扩维,window_out 分别从 EmbeddingSize/2 降到 HistoryBars/Segments,激活在 SoftPlus 与 TANH 间切换。任何一层 Add 失败都 delete descr 并 return false,避免内存泄漏。 开 MT5 把 HistoryBars、Segments、EmbeddingSize 代入,能直接算出第 4 层 descr.count = (HistoryBars + HistoryBars/Segments - 1) / (HistoryBars/Segments),外汇与贵金属品种上跑这套结构前务必认清过拟合与实盘滑点风险。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
        class="kw">return false;
  descr.type = defNeuronTransposeOCL;
  descr.window = BarDescr;
  prev_count = descr.count = HistoryBars;
  descr.activation = SoftPlus;
  if(!encoder.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;
  descr.window = HistoryBars / Segments;
  prev_count = descr.count = (HistoryBars + descr.window - class="num">1) / descr.window;
  descr.step = descr.window;
  descr.layers = BarDescr;
  descr.activation = SoftPlus;
  class="type">int prev_wout = descr.window_out = EmbeddingSize;
  if(!encoder.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;
  descr.count = prev_count;
  descr.window = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = EmbeddingSize;
  descr.layers = BarDescr;
  descr.activation = TANH;
  if(!encoder.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 = defNeuronAdaBN;
  descr.window = prev_wout;
  descr.count = prev_count;
  descr.window_out = class="num">256;
  descr.step = class="num">3; class=class="str">"cmt">// Top K
  descr.layers = BarDescr; class=class="str">"cmt">// Variables
    {
      class="type">int temp[class="num">15];
      for(class="type">uint i = class="num">0; i < temp.Size(); i++)
          temp[i] = class="type">int(i + class="num">1) * class="num">8;
      if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size())
          class="kw">return false;
    }
  descr.batch = class="num">1e4;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.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 = defNeuronConvOCL;
  descr.count = prev_count * BarDescr;
  descr.window = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = EmbeddingSize / class="num">2;
  descr.activation = SoftPlus;
  if(!encoder.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 = defNeuronConvOCL;
  descr.count = prev_count * BarDescr;
  descr.window = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = HistoryBars / Segments;
  descr.activation = TANH;
  if(!encoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }

编码器尾部与策略网络的层定义

编码器在第九、第十层收口:第九层用转置神经元把 BarDescr 个特征按 HistoryBars 窗口铺开,激活函数设为 None,不做非线性扭曲;第十层接反归一化层,节点数直接拉到 HistoryBars * BarDescr,把编码空间还原回原始量纲。这两层都不参与梯度激活,纯粹做张量形变。 Latent 指针从 encoder.At(LatentLayer) 取出后,actor 网络清空重搭。输入层取账户描述维度 AccountDescr,优化器指定 ADAM,但激活仍为 None——原始账户状态不急着压缩。 紧接着的批归一化层把 prev_count 个节点做归一,batch 设成 1e4,意味着训练时按一万条样本估算均值方差,能缓解账户特征量纲差异导致的梯度偏移。 拼接层把 LatentCount 个隐变量按 step = latent.count * latent.window * latent.layers 的跨度缝进网络,激活用 SoftPlus 保非负;后面三层 BaseOCL 全用 TANH 把输出压到 [-1,1],batch 统一 1e4。外汇与贵金属杠杆高,这类网络权重若过拟合历史样本,实盘信号失效概率会明显上升,建议先在 MT5 策略测试器用自有数据跑前向验证。

MQL5 / C++
  }
class=class="str">"cmt">//--- layer class="num">9
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronTransposeOCL;
  descr.count = BarDescr;
  descr.window = HistoryBars;
  descr.activation = None;
  if(!encoder.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 = defNeuronRevInDenormOCL;
  descr.count = HistoryBars * BarDescr;
  descr.layers = class="num">1;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Latent
  CLayerDescription *latent = encoder.At(LatentLayer);
  if(!latent)
      class="kw">return false;
class=class="str">"cmt">//--- Actor
  actor.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = AccountDescr;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.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 = defNeuronBatchNormOCL;
  descr.count = prev_count;
  descr.batch = class="num">1e4;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.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 = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = prev_count;
  descr.step = latent.count * latent.window * latent.layers;
  descr.batch = class="num">1e4;
  descr.activation = SoftPlus;
  descr.optimization = ADAM;
  if(!actor.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.batch = class="num">1e4;
  descr.activation = TANH;
  descr.optimization = ADAM;
  if(!actor.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 = LatentCount;
  descr.activation = TANH;
  descr.batch = class="num">1e4;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">5
  if(!(descr = new CLayerDescription()))

◍ 策略网络的概率头堆叠方式

这段构建逻辑把 actor 与 probability 两个网络头分开挂接,actor 先以 defNeuronBaseOCL 类型、SIGMOID 激活、batch=1e4、ADAM 优化器加入 NActions 个输出节点,失败即释放描述符并返回 false。 probability 网络从输入层开始:节点数 = latent.count * latent.window * latent.layers,激活函数沿用 latent.activation,同样走 ADAM。往后三层隐藏层均设 LatentCount 个节点、TANH 激活、batch=1e4,属于对称编码结构。 第四层把节点数压到 NActions/3,换用 SoftPlus 激活,输出恒非负,适合表达动作概率的偏置。末层用 defNeuronSoftMaxOCL 做归一化,step=1、激活 None,把前层输出收成总和为 1 的概率分布。 在 MT5 里把 LatentCount 与 NActions 调小(例如 32 与 9),能明显压低显存占用;外汇与贵金属品种波动剧烈,这类网络推理仅作概率参考,实盘仍属高风险。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = class="num">1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
   probability.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
     class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = latent.count * latent.window * latent.layers;
   descr.activation = latent.activation;
   descr.optimization = ADAM;
   if(!probability.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.activation = TANH;
   descr.batch = class="num">1e4;
   descr.optimization = ADAM;
   if(!probability.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 = TANH;
   descr.batch = class="num">1e4;
   descr.optimization = ADAM;
   if(!probability.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 / class="num">3;
   descr.activation = SoftPlus;
   descr.batch = class="num">1e4;
   descr.optimization = ADAM;
   if(!probability.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;
   prev_count = descr.count = prev_count;
   descr.step = class="num">1;
   descr.activation = None;
   descr.batch = class="num">1e4;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }

「三模型并行训练的内在逻辑」

训练算法放在 EA 的 Train 方法里,因为要同时跑环境编码器、执行者和方向判别三个模型,逻辑上得改造成并行结构。外层按批次迭代,每批从经验回放缓冲区随机抽一条轨迹,再选个起始点;内层沿连续状态推进,这种做法跟常规随机状态训练不同,轨迹动作由 EA 基于后续状态信息直接生成,指导更精确。 重播缓冲区的好处是除最后一条外都有后续状态数据,但代价是丢了未平仓位信息。框架里模型无循环结构,为了让模型既会开仓又会找最优平仓点,训练时须拼出小批次和「最优」仓位一起喂。 代码里先用 probability 向量做均匀抽样权重,大小是 Buffer.Size(),均值 1/Size;外层循环步长 Batch,用 MathRand 平方归一化挑起始点,避开头尾 NForecast+Batch 个位置。时间特征把账户时间戳相对 2023 全年秒数做正弦编码,再除月线秒数加另一周期项,这是给编码器塞周期律。 训练顺序固定:编码器前馈→执行者逼近最优操作→第三模型按下一柱颜色判方向,每轮记进度。全部迭代完吐日志并自关 EA。外汇与贵金属训练涉及杠杆与滑点,回测轨迹再理想也只代表历史概率,实盘可能显著偏离。

MQL5 / C++
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">class="kw">float> probability = vector<class="type">class="kw">float>::Full(Buffer.Size(), class="num">1.0f / Buffer.Size());
class=class="str">"cmt">//---
   vector<class="type">class="kw">float> result, target, state;
   matrix<class="type">class="kw">float> fstate = matrix<class="type">class="kw">float>::Zeros(class="num">1, NForecast * BarDescr);
   class="type">bool Stop = false;
class=class="str">"cmt">//---
   class="type">uint ticks = GetTickCount();
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
       class="type">int tr = SampleTrajectory(probability);
       class="type">int start = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast - Batch));
       if(start <= class="num">0)
         {
          iter -= Batch;
          class="kw">continue;
         }
       if(!Encoder.Clear() ||
          !Actor.Clear())
         {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
          Stop = true;
          class="kw">break;
         }
       result = vector<class="type">class="kw">float>::Zeros(NActions);
       for(class="type">int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
         {
          if(!state.Assign(Buffer[tr].States[i].state) ||
             MathAbs(state).Sum() == class="num">0 ||
             !bState.AssignArray(state))
           {
            iter -= Batch + start - i;
            class="kw">break;
           }
          class=class="str">"cmt">//---
          bTime.Clear();
          class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
          class="type">class="kw">double x = 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;);
          bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
          x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);

把账户状态塞进网络做前向推理

这段逻辑发生在每个时间步收尾,先把周线和日线的周期相位写进 bTime 缓冲:用 time 除以 PERIOD_W1、PERIOD_D1 的秒数得到 x,再取 2πx 的 sin 值入列,x 为 0 时直接落 0,避免除零噪声。紧接着把上一根的状态余额 PrevBalance、权益 PrevEquity 取出,用当前 result 与 _Point 算出一跳 profit,再向 bAccount 压入 9 个特征——含权益比、盈利占比、最大浮盈浮亏等,最后挂上 bTime 指针一并 BufferWrite。 前向传播分三步:Encoder 吃 bState 做编码,Actor 以 bAccount 加 Encoder 隐层输出做决策,Probability 再基于 Encoder 隐层算概率分布。任一步 feedForward 返回 false 就打印函数名加行号、置 Stop 并 break,说明这套自编码+策略网络在实盘回放里对中间层断层是零容忍的。 你在 MT5 里接这段时,重点核对 bAccount 第 3、4 维(最大多头/空头回撤幅度)是否为非负——原代码用 MathMax(...,0) 兜底,若你的行情源 result[0]-result[3] 符号反了,这两个特征会恒为 0,Actor 学到的风险偏好就会偏向裸奔。外汇与贵金属杠杆高,这类隐层特征错位可能让概率输出失真,上真实账户前务必用历史 tick 复跑一遍前向链路。

MQL5 / C++
  bTime.Add((class="type">class="kw">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  if(bTime.GetIndex() >= class="num">0)
     bTime.BufferWrite();
  class=class="str">"cmt">//--- Account
  class="type">class="kw">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
  class="type">class="kw">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
  class="type">class="kw">float profit = class="type">class="kw">float(bState[class="num">0] / _Point * (result[class="num">0] - result[class="num">3]));
  bAccount.Clear();
  bAccount.Add(class="num">1);
  bAccount.Add((PrevEquity + profit) / PrevEquity);
  bAccount.Add(profit / PrevEquity);
  bAccount.Add(MathMax(result[class="num">0] - result[class="num">3], class="num">0));
  bAccount.Add(MathMax(result[class="num">3] - result[class="num">0], class="num">0));
  bAccount.Add((bAccount[class="num">3] > class="num">0 ? profit / PrevEquity : class="num">0));
  bAccount.Add((bAccount[class="num">4] > class="num">0 ? profit / PrevEquity : class="num">0));
  bAccount.Add(class="num">0);
  bAccount.AddArray(GetPointer(bTime));
  if(bAccount.GetIndex() >= class="num">0)
     bAccount.BufferWrite();
  class=class="str">"cmt">//--- Feed Forward
  if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;
     class="kw">break;
     }
  if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder), LatentLayer))
     {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     Stop = true;
     class="kw">break;
     }
  if(!Probability.feedForward(GetPointer(Encoder), LatentLayer, (CBufferFloat*)NULL))
把异常扫描交给小布盯盘
这些诊断逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时异常评分,你只管判断要不要动手。

常见问题

两者都并行跑多个小子模型,但自适应瓶颈按上下文动态调各子模型权重与压缩强度,MoE 通常只做专家选择,不实时改压缩级别。
目前小布内置的是通用异常评分,不支持外部模型权重导入;若你想用自训参数,仍需在 MT5 端跑 EA,小布负责可视化预警。
原文作者在实现里用它替代部分瓶颈功能,便于在 OpenCL 下动态调整压缩,并非严格照抄论文结构,属于工程折中。
分块逼模型学局部模式,随机掩码强制重建隐藏段,两者共同提升泛化,避免模型只记住某段行情的特征。