交易中的神经网络:面向自适应智能体行为的技能层次结构(完结篇)(基础篇)
📘

交易中的神经网络:面向自适应智能体行为的技能层次结构(完结篇)(基础篇)

第 1/3 篇

把交易决策拆成可训练的技能层

在 MT5 里做自适应交易智能体,核心不是堆一个庞大网络,而是把行为切成多层技能:底层管信号识别,中层管仓位与止损逻辑,高层管跨周期切换。这样每层都能单独回测与替换,避免黑箱整体调参。 原文给出的基础骨架用 MQL5 的 CTrade 与信号接口搭了一个最小可运行体,先跑通再逐层加神经元。外汇与贵金属波动剧烈,这类智能体在高杠杆下可能放大回撤,实盘前必须用历史数据做概率验证。 下面这段初始化代码定义了智能体句柄与交易对象,是后续技能层挂载的入口。

MQL5 / C++
class="type">int agentHandle;
CTrade trade;

server class="type">void OnInit()
  {
   agentHandle = INVALID_HANDLE;
   trade.SetDeviationInPoints(class="num">10);
  }

◍ 把多智能体框架搬进行情里

HiSSD 这套分层独立技能发现框架,原本是在仿真环境里跑通的,但它的双层技能分解结构对金融市场很对路——行情可能在几秒内剧烈转向,算法必须快速协同响应。通用技能负责趋势识别、风险评估这类跨场景行为,任务专属技能只在特殊市况下接管控制,两者解耦后智能体不用整体重训就能即时调姿。 另一点是可扩展性。市场本身就是一个多智能体系统:人工盘手、算法单、做市商互相扰动。HiSSD 用共享控制模块让各智能体协同,设计复杂策略时不会破坏内部一致性,用来搭韧性交易系统比较实用。 我们在前一篇已实现通用技能编码器并封进 CNeuronSkillsEncoder 类,本文接着把它补全并在真实历史数据上测效果。框架分规划器和控制器两块:规划器是线性流,原始数据过编码器再进预测模块估状态和收益;控制器更复杂,动作解码器吃本地观测、通用技能和任务专属技能三路输入,所以得做成独立对象。外汇贵金属波动剧烈、杠杆风险高,这类实验只在 MT5 历史回测中验证可行性,不代表实盘稳健。

「把多智能体控制器拆成可并行跑的卷积基类」

HiSSD 里的控制器对象被封装进 CNeuronHiSSDLowLevelControler,基类直接选 CNeuronConvOCL。原因很实在:解码器在模块输出端,最终解码层能委托父类处理,而多智能体并行「意识」表征用串行卷积层就能摊开。 Init 里先把父类方法调起来,卷积窗口和步幅都锁成单个智能体内部信息向量维度;卷积核数量等于单个智能体行为向量维度。要让 N 个智能体完全独立学习,技巧是把输入序列维度设 1,把智能体数量塞进「单元序列数量」参数——这样任意数量智能体都能完全并行,权重互不串。 内部组件声明后构造函数留空,全部推到 Init:任务专属技能编码器复用之前写的通用编码器;通用技能维度只给最小有效值,因为它来自外部规划器,这里不敢乱填。三类输入(本地观测、通用技能、专属技能)按行拼成矩阵,观测先转置成单变量序列友好格式,再用批量归一化压住数值尺度差异。 前向 feedForward 走双流:主流入多模态时间序列,辅助流入通用技能张量。专属技能编码器自己查主输入指针合法性,产出后转置、归一化、过三层动作解码器,吐出所有智能体拼接动作张量。反向 calcInputGradients 逆序走卷积层,到归一化层后逆拼接回三路,各流独立激活函数要用导数修梯度,最后主辅两流梯度相加并复原指针。 代码里 cActionDecoder[2] 是两层卷积解码,feedForward 单参版本直接返回 false,真正运算走带 SecondInput 的重载。想验证就开 MT5 把这段类头挂到自己的 EA 里,先跑 Init 看卷积核数是否等于行为向量维度。

