交易中的神经网络:智能预测流程(结论)
引言
Time‑MoE框架为时间序列处理提供了一种全新思路。不同于传统模型,该框架基于基于点的标记化(point-based tokenization),完整保留每一笔 Tick 数据或 K 线的全部信息;再通过SwiGLU嵌入层对这些基础词元做特征增强,既可捕捉平缓趋势行情,也能识别波动率的剧烈脉冲。
作者的核心思路:在仅解码器 Transformer(Decoder-Only Transformer)内部采用稀疏混合专家结构,针对每个词元动态挑选最适配的子模型;搭配常驻的共享专家(shared expert),共同构成具备强自适应、可扩展的模型架构。多头预测输出进一步完善整套体系,可同时生成多时间维度预测 —— 从下一笔 Tick 到周线级别趋势。

在介绍Time-MoE框架的第一篇文章中,我们分步演示如何把论文《Time-MoE:基于混合专家的十亿规模时间序列基础模型》中的理论,转化为可运行的 MQL5 代码。我们开发了CNeuronSwiGLUOCL模块,该模块通过两组投影融合,将原始数据转换为隐向量。
在第二篇中,我们聚焦稀疏混合专家机制,并将其封装为独立模块CNeuronTimeMoESparseExperts。模块融合独立数据通路与共享数据通路、Top-K 路由,以及用于共享专家的 sigmoid 门控。
现在,我们将把所有组件整合为完整模型,搭建训练流程。最后,我们将在真实历史行情数据上测试训练完成的模型。
注意力模块
在搭建完整模型架构前,需要完成一项微小但至关重要的改动:把 Transformer 架构里标准的前馈网络(FeedForward)模块,替换为我们自研的稀疏混合专家模块。补充说明:Time-MoE基于仅解码器架构,其中交叉注意力(cross-attention)发挥关键作用,使解码器能够利用上下文层的信息。
实现方式:选用现成的交叉注意力组件CNeuronCrossDMHAttention作为基类,该组件已经内置全部所需机制。我们只需要将原前馈模块,替换成对CNeuronTimeMoESparseExperts的调用。全新CNeuronTimeMoEAttention对象的结构见下文。
class CNeuronTimeMoEAttention : public CNeuronCrossDMHAttention { protected: //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override { return feedForward(NeuronOCL); } virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override { return calcInputGradients(NeuronOCL); } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override { return updateInputWeights(NeuronOCL); } public: CNeuronTimeMoEAttention(void) {}; ~CNeuronTimeMoEAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint window_cross, uint units_cross, uint heads, uint layers, uint experts, uint experts_dimension, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTimeMoEAttention; } };
值得注意的是,CNeuronCrossDMHAttention对象基于动态内部架构的思路构建。这意味着该模块的核心组件在类结构中没有硬编码,而是在初始化阶段按需实时创建对应数量的组件。类结构内部仅维护一个动态数组,用来存放这些对象的指针。如此一来,我们可以直接按需重塑内部布局,而无需修改整套代码库:只需要调整 Init 方法内的逻辑即可。
其余方法要么直接继承自父类,要么仅需小幅修改。这种实现方式具备极高的灵活性与复用性,只需修改初始化参数,就能快速生成该模块的多种变体。这正是我们重点关注初始化方法的原因 —— 注意力模块(包含 MoE 模块)的内部结构,就是在这个方法中生成的。
bool CNeuronTimeMoEAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint window_cross, uint units_cross, uint heads, uint layers, uint experts, uint experts_dimension, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
在该方法体内,我们首先调用基神经层的同名初始化方法。以此搭建模块的骨架。接下来就是核心逻辑。我们准备内部层数组 cLayers,并将其绑定至 OpenCL 接口。
cLayers.Clear(); cLayers.SetOpenCL(OpenCL); CNeuronRelativeSelfAttention *attention = NULL; CNeuronRelativeCrossAttention *cross = NULL; CNeuronTimeMoESparseExperts *MoE = NULL; bool use_self = units_count > 0; int layer = 0;
紧接着,定义所需局部变量。准备阶段到此完成。
随后,循环遍历指定的内部层数(layers),逐步构建完整处理链路。若主信息流包含多于一个词元,则先创建并初始化自注意力模块,将其加入 cLayers 数组。解码器借此可以先读取已经生成的词元,之后再去读取外部上下文信息。
for(uint i = 0; i < layers; i++) { if(use_self) { attention = new CNeuronRelativeSelfAttention(); if(!attention || !attention.Init(0, layer, OpenCL, window, window_key, units_count, heads, optimization, iBatch) || !cLayers.Add(attention) ) { delete attention; return false; } layer++; }
下一步创建交叉注意力模块,该模块将上一层输出与上下文信息融合。实时传入窗口参数与注意力头数量。该模块会立即就绪,准备计算查询、键、值(QKV)。循环内部,所有对象指针存入层数组,就如同用独立节点拼装单一处理链。
cross = new CNeuronRelativeCrossAttention(); if(!cross || !cross.Init(0, layer, OpenCL, window, window_key, units_count, heads, window_cross, units_cross, optimization, iBatch) || !cLayers.Add(cross) ) { delete cross; return false; } layer++;
而最核心的部分紧跟在交叉注意力之后:实例化 CNeuronTimeMoESparseExperts。在这里配置 MoE 模块:设置专家数量、专家投影维度(即每个专家处理的特征数量)以及 topK 参数,topK 用于控制每次前向计算激活多少个专家。当然,同时完成与 OpenCL 上下文的绑定。
MoE = new CNeuronTimeMoESparseExperts(); if(!MoE || !MoE.Init(0, layer, OpenCL, window, experts_dimension, units_count, 1, experts, topK, optimization, iBatch) || !cLayers.Add(MoE) ) { delete MoE; return false; } layer++; }
全部层的初始化循环结束后,把最后一层的结果缓冲区绑定到本对象的外部接口。
SetOutput(MoE.getOutput(), true); SetGradient(MoE.getGradient(), true); //--- return true; }
最终,Init 方法把一个空容器实例转化为一套完整可运行的结构,由三类层按固定顺序组合而成;整个过程完全不用修改底层注意力机制。全部动态能力仅靠新增组件、参数配置实现,是典型的轻量化、易维护的灵活架构。
但这里存在一个问题:交叉注意力模块需要两路信息流:主信息流与上下文信息流。而 Time-MoE 框架本身只提供一路信息流。该问题通过最小改动底层机制来解决。我们直接重写前向传播与反向传播方法,让方法仅接收一路输入信息流,再把这条信息分流为两路。
bool CNeuronTimeMoEAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; return CNeuronCrossDMHAttention::feedForward(NeuronOCL, NeuronOCL.getOutput()); }
初看,这个操作仿佛是把交叉注意力变成了自注意力。但我们的设计思路是:主信息流仅传入最新时间步的词元。实现方式是缩减主信息流中参与计算的数据量。而上下文信息流传入全部历史信息,让主信息流内的词元,可以借助完整历史上下文完成特征增强。依托这种设计,Decoder-Only 架构下 Time-MoE 的全部优势得以保留:每一个新词元,都会向对应专家获取其特征子空间内的预测信息,同时参考全部历史市场动态。
该类所有方法的完整源代码已附在文章附件中。
模型架构
现在所有必要组件均已实现,我们终于开始构建可训练模型的完整架构。和前文的研究一样,本项目仍然基于 行动者 - 调度器 - 评价器(Actor–Director–Critic) 框架,该框架在强化学习任务中已被验证有效。我们在该框架内部集成所有和 Time-MoE 相关的自研模块,将其嵌入 环境状态编码器—— 该模型模块负责为输入数据生成语义丰富的表征。
但在这一阶段出现了一个关键问题。我们所采用的整体 管道 仅有一个模型输出,这是一个通用特征向量,供 Actor、Director 和 Critic 全部组件使用。与此同时,原文的 Time-MoE 架构包含若干预测头,每个预测头对应不同的预测周期。这就带来了潜在冲突。将全部输出合并到单一缓冲区会提升训练难度,同时降低可解释性。
我们决定不把所有内容合并为单一结构。而是在编码器和预测头之间实现清晰的逻辑隔离。编码器(Time-MoE)作为通用特征提取器,是供所有预测头共享的单一组件。随后,针对每一个预测周期,单独创建一个模型,该模型接收编码器的输出作为输入,并完成对应任务的预测。
该方案具备切实优势:我们保留了单一可训练单元用于信息提取,同时保证预测层的独立性与灵活性。每个预测头各司其职,对应各自的时间尺度,能够适配相应的市场波动特性。最终整套架构保持模块化与可扩展特性,同时对时序上下文结构具备很强的自适应能力。
在确定框架整体逻辑之后,我们着手设计可训练模型的架构,其中各个子系统均按照统一规则搭建。这保证了结构清晰,并且能够根据交易核心的需求灵活扩展模型规模。
全部初始化工作在 CreateDescriptions方法中完成,该方法从编码器开始逐层组装网络架构。
bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&forecast1, CArrayObj *&forecast2, CArrayObj *&forecast3, CArrayObj *&actor, CArrayObj *&director, CArrayObj *&critic ) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!forecast1) { forecast1 = new CArrayObj(); if(!forecast1) return false; } if(!forecast2) { forecast2 = new CArrayObj(); if(!forecast2) return false; } if(!forecast3) { forecast3 = new CArrayObj(); if(!forecast3) return false; } if(!actor) { actor = new CArrayObj(); if(!actor) return false; } if(!director) { director = new CArrayObj(); if(!director) return false; } if(!critic) { critic = new CArrayObj(); if(!critic) return false; }
原始历史行情数据被送入编码器。第一层仅仅是一个基础对象,本质上用作读取数据的接口。接下来是带噪声的批量归一化层,它可以稳定输入值的分布,实现一定程度的数据增强,并有助于防止模型过拟合。
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; uint prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormWithNoise; descr.count = prev_count; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
随后使用拼接差分(ConcatDiff)层。该层将每一根 K 线拆解为各个组成分量,新增差分通道,为后续更复杂的处理准备数据结构。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatDiff; prev_count = descr.count = HistoryBars; descr.layers = BarDescr; descr.step = 1; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
在这之后,数据送入 Mamba4CastEmbedding 模块。正是在这一层,输入序列同时融合来自多个时间尺度的标记。该机制能够得到输入信号的多频表征。该层输出固定长度的隐窗口(NSkills),随后经过转置层,将张量维度调整为适合后续独立单变量通道序列处理的形式。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defMamba4CastEmbeding; prev_count = descr.count = HistoryBars; descr.window = 2 * BarDescr; uint prev_out = descr.window_out = NSkills; { uint temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_out = descr.count;
接下来是 SwiGLU 嵌入层。它提升特征深度,构建状态抽象表征的初始基底。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSwiGLUOCL; descr.count = Segments; descr.window = (prev_out + Segments - 1) / Segments; descr.variables = prev_count; prev_out = descr.window_out = EmbeddingSize; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_count = descr.count; uint prev_var = descr.variables;
之后使用 TransposeRCDOCL 转置层,用于重排时间步与待分析通道的维度。该操作对我们所构建注意力模块的正常运行至关重要。正是依靠这一步操作,所有待分析变量最新时间步的词元会被归集至缓冲区起始位置。
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeRCDOCL; descr.count = prev_var; descr.window = prev_count; descr.step = prev_out; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out * prev_var; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
紧随其后的是批量归一化层,在数据送入注意力模块之前稳定数据分布。
现在我们来到核心模块 —— TimeMoEAttention(时间混合专家注意力模块)。这是一个多头注意力模块,包含大量专家,每个专家专注于时序信息的不同方面。模块接收紧凑词元作为输入,这些词元代表当前状态以及取自全部历史的上下文,词元以此提取数据中的信息含义。输入维度与专家数量 NExperts 通过参数指定,每一个注意力头都会选取 Top-K(前 K 个)专家,形成紧凑且表现力强的隐表征。该层是编码器的最后一层,定义后续所有预测与决策模型使用的输出空间。
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTimeMoEAttention; descr.window_out = EmbeddingSize / 4; { uint temp[] = {prev_out, prev_out, 8, TopK}; //Window Main, Window Cross, Experts dimension, TopK if(ArrayCopy(descr.windows, temp) < ArraySize(temp)) return false; } { uint temp[] = {prev_var, prev_var * prev_count, NExperts}; //Units Main, Units Cross, Experts if(ArrayCopy(descr.units, temp) < ArraySize(temp)) return false; } descr.layers = 6; descr.step = 4; // Attention heads descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
编码器输出端会生成一份共享隐数组,可供全部模型访问。第一个预测模型(forecast1)直接读取该数组。基础输入层接收张量。随后进行简单卷积处理,为每一个待分析通道生成指定预测周期的预测结果。在这个模型中,仅预测下一个数值。
//--- CLayerDescription *latent = descr; //--- Forecast 1 forecast1.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = int(latent.windows[0] * latent.units[0]); descr.activation = None; descr.optimization = ADAM; if(!forecast1.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = latent.units[0]; descr.window = latent.windows[0]; descr.step = descr.window; descr.layers = 1; descr.window_out = 1; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = TANH; if(!forecast1.Add(descr)) { delete descr; return false; }
这里有一项重要技术细节需要重点关注。在环境状态编码器的数据预处理阶段,我们有意扩展原始特征空间。这种扩展对于提取高层特征、生成表现力更强的隐表征是必要的。但该变换会造成预测值与训练阶段使用的真实数据之间出现维度不匹配问题。
为保证预测结果可以和真实值正确对齐,预测输出必须映射回原始空间的尺度与结构。在本方案中,我们使用简单全连接层实现该目标,将数据维度降至指定值 BarDescr。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = BarDescr; descr.activation = TANH; descr.optimization = ADAM; if(!forecast1.Add(descr)) { delete descr; return false; }
第二个需要单独说明的要点:恢复归一化过程丢失的原始数据分布。此前我们使用 RevIN 反归一化层完成这项工作,该层能够自动从编码器的对应层提取归一化参数。但当前实现存在架构限制:归一化层保存在环境编码器内部,预测模型无法直接访问。换句话说,RevIN 在这里无法工作 —— 无法获取批量统计信息。
乍看之下解决方案似乎有点矛盾:我们再次使用批量归一化层,但不再用于归一化,而是用来 恢复原始尺度。没错,听起来像文字游戏,但在工程实践中这是完全可行的技术手段。
原理在于 BatchNorm(批量归一化)架构包含两个输出参数:缩放参数(scale)与偏移参数(bias),二者均可设置为可训练参数。传统场景中它们用于稳定并加速训练,而在我们这里承担另一种作用 —— 学习逆变换,让预测值贴近原始分布。因此模型不再存储归一化统计量,而是自行学习如何对数据反归一化,适配真实市场环境。
该方案设计巧妙:保持模型结构紧凑,避免状态缓冲区不必要的复杂化;最重要的是不会破坏计算图,完全兼容现有的反向传播逻辑。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNorm; descr.count = BarDescr; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!forecast1.Add(descr)) { delete descr; return false; }
整套设计追求最高效率:固定窗口、单层网络、定长输出。
第二个和第三个预测模型(forecast2, forecast3)使用与第一个模型相同的输入数据。它们几乎完全复用前者架构,区别仅在于预测窗口更长。预测卷积层的参数从第一个预测头复制而来,滤波器数量扩展至目标预测周期所需规模。
//--- Forecast 2 forecast2.Clear(); //--- Input layer if(!forecast2.Add(forecast1.At(0))) return false; //--- layer 1 if(!(descr = new CLayerDescription())) return false; if(!descr.Copy(forecast1.At(1))) { delete descr; return false; } prev_out = descr.window_out = NForecast / 2; if(!forecast2.Add(descr)) { delete descr; return false; } prev_count = descr.count;
另一个具有技术重要性的点是预测模块的架构。卷积层输出端,模型生成各个独立单变量时间序列的预测值,每一条序列反映某个特定指标或特征的局部动态。这让预测结果具备空间稀疏特性,但信息丰富。
这就引出一个问题:如何聚合这些预测结果,并转换为可以和原始数据对比的形式?原则上可以使用经典全连接层,它完全具备信息聚合能力。但随着预测周期增加,输出序列长度随之变长,这种实现方式效率会下降。参数量会爆发式增长,同时局部时序依赖关系容易丢失。
为规避这个问题,我们先对张量做转置、重排坐标轴,将时间步转为独立通道。这样后续卷积层就可以分别作用于每一个时间切片。换句话说,不再将整条序列作为整体处理,而是让模型基于特征维度分析每一个时间点。该操作带来双重收益:降低输出表征的维度(起到瓶颈层作用),同时保留各个时间步之间的时序连贯性。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.window = prev_out; descr.count = prev_count; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!forecast2.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.window = prev_count; descr.step = prev_count; prev_count = descr.count = prev_out; descr.layers = 1; prev_out = descr.window_out = BarDescr; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = TANH; if(!forecast2.Add(descr)) { delete descr; return false; }
由此,我们实现了一套高效的聚合机制,并且不会丢失预测结果的精细结构。当在高多重性数据上训练模型、以及使用多图特征时,该特性尤为宝贵;这类场景下,每个时间切片都承载着特有信息,不能简单压缩为通用模式。
流程的最后执行一次最终批量归一化操作,用来恢复原始数据分布。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out; descr.activation = None; if(!forecast2.Add(descr)) { delete descr; return false; }
该模型作为长周期预测模型,使用同样的特征,但以更长的预测视野解读这些特征。
至于第三个预测模型,其架构和第二个模型完全一致。唯一区别是扩展后的预测周期,让模型可以对更远的未来进行推演。除此之外,它采用相同的卷积层与转置层,以及相同的维度匹配和归一化逻辑。因此为避免内容重复、干扰主线讲解,我们不再赘述其详细结构,转而聚焦决策模型。
接下来是 Actor 模型,也就是策略决策模块。它将当前账户状态描述符作为输入。输入数据会经过标准归一化处理。
//--- Actor actor.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = AccountDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = AccountDescr; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
随后数据送入 CrossDMHAttention 模块。这是一种交叉注意力机制:主信息流为当前账户信息,上下文信息流取自编码器输出的隐特征。该设计使模型注意力机制能够结合交易者当前账户情况,识别市场状态中最相关的特征。模块内部堆叠三层注意力层,每层包含多个注意力头。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; { uint temp[] = {AccountDescr, // Inputs window latent.windows[0] // Cross window }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {1, // Inputs units latent.units[0] // Cross units }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.step = 4; // Heads descr.window_out = 32; descr.batch = 1e4; descr.layers = 3; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
注意力模块之后串联三层全连接层,将输出数据转换为动作概率(NActions)。生成交易决策时,使用的就是这部分输出。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.batch = BatchSize; descr.activation = TANH; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SoftPlus; descr.batch = BatchSize; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions; descr.activation = SIGMOID; descr.batch = BatchSize; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
至此,整套架构按顺序、有逻辑、模块化搭建:从历史数据预处理,经过 Transformer 机制与多头注意力,再到预测与决策模块。这种方案保证了模型的透明性、可靠性与充足灵活性 —— 对于实盘交易场景尤为关键,这类架构不能出现碎片化问题。全部模块锚定在同一个公共隐空间内,各个模型在各自上下文内运行,同时维持全局一致性。
Critic 与 Director 模型的架构和 Actor 类似。唯一差别是输入不再是账户状态,而是由 Actor 自身生成的动作向量。输出是对这些动作的数值评估:Critic 输出价值函数,而 Director 输出来自二分类(好/坏动作)的训练信号。内部结构包含同样的归一化模块、交叉注意力机制以及级联输出层。为避免重复赘述已介绍过的细节,这里不再展开。所有组件的完整架构见附件。
模型训练
本研究下一步就是模型训练。这里会遇到一项核心难点。原始 Time-MoE 框架的作者使用规模极其庞大的 Time-300B 数据集训练神经网络。该数据集涵盖九大领域超过 3000 亿个时间点,领域包括经济、能源、交通、医疗等。海量数据赋予模型极强的泛化能力,但同时需要本地或半自动化训练环境无法提供的算力资源。
在本地复现这样的数据集并不现实。但如果仅仅依靠在线方式,使用实时流入数据做训练,系统将无法学习稳定规律。在市场高波动、观测窗口有限的环境下,该问题会更加突出。
因此我们采用折中方案:放弃预先收集静态训练数据集,转而逐步累积并使用内部状态缓冲区,以此管控训练流程。这里复用了此前在 TimeFound 模型中验证过的机制;该机制效果可靠,本次不仅用于训练预测模型,还用于训练决策模型。
该机制本质是在伪真实数据上训练智能体。行情历史数据取自交易终端,动作评估则在仿真环境内完成。整套流程在 EA"…\Experts\TimeMoE\Study.mq5" 的 Train 方法内部实现。在这里开始逐段处理历史数据、构建训练批次、采集交易账户上下文,并利用反向传播依次训练模型。
第一阶段,该方法确定训练的时间边界:计算训练所用历史区间的起始索引与结束索引。
void Train(void) { int start = iBarShift(Symb.Name(), TimeFrame, Start); int end = iBarShift(Symb.Name(), TimeFrame, End); int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);
接下来加载价格序列,并初始化主要技术指标。系统会逐一检查指标就绪状态;若指标尚未完成计算,系统会短暂等待、间歇暂停。这一步是稳定缓冲区初始化所必需的,否则后续的数据处理将失去意义。
if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } //--- int count = -1; bool calculated = false; do { count++; calculated = (RSI.BarsCalculated() >= bars && CCI.BarsCalculated() >= bars && ATR.BarsCalculated() >= bars && MACD.BarsCalculated() >= bars ); Sleep(100); count++; } while(!calculated && count < 100); if(!calculated) { PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__); ExpertRemove(); return; } RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); //--- if(!ArraySetAsSeries(Rates, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } bars -= end + HistoryBars + NForecast; if(bars < 0) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; }
指标一旦就绪,就开始采集用于训练的样本。我们搭建训练循环,在每一次迭代中,从有效的 K 线区间内随机选取一个位置。这个位置就是下一组训练场景的入口。基于该位置,会生成三个核心向量:
- bState —— 以指标与价格参数构成张量,描述市场状态;
- bTime —— 反映时间上下文信息;
- Result —— 包含基于未来价格走势构建的预测目标值。
vector<float> result, target, neg_target; bool Stop = false; //--- uint ticks = GetTickCount(); //--- for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars); if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime), Result)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; }
除市场状态之外,程序还会生成一个伪账户状态,用来模拟交易者在当前阶段的交易环境。这部分逻辑在SampleAccount函数中实现,该函数生成特征向量,包含账户余额、净值、持仓规模、累计盈亏、风险止盈比、风险止损比,以及用于模拟隐藏季节性规律的正弦信号。
const vector<float> account = SampleAccount(GetPointer(bState), datetime(bTime[0])); if(!bAccount.AssignArray(account)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; }
该向量作为智能体的输入,拓宽模型对当前局面的理解:智能体不仅依据市场行情做决策,同时结合自身持仓状态。
输入数据准备完成后,模型执行前向传播:编码器对市场信息进行编码,之后三组Forecast[i] 模块分别生成不同时间周期的预测结果。
//--- Feed Forward if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } for(uint f = 0; f < caForecast.Size(); f++) if(!caForecast[f].feedForward(GetPointer(cEncoder), -1, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f); Stop = true; break; }
与此同时,Actor 生成交易决策,该决策会立刻交由两个独立模型评估:Critic 和Director。评价器基于经典价值评估方法分析决策;指导器则作为二分类器提供强反馈,区分有效动作与明显错误的动作。
if(!cActor.feedForward(GetPointer(bAccount), 1, false, GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cCritic.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cDirector.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
接下来调用反向传播方法,对模型参数进行优化。预先准备好的后续环境状态张量,用于训练各个预测模型。
//--- Study for(uint f = 0; f < caForecast.Size(); f++) if(!caForecast[f].backProp(Result, (CBufferFloat*)NULL) || !cEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f); Stop = true; break; }
使用CheckAction函数获取动作的客观评估结果。该函数模拟开立虚拟持仓,结合基于真实历史数据的折扣因子计算预期收益。基于上述数据生成奖励值,回传给模型,并以此为依据更新全部组件的参数:Actor、Critic和Director。
cActor.getResults(Action); double equity = bAccount[2] * bAccount[0] * EtalonBalance / (1 + bAccount[1]); double reward = CheckAction(Action, Result, equity); Result.Clear(); if(!Result.Add(float(reward))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) || !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Result.Update(0, float(reward > 0))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cDirector.backProp(Result, GetPointer(cEncoder), LatentLayer) || !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
梯度校正快速完成,单次更新即可执行。误差回传到网络内部,权重根据训练信号更新。模型由此逐步学习有效规律,根据新样本调整行为,学会规避错误、强化有效动作。
为实时可视化监控训练过程,代码会定期在图表注释中输出关键训练指标:预测误差、评价器准确率。
if(GetTickCount() - ticks > 500) { double percent = double(iter) * 100.0 / (Iterations); string str = ""; for(uint f = 0; f < caForecast.Size(); f++) str += StringFormat("%-12s%d %6.2f%% -> Error %15.8f\n", "Forecast", f, percent, caForecast[f].getRecentAverageError()); str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Critic", percent, cCritic.getRecentAverageError()); str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Director", percent, cDirector.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } }
完成指定迭代次数后,该方法会将训练结果输出至终端日志,并调用ExpertRemove干净退出程序。
Comment(""); //--- for(uint f = 0; f < caForecast.Size(); f++) PrintFormat("%s -> %d -> %-15s%d %10.7f", __FUNCTION__, __LINE__, "Forecast", f, caForecast[f].getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic", cCritic.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Director", cDirector.getRecentAverageError()); ExpertRemove(); //--- }
因此,每一轮训练都是一连串小型仿真,每一段仿真都会复现一段历史行情:从市场状态一直到交易结果。智能体通过直接经验学习,在失败与成功的交易样本中迭代,借助均衡评估体系与定向修正机制,逐步适应各类市场环境、优化自身策略。
主训练阶段结束后,模型进入微调阶段,采用在线学习模式执行。该阶段使用 MetaTrader5 内置策略测试器,可在尽可能贴近真实市场的环境下运行算法,同时仿真过程完全可控。该方案不仅能够评估模型效果,还能让模型适配当前市场环境,依据当下波动率与价格运行特征持续调参。
这套在线学习机制沿用前期研究的代码,未做修改。模型持续从新数据学习,实时调整交易动作;当市场状态频繁切换时,这一点尤为重要。
测试
模型训练流程分为两个阶段。该方案能够稳定、可靠、循序渐进地搭建整套系统。
首先是离线训练。我们采用 EURUSD 一分钟周期的 15 年历史数据。海量数据为模型提供丰富多样的市场场景。Encoder编码器学习识别行情规律、挖掘关键模式,并将市场状态编码为紧凑且信息丰富的特征向量。该向量是智能体所有决策的基础。训练过程中,Actor接收Critic和Director的反馈信号,学习行为策略。
随后进入在线微调阶段。在 MetaTrader5 策略测试器内运行。模型以逐 K 线的写实方式与历史数据交互,包含市场噪声、随机波动与不稳定特征。这有助于让智能体行为贴近真实行情动态,在近似实盘的条件下优化策略。
训练完成后,使用全新数据对模型进行测试:采用 2025 年 1 月的行情报价。所有参数提前固定,全程不再改动。以此保证评估客观透明。测试结果如下所示。

测试结果好坏参半,解读时必须谨慎。一方面,累计收益这项传统核心指标表现亮眼:初始资金 100 美元,测试周期结束后,系统实现净利润 1209 美元。收益为本金的 12 倍。资金曲线在月中之前稳定增长,之后在 1350–1400 美元区间相对企稳。
但与此同时,极端回撤问题十分突出。资金最大回撤超过 72%,净值最大回撤超过 87%。这意味着在交易最艰难的阶段,系统会亏掉绝大部分本金。即便后续资金能够修复,也说明这套行为策略属于高风险类型。
盈利因子(Profit Factor)为 1.49,很多人会认为该数值尚可;但在如此大的回撤面前,该水平很难覆盖潜在的持续亏损阶段。恢复因子(Recovery Factor)(净利润与最大回撤之比)接近 1,代表亏损后的资金修复速度非常缓慢。
交易统计显示,该月一共开仓近 2500 笔交易,胜率 53.98%。平均盈利仅略大于平均亏损。
总体来看,上述结果证明模型能够识别盈利信号、实现账户增值,但代价是承受很深的回撤,以及长时间的连续亏损。若要投入实盘交易,该策略需要额外风控保护机制,降低账户出现深度回撤的风险。
结论
本研究从学术论文中提取Time‑MoE框架核心思想,逐步将其实现为MQL5与OpenCL可运行代码。我们实现了SwiGLU嵌入层,搭建稀疏混合专家模块,并将其集成至交叉注意力机制。基于Actor–Director–Critic架构搭建了完整训练流水线。得益于清晰的模块化设计,所有组件相互关联,同时便于参数配置与功能扩展。
基于 15 年历史数据开展的第一阶段离线训练,让编码器(Encoder)构建出信息丰富的市场隐空间表征;在 Critic 和 Director 的监督下,Actor 掌握基础交易策略。第二阶段在 MetaTrader5 策略测试器中进行在线微调,把现代市场的价格波动与噪声特征融入模型参数,使模型具备交易能力。
采用 2025 年 1 月行情进行测试的结果表明:智能体(Agent)可以稳定生成盈利,但同时伴随深度回撤。这说明模型仍需进一步优化风控体系,并精细化调整交易入场出场条件。
Links
文中使用的程序
| # | 名称 | 类型 | 说明 |
|---|---|---|---|
| 1 | Study.mq5 | EA | 用于离线模型训练的EA |
| 2 | StudyOnline.mq5 | EA | 用于在线模型训练的EA |
| 3 | Test.mq5 | EA | 模型测试EA |
| 4 | Trajectory.mqh | 类库 | 描述系统状态与模型架构的结构体 |
| 5 | NeuroNet.mqh | 类库 | 用于搭建神经网络的类库 |
| 6 | NeuroNet.cl | 库文件 | OpenCL程序的代码库文件 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/18548
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:智能预测流程(稀疏混合专家)
新手在交易中的10个基本错误
构建交易系统(第 1 部分):量化方法
感谢您分享代码。我关注您的文章已经有一段时间了。这些文章似乎都是基于最新的学术论文撰写的。 但我编译时遇到了问题,所有代码都出现了过多的语法错误。其中许多错误源于“NeuroNet.cl”文件,如果我没记错的话,这是“NeuroNet.mqh”中OpenCL计算的核心部分。 我相信您那边的所有代码都已成功编译。因此我推测问题可能出在我这边的 MQL 版本上,它可能与您的版本不同。您能否就此提供一些详细信息?谢谢!
[该]交易仅做多[。]
就我个人而言,对于像标普500指数这样上涨趋势更为稳定的标的,纯多头算法可能更合适。
当然,欧元兑美元只是本文中相关概念和运作原理的一个示例,所以……