交易中的神经网络:面向自适应智能体行为的技能层次结构(完结篇)(基础篇)
把交易决策拆成可训练的技能层
在 MT5 里做自适应交易智能体,核心不是堆一个庞大网络,而是把行为切成多层技能:底层管信号识别,中层管仓位与止损逻辑,高层管跨周期切换。这样每层都能单独回测与替换,避免黑箱整体调参。 原文给出的基础骨架用 MQL5 的 CTrade 与信号接口搭了一个最小可运行体,先跑通再逐层加神经元。外汇与贵金属波动剧烈,这类智能体在高杠杆下可能放大回撤,实盘前必须用历史数据做概率验证。 下面这段初始化代码定义了智能体句柄与交易对象,是后续技能层挂载的入口。
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 看卷积核数是否等于行为向量维度。
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 里对照自建工程。
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 失败,策略信号可能整体偏移,需自行验证。
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 控制是否缩放及偏移量。外汇与贵金属模型用这套结构时波动放大,梯度爆炸概率偏高,建议先在小样本上验证数值稳定性。
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;
}