MQL5 / C++
class CNeuronHiSSDLowLevelControler:  class="kw">public CNeuronConvOCL
  {
class="kw">protected:
   class="type">uint                iTaskSkills;
   class="type">uint                iCommonSkills;
   class=class="str">"cmt">//---
   CNeuronSkillsEncoder      cTaskSpecificSkillsEncoder;
   CNeuronTransposeOCL       cTranspose;
   CNeuronBaseOCL           cObservAndSkillsConcat;
   CNeuronBatchNormOCL      cNormalizarion;
   CNeuronConvOCL           cActionDecoder[class="num">2];
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool             feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; }
   class="kw">virtual class="type">bool             feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override;
   class="kw">virtual class="type">bool             updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; }
   class="kw">virtual class="type">bool             updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second) class="kw">override;
   class="kw">virtual class="type">bool             calcInputGradients(CNeuronBaseOCL *prevLayer) class="kw">override { class="kw">return class="kw">false; }
   class="kw">virtual class="type">bool             calcInputGradients(CNeuronBaseOCL *NeuronOCL,
                                               CBufferFloat *SecondInput,
                                               CBufferFloat *SecondGradient,

低层控制器类的接口与初始化签名

在基于 OpenCL 的强化学习策略网络里,CNeuronHiSSDLowLevelControler 承担底层动作控制的神经元职责。它继承自通用 OpenCL 神经元基类,对外暴露的虚函数几乎都带 override 标记,说明其必须严格匹配基类的调用约定,否则在 MT5 的神经网络模块编译期就会报错。 类内声明了默认构造与析构,以及一组核心虚接口:Type() 直接返回 defNeuronHiSSDLowLevelControler 常量,用于运行时类型辨识;Save / Load 负责将权重序列化到文件句柄;WeightsUpdate 接收源神经元与学习率 tau 完成梯度更新;SetOpenCL 则注入计算设备对象。 Init 函数的参数表很长,值得逐一看:numOutputs 与 myIndex 决定该神经元在层内的位置,time_step 和 window 控制时序展开长度,variables / task_skills / common_skills / n_actions 描述状态与动作空间维度,heads 对应多头注意力头数,optimization_type 与 batch 则绑定优化器与批大小。实际调参时,若 n_actions 设错(例如黄金 M5 策略里动作只有 3 类却填了 5),初始化会返回 false。 下面给出该类声明与 Init 签名片段,方便你在 MetaEditor 里对照自建工程。

MQL5 / C++
ENUM_ACTIVATION SecondActivation = None
) class="kw">override;
class="kw">public:
  CNeuronHiSSDLowLevelControler(class="type">void) {};
  ~CNeuronHiSSDLowLevelControler(class="type">void) {};
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint time_step, class="type">uint variables,
                         class="type">uint task_skills, class="type">uint common_skills, class="type">uint n_actions,
                         class="type">uint window, class="type">uint step, class="type">uint window_key, class="type">uint heads,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">override const   { class="kw">return defNeuronHiSSDLowLevelControler;  }
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Save(class="type">int const file_handle) class="kw">override;
  class="kw">virtual class="type">bool      Load(class="type">int const file_handle) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj) class="kw">override;
};
class="type">bool CNeuronHiSSDLowLevelControler::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                         class="type">uint time_step, class="type">uint variables, class="type">uint task_skills,
                                         class="type">uint common_skills, class="type">uint n_actions, class="type">uint window,
                                         class="type">uint step, class="type">uint window_key, class="type">uint heads,

◍ HiSSD控制器的初始化与前向链路

