交易中的神经网络:具有层化记忆的智代(终篇)(基础篇)
◍ 层化记忆智代在 MT5 上的基本定位
这篇系列把一类带层化记忆结构的神经网络代理(智代)搬进 MetaTrader 5,用来替代传统静态指标做行情状态判别。它核心不在于预测点位,而是让网络按时间层级缓存市场特征,再对当前结构做条件响应。 原文发布于 2025 年 12 月 15 日,在 MQL5 社区显示为 870 次浏览、0 条评论,说明这类偏底层的实现方案仍属小众验证阶段,未进入大众讨论池。 对已有 MT5 开发经验的交易者,最直接可做的动作是:开 MT5 → 用 MetaEditor 新建 EA 骨架 → 把文中网络层缓存逻辑接进 OnTick,先跑 EURUSD 的 M15 看层间激活差异。外汇与贵金属杠杆高,这类实验性系统仅作技术验证,实盘存在显著回撤风险。
「FinMem 框架在 MT5 里的取舍」
FinMem 原版用层化记忆把数据分两路:工作记忆吃每日新闻和行情波动这类短期料,长期记忆存分析报告和研究素材。优先级由结构自动排,短期事件即时算,深层信息留着以后用。 我们之前的 MQL5 移植版有意拿掉了大语言模型——这是原框架的核心部件。去掉之后整条架构都得改,但信息流主干还是复刻了:CNeuronFinMem 对象保留层化处理方式,把短期信息和长期策略方法揉在一起,力求在动态行情里出稳定可预期的表现。 框架作者实验里,FinMem 在有限输入下仍能并发分析数十个市场信号并按重要性结构化,优于其他自主交易模型。外汇和贵金属市场高波动、高杠杆,这类信号并发处理只降低认知负载,不承诺收益。
FinMem 前馈与梯度回传的双流闭环
CNeuronFinMem 的 feed-forward 吃两条数据流:环境状态张量(报价 + 指标数值)和账户状态向量(余额、盈亏、时间戳)。张量先经置换便于投影,再分送两个记忆模块——一个啃柱线时间动态,一个挖多模态单变量间的隐藏相关,最后用交叉注意力把两套表征揉在一起。 账户向量随后介入两次交叉注意力:一次和记忆结果比,看市场变动怎么撬动余额;一次和智代近期动作比,评估策略效率与可接受风险。注意智代动作张量本身是第三条源,但类只接两条输入,于是借对象输出缓冲区存上一帧动作,用指针回调内部交叉注意力,形成重复模块式的闭环。 反向传播 calcInputGradients 必须严格镜像前馈逆序。前馈末尾调了父类生成新动作张量,所以梯度也从父类方法起步,先灌进两条并行路径的内部交叉注意力。难点在于其中一条路径递归用了上一次前馈的结果,回传时会成环——要靠替换缓冲区指针把旧结果无损恢复出来再断环。 账户状态缓冲区被记忆流和动作流共用,两路梯度得分别算完再汇总。环境-状态张量比动作矢量大得多,因此可用其多余缓冲区暂存梯度数据,开销最小。所有指针复位后方法返回逻辑结果,整套非线性链路在 MT5 里跑通时,梯度分发准确性高度依赖这些指针切换顺序。 [CODE] 段里 CNeuronFinMem 继承 CNeuronRelativeCrossAttention,保护成员含 1 个转置对象、2 个记忆单元与 3 个交叉注意力对象;feedForward 和 calcInputGradients 的各双参重载是核心,单参版直接返回 false 被架空。Init 参数暴露了 window / heads / accoiunt_descr / nactions 等维度,调参时 units_count 与 heads 不匹配会直接让 OpenCL 内核编译失败。
<span class="keyword">class</span> CNeuronFinMem : <span class="keyword">class="kw">public</span> CNeuronRelativeCrossAttention { <span class="keyword">class="kw">protected</span>: CNeuronTransposeOCL cTransposeState; CNeuronMemory cMemory[<span class="number">class="num">2</span>]; CNeuronRelativeCrossAttention cCrossMemory; CNeuronRelativeCrossAttention cMemoryToAccount; CNeuronRelativeCrossAttention cActionToAccount; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL) <span class="keyword">class="kw">override</span> { <span class="keyword">class="kw">return</span> <span class="keyword">false</span>; } <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> calcInputGradients(CNeuronBaseOCL *NeuronOCL) <span class="keyword">class="kw">override</span> { <span class="keyword">class="kw">return</span> <span class="keyword">false</span>; } <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> updateInputWeights(CNeuronBaseOCL *NeuronOCL) <span class="keyword">class="kw">override</span> { <span class="keyword">class="kw">return</span> <span class="keyword">false</span>; } <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">public</span>: CNeuronFinMem(<span class="keyword">class="type">void</span>) {}; ~CNeuronFinMem(<span class="keyword">class="type">void</span>) {}; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Init(<span class="keyword">class="type">uint</span> numOutputs, <span class="keyword">class="type">uint</span> myIndex, COpenCLMy *open_cl, <span class="keyword">class="type">uint</span> window, <span class="keyword">class="type">uint</span> window_key, <span class="keyword">class="type">uint</span> units_count, <span class="keyword">class="type">uint</span> heads, <span class="keyword">class="type">uint</span> accoiunt_descr, <span class="keyword">class="type">uint</span> nactions, ENUM_OPTIMIZATION optimization_type, <span class="keyword">class="type">uint</span> batch); <span class="comment">class=class="str">"cmt">//---</span>
◍ 金融记忆神经元的接口与前向传播链路
CNeuronFinMem 类在派生时重写了 7 个虚函数:Type 直接返回 defNeuronFinMem 常量用于运行时识别;Save / Load 负责二进制句柄的序列化;WeightsUpdate 接收源神经元与 tau 学习率做权重更新;SetOpenCL 注入计算上下文;Clear 释放状态。这套接口决定了该神经元能无缝挂进 MT5 的 OCL 训练管线。 feedForward 的前向顺序很固定:先跑转置状态层 cTransposeState,再让两块 cMemory[0]、cMemory[1] 依次前向,其中后者吃转置层的输出。随后 cCrossMemory 交叉两块记忆,cMemoryToAccount 与 cActionToAccount 各自融合 SecondInput 账户特征,最后交换 Output / PrevOutput 缓冲并走相对交叉注意力。任意一步返回 false 即中断,说明单层失败会让整批样本报废。 calcInputGradients 反向时先校验 NeuronOCL、SecondInput、SecondGradient 非空,否则直接退出。它先对相对交叉注意力求梯度,再交换缓冲、把 Gradient 暂存到 temp,借 cMemoryToAccount 的上一步输出重置梯度指针,最后算隐藏层梯度。注意 temp 变量实际未被复用,属于可删的冗余行。 在 MT5 里把这段编译进 EA 后,可故意让 SecondInput 传空指针验证第 41 行防护是否生效;外汇与贵金属杠杆高,模型过拟合时回测漂亮实盘仍可能大幅回撤。
class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronFinMem; } 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=class="str">"cmt">//--- class="kw">virtual class="type">bool Clear(class="type">void) class="kw">override; }; class="type">bool CNeuronFinMem::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cTransposeState.FeedForward(NeuronOCL)) class="kw">return false; if(!cMemory[class="num">0].FeedForward(NeuronOCL) || !cMemory[class="num">1].FeedForward(cTransposeState.AsObject())) class="kw">return false; if(!cCrossMemory.FeedForward(cMemory[class="num">0].AsObject(), cMemory[class="num">1].getOutput())) class="kw">return false; if(!cMemoryToAccount.FeedForward(cCrossMemory.AsObject(), SecondInput)) class="kw">return false; if(!cActionToAccount.FeedForward(this.AsObject(), SecondInput)) class="kw">return false; if(!SwapBuffers(Output, PrevOutput)) class="kw">return false; if(!CNeuronRelativeCrossAttention::feedForward(cActionToAccount.AsObject(), cMemoryToAccount.getOutput())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFinMem::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -class="num">1) { if(!NeuronOCL || !SecondInput || !SecondGradient) class="kw">return false; if(!CNeuronRelativeCrossAttention::calcInputGradients(cActionToAccount.AsObject(), cMemoryToAccount.getOutput(), cMemoryToAccount.getGradient(), (ENUM_ACTIVATION)cMemoryToAccount.Activation())) class="kw">return false; if(!SwapBuffers(Output, PrevOutput)) class="kw">return false; CBufferFloat *temp = Gradient; if(!SetGradient(cMemoryToAccount.getPrevOutput(), false)) class="kw">return false; if(!calcHiddenGradients(cActionToAccount.AsObject(), SecondInput, SecondGradient, SecondActivation)) class="kw">return false; if(!SwapBuffers(Output, PrevOutput))
「反向传播里逐层梯度的串接顺序」
这段片段处在神经网络反向传播的中段,职责是把输出侧的误差沿隐藏层一路回传。任何一步 calcHiddenGradients 返回失败就直接 return false,整轮梯度计算中止,权重不会被更新。 从代码看,梯度回传不是一次性完成的:先处理 SecondInput 对应的交叉记忆梯度,再用 SumAndNormilize 把 SecondGradient 按窗口 1 做归一;随后对 cMemory[0]、cTransposeState、NeuronOCL 依次求隐藏梯度。注意 NeuronOCL 这里被临时存了梯度(temp),切换 prevOutput 后重算,再还原,说明同一层在不同上下文里梯度要分段累加。 在 MT5 里跑这套逻辑时,若你改了网络层数,必须保证 cMemory 数组下标与调用顺序一致,否则 cMemory[1] 取到的输出维度对不上,calcHiddenGradients 会静默返回 false。外汇与贵金属模型训练波动大,这种隐性失败在高杠杆品种上可能让回测曲线突然走平,属常见坑。
class="kw">return false; Gradient = temp; if(!cCrossMemory.calcHiddenGradients(cMemoryToAccount.AsObject(), SecondInput, cMemoryToAccount.getPrevOutput(), SecondActivation)) class="kw">return false; if(!SumAndNormilize(SecondGradient, cMemoryToAccount.getPrevOutput(), SecondGradient, class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; if(!cMemory[class="num">0].calcHiddenGradients(cCrossMemory.AsObject(), cMemory[class="num">1].getOutput(), cMemory[class="num">1].getGradient(), (ENUM_ACTIVATION)cMemory[class="num">1].Activation())) class="kw">return false; if(!cTransposeState.calcHiddenGradients(cMemory[class="num">1].AsObject())) class="kw">return false; if(!NeuronOCL.calcHiddenGradients(cMemory[class="num">0].AsObject())) class="kw">return false; temp = NeuronOCL.getGradient(); if(!NeuronOCL.SetGradient(cTransposeState.getPrevOutput(), false) || !NeuronOCL.calcHiddenGradients(cTransposeState.AsObject()) || !NeuronOCL.SetGradient(temp, false) || !SumAndNormilize(temp, cTransposeState.getPrevOutput(), temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; }
智代模型的层栈怎么搭
可训练模型的结构全部在 CreateDescriptions 里落定,目前只训一个叫「智代」的模型,它是财务系统里的决策核心。框架本身还带了独有学习算法,但这次先不碰,重点看架构骨架。 输入先走全连接层吃原始数据,紧接着批归一化层(batch 设了 1e4),作用是压住数据尺度波动、让后续训练更稳。再往后接我们之前写的 FinMem 模块,它负责数据处理和决策形成的关键一层。 windows 数组里给了三个输入张量维度:单根 K 线描述、账户状态、智代动作。动作维度被设成常量 NActions 的两倍,前半段当分布均值、后半段当方差,交叉注意力会把主输入流劈成两个等长向量,从而在输出拿到成对的均值和方差。 分布内数值交给 VAE 潜态层处理,最后用卷积层(defNeuronConvSAMOCL,窗口与步长都是 3,激活 SIGMOID)把结果投回智代动作区间。外汇与贵金属波动剧烈,这类模型仅提供概率化决策倾向,实盘前务必在 MT5 用历史数据验证。 下面这段代码就是 CreateDescriptions 的实体,逐行看能直接抄进 EA 里改参。指针无效就 new 一个 CArrayObj;输入层维度 = HistoryBars * BarDescr,优化用 ADAM。批归一化层 count 沿用上层、batch=1e4。FinMem 层 windows 写死 {BarDescr, AccountDescr, 2*NActions},window_out=32、step=4 即 32 个头。VAE 层 count=NActions。卷积层 count=NActions/3,SIGMOID 收口。
class="type">bool CreateDescriptions(CArrayObj *&actor) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) 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; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); 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 = defNeuronFinMem; class=class="str">"cmt">//--- Windows { class="type">int temp[] = {BarDescr, AccountDescr, class="num">2*NActions}; class=class="str">"cmt">//Window, Account description, N Actions if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return false; } descr.count = HistoryBars; descr.window_out = class="num">32; descr.step = class="num">4; class=class="str">"cmt">// Heads 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">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronVAEOCL; descr.count = NActions; 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 = defNeuronConvSAMOCL; descr.count = NActions / class="num">3; descr.window = class="num">3; descr.step = class="num">3; descr.window_out = class="num">3; descr.activation = SIGMOID;