在 MT5 的 OpenCL 神经网络模块里,CNeuronHiSSDLowLevelControler 的 Init 承担了一连串子层对象的装配。先是基类卷积层用 SIGMOID 激活,随后任务技能编码器 cTaskSpecificSkillsEncoder 以 None 激活初始化,index 从 0 起逐层自增,任何一步返回 false 都会中断整个网络构建。 窗口尺寸由 time_step + iTaskSkills + iCommonSkills 算出,其中 iCommonSkills 被强制取 MathMax(common_skills, 1),避免为 0 导致后续拼接层神经元数为空。cObservAndSkillsConcat 与 cNormalizarion 均设 None 激活,说明这两层只做线性变换与归一,不参与非线性映射。 前向 feedForward 的调用顺序很固定:先跑任务技能编码与转置,再用 Concat 把观测与技能压进拼接层,归一后送进动作解码器循环。解码器用 SoftPlus 激活,且每循环一次 window_size 被重置为 window_key,这个细节直接决定下一层输入维度。 把下面这段 Init 片段直接塞进你的 EA 头文件做断点跟踪,能在 MT5 策略测试器里看清每层 index 的走向与返回状态,外汇与贵金属行情波动剧烈,这类 GPU 计算链路一旦某层 false Silent 失败,策略信号可能整体偏移,需自行验证。

MQL5 / C++
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, window_key, window_key,
                            n_actions, class="num">1, variables, optimization_type, batch))
     class="kw">return class="kw">false;
  SetActivationFunction(SIGMOID);
class="type">int index = class="num">0;
if(!cTaskSpecificSkillsEncoder.Init(class="num">0, index, OpenCL, time_step, variables, task_skills,
                                    window, step, window_key, heads, optimization, iBatch))
   class="kw">return class="kw">false;
cTaskSpecificSkillsEncoder.SetActivationFunction(None);
iTaskSkills = task_skills;
iCommonSkills = MathMax(common_skills, class="num">1);
index++;
if(!cTranspose.Init(class="num">0, index, OpenCL, time_step, variables, optimization, iBatch))
   class="kw">return class="kw">false;
class="type">uint window_size = (time_step + iTaskSkills + iCommonSkills);
index++;
if(!cObservAndSkillsConcat.Init(class="num">0, index, OpenCL, window_size * iVariables,
                                optimization, iBatch))
   class="kw">return class="kw">false;
cObservAndSkillsConcat.SetActivationFunction(None);
index++;
if(!cNormalizarion.Init(class="num">0, index, OpenCL, cObservAndSkillsConcat.Neurons(),
                         iBatch, optimization))
   class="kw">return class="kw">false;
cNormalizarion.SetActivationFunction(None);
  for(class="type">uint i = class="num">0; i < cActionDecoder.Size(); i++)
     {
     index++;
     if(!cActionDecoder[i].Init(class="num">0, index, OpenCL, window_size, window_size, window_key,
                                class="num">1, iVariables, optimization, iBatch))
        class="kw">return class="kw">false;
     cActionDecoder[i].SetActivationFunction(SoftPlus);
     window_size = window_key;
     }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronHiSSDLowLevelControler::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
  if(!SecondInput)
     class="kw">return class="kw">false;
if(!cTaskSpecificSkillsEncoder.FeedForward(NeuronOCL))
   class="kw">return class="kw">false;
if(!cTranspose.FeedForward(NeuronOCL))
   class="kw">return class="kw">false;
if(!Concat(cTranspose.getOutput(), cTaskSpecificSkillsEncoder.getOutput(), SecondInput,
           cObservAndSkillsConcat.getOutput(), cTranspose.GetCount(),
           iTaskSkills, iCommonSkills, iVariables))
   class="kw">return class="kw">false;
  if(!cNormalizarion.FeedForward(cObservAndSkillsConcat.AsObject()))
     class="kw">return class="kw">false;
  CNeuronBaseOCL *neuron = cNormalizarion.AsObject();
  for(class="type">uint i = class="num">0; i < cActionDecoder.Size(); i++)
     {
     if(!cActionDecoder[i].FeedForward(neuron))
        class="kw">return class="kw">false;
     neuron = cActionDecoder[i].AsObject();
     }
class=class="str">"cmt">//---
  class="kw">return CNeuronConvOCL::feedforward(neuron);
  }
class="type">bool CNeuronHiSSDLowLevelControler::calcInputGradients(CNeuronBaseOCL *NeuronOCL,

「反向传播里梯度如何逐层回传」

这段实现的是解码器链的反向传播入口,从最后一层动作解码神经元开始,沿着 cActionDecoder 容器倒序调用 calcHiddenGradients,把梯度一层层往前送。 容器大小由 cActionDecoder.Size() 决定,若返回 0 则直接退出,说明网络还没装配好,这时候在 MT5 里跑回测会拿不到任何梯度更新。 归一化层与观测-技能拼接层各自调用 calcHiddenGradients 后,通过 DeConcat 把梯度拆回任务专属编码器、转置层和拼接层三路;其中 iTaskSkills、iCommonSkills、iVariables 三个索引参数决定了拆分维度,调参时改错一个就可能让梯度张量越界。 最后对转置层、任务编码器和主神经元做激活函数反演(DeActivation)与梯度重算,并用 SumAndNormilize 把临时梯度合并归一,标志位 false 与四个整型 0/0/0/1 控制是否缩放及偏移量。外汇与贵金属模型用这套结构时波动放大,梯度爆炸概率偏高,建议先在小样本上验证数值稳定性。

MQL5 / C++
                                                                   CBufferFloat *SecondInput,
                                                                   CBufferFloat *SecondGradient,
                                                                   ENUM_ACTIVATION SecondActivation = -class="num">1)
  {
   if(!NeuronOCL || !SecondGradient)
      class="kw">return class="kw">false;
   class="type">uint total = cActionDecoder.Size();
   if(total <= class="num">0)
      class="kw">return class="kw">false;
   CObject *neuron = cActionDecoder[total - class="num">1].AsObject();
class=class="str">"cmt">//---
   if(!CNeuronConvOCL::calcInputGradients(neuron))
      class="kw">return class="kw">false;
   for(class="type">int i = class="type">int(total - class="num">2); i >= class="num">0; i--)
     {
      if(!cActionDecoder[i].calcHiddenGradients(neuron))
         class="kw">return class="kw">false;
      neuron = cActionDecoder[i].AsObject();
     }
if(!cNormalizarion.calcHiddenGradients(neuron))
   class="kw">return class="kw">false;
if(!cObservAndSkillsConcat.calcHiddenGradients(cNormalizarion.AsObject()))
   class="kw">return class="kw">false;
if(!DeConcat(cTranspose.getGradient(), cTaskSpecificSkillsEncoder.getGradient(), SecondGradient,
             cObservAndSkillsConcat.getGradient(), cTranspose.GetCount(),
             iTaskSkills, iCommonSkills, iVariables))
   class="kw">return class="kw">false;
if(SecondActivation != None)
  {
   if(!DeActivation(SecondInput, SecondGradient, SecondGradient, SecondActivation))
      class="kw">return class="kw">false;
  }
if(NeuronOCL.Activation() != None)
  {
   if(!DeActivation(cTranspose.getOutput(), cTranspose.getGradient(),
                    cTranspose.getGradient(), NeuronOCL.Activation()))
      class="kw">return class="kw">false;
  }
if(cTaskSpecificSkillsEncoder.Activation() != None)
  {
   if(!DeActivation(cTaskSpecificSkillsEncoder.getOutput(), cTaskSpecificSkillsEncoder.getGradient(),
                    cTaskSpecificSkillsEncoder.getGradient(), cTaskSpecificSkillsEncoder.Activation()))
      class="kw">return class="kw">false;
  }
if(!NeuronOCL.calcHiddenGradients(cTaskSpecificSkillsEncoder.AsObject()))
   class="kw">return class="kw">false;
   CBufferFloat *temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(cTranspose.getPrevOutput(), class="kw">false) ||
      !NeuronOCL.calcHiddenGradients(cTranspose.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp, iVariables, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1) ||
      !NeuronOCL.SetGradient(temp, class="kw">false))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }

常见问题

建议先拆成信号识别、仓位触发、风控退出三层,每层单独用历史行情做监督训练,再拼起来回测。
会让各智能体跑独立卷积基类并共享行情缓冲,只在风控层做交叉约束,可避免相互拖拽。
小布可读取你的品种页数据,自动标注各技能层信号冲突点,并给出并行化改造建议。
传入窗口长度、特征维度、学习率即可;切片用60~120根K线最稳,过长易记忆噪声。
多是某层用了不可导阈值或梯度被手动截断;逐层打印梯度范数,归零那层就是断点。