交易中的神经网络:跨域时间序列预测框架(结论)
引言
过去数十年间,金融交易发生了根本性变革,演变为一个高科技且高度纪律化的领域。过去交易者的成败很大程度取决于直觉、经验与个人观察;而如今,能否快速高效处理海量数据、生成精准预测、依托高级算法做出决策,已经成为核心能力。随着算力提升与现代机器学习方法的出现,交易者与研究者拥有了全新工具,可以在看似混乱的市场数据当中识别模式与趋势。尽管已经取得长足进步,但金融时间序列的中短期预测依旧是一项极具挑战性的任务。
根源在于金融市场本身独特且复杂的特性。金融市场属于高噪声系统:真实信号往往被大量随机波动与数据伪影所掩盖。市场数据具备非平稳性:统计属性会随时间发生改变,体现为市场状态切换,从平静行情切换至高波动行情,并且反复交替。除此之外,市场还会遭遇突发事件冲击,例如新闻、危机、监管干预,这些事件会打破正常价格运行模式。以上种种因素,使得传统时间序列模型存在固有的局限性。
经典统计方法在分析平稳过程时效果良好,但应用于现代市场数据时效果有限。而神经网络,包括长短期记忆网络(LSTM)与门控循环单元(GRU),作为建模复杂时序依赖关系更灵活的工具,已经得到广泛使用。然而,这类模型同样需要大量训练数据;迁移到新的交易品种或者全新市场环境时,模型精度与稳定性经常发生衰减。原因在于大部分模型高度绑定特定的数据结构与样本模式,不具备普适性。
由此引出一项核心难题:如何构建一套无需额外训练就具备泛化与自适应能力的模型,也就是零样本预测(Zero‑Shot Forecasting)。这类模型在大量来自不同领域的多样化时间序列上完成预训练后,依靠内部学习得到的表征,能够在完全未见过的全新数据上识别相似的模式与规律。这从根本上改变预测范式:模型不再是单一市场的专属专家,而是一名可以跨任务迁移知识的通用分析师。
我们在前一篇文章中开始介绍的TimeFound框架,正是专门围绕上述理念所开发。TimeFound是一款功能强大、通用性强的跨域时间序列模型,基于Transformer架构构建。Transformer最初是为自然语言处理任务提出的一种革命性方法,因其能够高效捕捉序列中的长距离依赖关系,现已得到广泛应用。在TimeFound中,该能力被用于金融时间序列分析,并且新增一项关键组件:多分辨率分块(Multi‑Resolution Patching)机制。
多分辨率分块的理念是将原始时间序列切分为多个块(patch),各块长度各不相同,用以表征不同时间尺度。例如,短分块捕捉局部快速波动,而长分块则捕捉变化更缓慢的全局趋势。这种多层级设计让模型可以同时兼顾多层信息。对于金融数据而言,这一点至关重要,因为市场信号会在多个不同时间周期上同时显现。
从技术架构上看,TimeFound由一组相互独立的投影模块构成,每一组对应一种特定尺度的分块。每个投影模块是带残差连接的双层小型感知机。模块输入为分块数据以及有效点位的二进制掩码,借此可以有效忽略填充位以及人为补全的缺失值。依靠独立投影模块与分块重复机制的特性,模型可以区分不同时间尺度各自的贡献,使其对相位偏移、输入序列长度变化以及数据缺失都具备鲁棒性。
为训练TimeFound,研究人员收集并精心预处理了海量跨领域时间序列数据集,涵盖宏观经济指标、市场报价、各类物理过程等。以此赋予模型泛化能力,使其在从未见过的全新数据上依旧拥有良好表现。
该框架的自定义可视化示意图如下。

在前一篇文章的实践部分,我们详细讲解了多尺度分块模块的实现,它是TimeFound框架数据预处理模块的核心组件。该模块负责高效生成信息token:即对时间序列各个片段做紧凑且信息丰富的表征,时序片段被切分为不同长度、不同尺度的分块。正是依靠这套机制,模型在处理数据时,能够同时兼顾局部市场特征与全局市场特征。
今天,我们继续之前的开发工作。
TimeFound编码器‑解码器结构
分块阶段输出的token张量送入Transformer模块做后续计算分析。智能数据处理在此处开启:挖掘隐藏模式、预测行情动态、生成交易信号。
TimeFound框架原文作者在此处采用经典的编码器‑解码器架构,该架构广泛应用于机器翻译与序列分析模型。然而,针对时间序列场景,该结构经过适配改造,增加多项扩展设计以适配金融数据的固有特性。
编码器使用双向自注意力(Self‑Attention)。也就是说,每个token都会结合其余全部token做分析,沿着时间轴同时向前和向后读取上下文。因此,模型不仅可以获取当前时刻之前发生的信息,还能够捕捉与后续事件相关的潜在依赖关系。该特性对于分析已经完整的历史数据片段尤为适用,能够挖掘更深层次的内在关联。
另一方面,解码器会刻意限制可见范围,每个token仅仅可见时间上排在它前面的内容。解码器的核心组件是交叉注意力(Cross-Attention)模块。这里,编码器输出的数据作为上下文,解码器将输入token与该上下文信息做关联计算,以此输出精度更高的预测结果。
编码器与解码器之间的任务分工,配合注意力机制,令TimeFound架构拥有极高灵活性。模型不仅可以响应近期价格波动,还能够在广阔的历史上下文下分析资产行为,识别市场模式,区分普通噪声与有效信号。
我们的代码库已经实现多种版本的自注意力与交叉注意力模块:从基础版本,到带掩码、可动态选择注意力头的高级版本。在实现TimeFound模型时,我们自然会复用这些已有的代码成果。然而,这里有一处概念上的关键细节需要注意:框架原文作者采用自回归方式生成预测结果。
模型并不会一步输出完整周期的预测结果,而是迭代式运行。在每一轮迭代中,Transformer将历史数据(代表多尺度市场模式的token张量)作为输入,生成对应下一个片段的单个token,该token代表预测状态。将生成结果追加至历史数据末尾,循环重复上述流程。因此,在目标预测区间上一步步构建完整预测序列。
而在我们的实例中,目标并不是做远期预测。也就是说,我们并不打算对很远的未来进行预测,而是搭建一套系统化头寸管理算法。模型每一根新K线到来时,基于真实已确定的行情数据重新计算市场状态,输出交易决策:持仓、平仓、反手、加仓。当然,执行交易时依然会对未来一段时间做出预测,但每一根新K线都会根据最新现实行情修正预测。
即便如此,自回归本身会带来特定的架构约束。每一步迭代,编码器和解码器都必须输入同一套历史数据。编码器基于全部历史数据做分析,提取通用模式;解码器使用同一段历史序列里的当前状态,结合编码器挖掘出的历史依赖关系完成分析,输出预测值。由此形成两套并行的数据流,处理同一批输入数据。
为实现二者同步,我们创建CNeuronTimeFoundTransformerUnit对象。该对象承担信息流转的统一调度工作。以此保证架构整洁,便于后续功能扩展或者组件替换。该新对象的结构如下。
class CNeuronTimeFoundTransformerUnit : public CNeuronCrossDMHAttention { protected: CNeuronMVMHAttentionMLKV cSelfAttention; //--- 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: CNeuronTimeFoundTransformerUnit(void) {}; ~CNeuronTimeFoundTransformerUnit(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint heads_kv, uint units_count, uint layers, uint layers_to_one_kv, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTimeFoundTransformerUnit; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; };
在这个新类给出的结构中,我们只能看到一个内部对象:自注意力模块,它充当待构建模型的编码器。解码器的功能则由父类实现,该父类本身就是一个交叉注意力模块。
然而,表面看上去实现简洁,背后却是一套精心设计的算法。编码器的实际工作由独立通道分析组件CNeuronMVMHAttentionMLKV完成。它的核心特性是能够对每一个单变量序列,也就是每一个交易变量或技术指标,做相互独立的分析。举个例子,如果输入价格、成交量、波动率数据,CNeuronMVMHAttentionMLKV会在各个时序内部建立因果关联,避免在早期阶段混合不同的信号。该特性最大程度降低不同信号之间带来的干扰噪声,让模型更好理解每一路数据源本身的结构,大幅提升输出结果的可解释性。
多层键‑值(MLKV)机制进一步提升计算效率,这是用于降低Transformer计算复杂度的一种非常巧妙的优化思路。在传统实现中,每一层自注意力都拥有属于自己的键(Key)与值(Value)张量。而本实现中,键、值张量会被保存,并在多个网络层之间复用;查询向量(Query)则在每一层持续迭代优化。该方案减轻计算开销,同时实现表征的渐进式精炼,好比反复阅读同一篇文章,每次都能发掘出新细节。这一点在交易场景尤为珍贵:模型第一轮识别局部模式,后续逐层进一步提炼出更加宏观的市场结构。
因此,在本实现中,编码器承担双重作用:
- 一方面,典型双向自注意力(Bi‑Directional Self‑Attention)沿着时间轴双向识别token之间的关联关系。
- 另一方面,对各个单变量序列做模块化解析,实现对数据结构的多层级理解。
现在,轮到解码器部分。在本章节中,我们使用CNeuronCrossDMHAttention模块,该模块实现了多样化多头交叉注意力,并且具备多项架构层面的优势。
首先,遵循标准Transformer架构,解码器包含两类注意力模块:
- 自注意力 —— 分析主数据流内部各token之间的关联性;
- 交叉注意力 —— 利用编码器输出的上下文信息,对上述token做信息增强。
两类注意力均采用相对位置编码模块。也就是说,注意力模块不只读取token的绝对位置,还会计算token与其他token的相对位置。另外,该注意力机制还引入三种偏置项:
- 内容相关位置偏置;
- 全局上下文偏置;
- 全局位置偏置。
这让模型不仅可以精准考量事件之间的时间距离,还能捕捉事件之间的定性关联关系,这对于时序行情数据的分析至关重要。
注意力模块之后是多头前馈网络(FeedForward)块。它的作用是在不同的特征子空间内完成独立的非线性变换处理,使其能够识别种类丰富的市场模式,同时不会丢失序列中重要的结构信息。
我们还需要留意架构组件之间一处关键差异:与编码器不同,解码器不再对单变量序列做隔离处理。编码器中各个指标变量相互独立分析,保留各自单变量序列;而在解码器中,全部token合并为一条统一数据流。至此,单变量序列之间的边界消失,模型将所有信息视作一个整体。
因此,解码器能够分析跨变量依赖关系,也就是各个变量单独分析时难以显现的内在关联。价格的变化,不只可以由价格自身历史解释,还可以由波动率、成交量,或是新闻舆情活跃度等因素共同解释。正是在交叉注意力模块中,当前状态token融合编码器编码得到的信息,挖掘出上述各类依赖关系。
最终得到一套能力强大、具备可解释性的架构:简洁精巧的编码器组件CNeuronMVMHAttentionMLKV,搭配基于CNeuronCrossDMHAttention、灵活自适应的解码器。该模型扩展性良好,适配各类市场数据,同时保持很高的计算效率。
编码器采用静态声明方式,因此类的构造函数与析构函数可以置空。继承而来以及自定义声明的组件,全部在Init方法中统一完成初始化。该方法提供一组常量参数,用于明确指定待创建对象的架构配置。
bool CNeuronTimeFoundTransformerUnit::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint heads_kv, uint units_count, uint layers, uint layers_to_one_kv, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronCrossDMHAttention::Init(numOutputs, myIndex, open_cl, window, window_key, variables, window, units_count * variables, heads, layers, optimization_type, batch)) return false; SetActivationFunction(None);
在该方法体内,我们首先调用父类的同名方法,父类已经实现了继承对象与接口的初始化逻辑。
这里,需要仔细说明传入父类方法的初始化参数。首先需要注意:我们正在创建的子类对象与父类不同,父类需要两路输入数据流:待分析数据与上下文数据。然而,本对象的输入只接收一路数据流,上下文由模块内部的编码器生成。
另外,输入数据为表征当前历史窗口的多模态token序列。每一条单变量序列由一组token表达,每个token对应单个分块,即时间序列的局部片段。借助这些分块,模型可以聚焦局部变化与模式,挖掘各个变量内部的短期依赖关系。
该对象的输出要求是:每条单变量序列仅输出一个token,分别代表对多模态时间序列下一个片段的编码预测结果。与此同时,Transformer架构本身会保证主信息流的维度保持不变。
综合以上特性,我们做如下设计:解码器的主通路只送入描述环境状态最后一个片段的token,每个单变量序列各提供一个。而全部历史信息,经过编码器挖掘内部依赖关系完成增强之后,通过额外的信息流通路传入。
父类方法执行成功后,继续完成编码器的初始化,此处传入待分析序列的总长度信息。
if(!cSelfAttention.Init(0, 0, OpenCL, window, window_key, heads, heads_kv, units_count, layers, layers_to_one_kv, variables, optimization, iBatch)) return false; cSelfAttention.SetActivationFunction(None); //--- return true; }
请注意,在我们的实现中,编码器与解码器均采用多层结构对象。两个模块配置的网络层数保持一致。
接下来,要开发该对象的前向传播算法。算法逻辑本身较为简单:只需要依次调用对应方法,先执行编码器,再执行解码器。然而,需要重点留意信息流的一些细节。在该方法的参数中,我们接收一个指向源数据对象的指针。传入的数据为三维张量,维度为[序列长度 × 变量数 × Token尺寸]。该数据排布格式恰好适配编码器,因此我们直接把该对象指针传递给编码器。
bool CNeuronTimeFoundTransformerUnit::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cSelfAttention.FeedForward(NeuronOCL)) return false;
按照之前的设计约定,解码器主信息流的输入,应当取代表最新市场状态片段的token。而我们的模型输入数据是按时间倒序存放:最新的数据位于最前面。因此我们需要的目标token就处在源数据缓冲区的起始位置。利用这一特性,省去拷贝对应数据片段的步骤;直接将源数据对象指针作为主信息流输入,编码器的输出结果则通过辅助信息流传入解码器。
if(!CNeuronCrossDMHAttention::feedForward(NeuronOCL, cSelfAttention.getOutput())) return false; //--- return true; }
接下来,我们结束该方法,首先将运算得到的布尔结果返回给调用方程序。
我建议您自行查阅反向传播相关方法的算法逻辑。CNeuronCrossDMHAttention类及其全部方法的完整源代码见附件。
模型架构
当组成模型结构组件所需的全部对象都创建完毕后,我们进入下一阶段:搭建完整一体化架构。需要着重说明的是,和我们之前的开发项目一样,本次构建的是一套运行在强化学习框架下的交易智能体。
在本任务场景中,TimeFound框架仅用于环境状态分析:完成市场数据预处理,并挖掘历史时间序列当中的隐藏模式。该组件以此身份集成至环境编码器架构,为模型其余模块输出结构化、经过清洗且具备可解释性的当前市场状态表征。
整个交易智能体内部采用Actor‑Director‑Critic(行动者–调度器–评估器)结构,每个组件承担明确划分的任务。在当前的实现中,系统由四个独立模型协同工作构成:
- 环境状态编码器(编码器)负责对输入市场数据做深度分析,挖掘时序数据中的稳定模式,生成内容丰富的当前交易上下文表征,并将该表征传递给Actor。
- Actor是智能体的核心模块,接收编码器输出的数据,生成具体的交易决策。
- Director与Critic二者属于评估子系统,借助内部未来模型对执行者输出的决策进行评估,预判行为可能带来的后果。另外,Director输出更为严格的二分类评判:该行为是否具备考量价值;而Critic输出期望奖励的量化指标。二者共同生成反馈信号,供智能体学习,使其能够适应不断变化的市场环境。
该方法确保系统的模块化与灵活性:每一个组件都可以单独迭代优化,或是适配特定类型的交易策略。
所有模型的架构都在CreateDescriptions方法中定义,该方法接收四个动态数组指针(每个模型对应一个数组),每个数组内存储专属对象序列,完整描述待构建模型的结构。
bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&actor, CArrayObj *&director, CArrayObj *&critic ) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) 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; }
让我们先从编码器开始。我们将包含时间序列历史数据的张量送入模型,并直接把数据传递至维度足够的全连接层。该层相当于模型的一个入口过滤层。我们将神经元数量设置为历史K线数量与单根K线特征描述符数量的乘积,以此完整保留价格走势、成交量以及其余各类指标的全部信息。
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
接下来是BatchNormWithNoise层。该层用于稳定训练过程,同时引入少量随机扰动,避免模型在优化过程中陷入局部极小值。这里实现噪声可控注入:在归一化计算期间加入随机波动,以此提升模型在现实噪声繁多的市场数据上的泛化能力。
//--- 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(拼接差分)模块补充数据动态变化信息。该层沿着时间轴计算一阶差分,并将差分结果作为新通道拼接到原始数据中,使模型能够更好捕捉各项指标的变化速率。
//--- 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模块,在此为每一根历史K线加入时间编码。我们设置两种周期的谐波:小时周期与日周期,让模型能够捕捉日线模式与日内波动。之后,每根K线对应的嵌入向量合并,形成完整的整体表征。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defMamba4CastEmbeding; prev_count = descr.count = HistoryBars; descr.window = 2 * BarDescr; int prev_out = descr.window_out = NSkills; { int 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; }
完成嵌入向量后,我们进入TimeFoundPatching模块。该模块将输出张量切分为预先设定数量、尺寸相等的片段(分块),并将所有分块整理为一个完整矩阵。每个分块都包含局部行情动态信息,全部分块共同作为Transformer块的输入。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTimeFoundPatching; descr.count = prev_count; prev_count=descr.window = Segments; descr.variables = prev_out; descr.window_out = EmbeddingSize; descr.step = 8; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
现在到了我们的核心层:TimeFoundTransformerUnit。该对象使模型既可以对每一条单变量序列做独立分析,又能够挖掘跨变量之间的依赖关系。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTimeFoundTransformerUnit; descr.count = prev_count; descr.window = EmbeddingSize; descr.window_out =descr.window/4; { int temp[]={4,2}; if(ArrayCopy(descr.heads,temp)<ArraySize(temp)) return false; } descr.layers=3; descr.step=3; descr.variables=prev_out; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
Transformer的输出端,我们期望得到各个分析通道对应下一个分块的预测token。需要提醒您的是,通道数量在预处理阶段已经得到大幅扩充。现在,每个通道都会输出一个新的token,代表该通道在未来时间区间内走势的预测结果。
为把这一紧凑的切片(slice)还原为指定预测视界下完整的预测序列,我们使用卷积层完成该转换。卷积层接收一组预测token,经过若干滤波器运算,将其变换为目标长度的序列数据,可供交易机器人直接后续调用。该方法既保留token级自回归预测的优势,又能够输出可供策略使用的完整时间序列。
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 1; descr.step = EmbeddingSize; prev_count=descr.layers = prev_out; descr.window = EmbeddingSize; prev_out=descr.window_out = NForecast; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = SoftPlus; if(!encoder.Add(descr)) { delete descr; return false; }
卷积层输出之后,我们得到一组目标长度的单变量序列,每一条序列对应一个通道的预测结果。然而,为了与真实数据做比对,我们并不需要一组相互独立的时间序列,而是需要一条完整的多模态序列:同一时间点下,相邻元素分别对应不同来源的信息。因此,下一步对结果做矩阵转置(transpose)。最终输出矩阵维度为[预测视界 × 通道数量]。
//--- layer 7 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(!encoder.Add(descr)) { delete descr; return false; }
接下来,对于得到的矩阵再使用一层卷积层,该层的作用是将通道数恢复至原始维度。
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_out; descr.window = prev_count; descr.step = prev_count; descr.layers = 1; prev_out=descr.window_out = BarDescr; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = TANH; if(!encoder.Add(descr)) { delete descr; return false; } prev_count=descr.count;
最后,为将预测结果转换为真实数值,我们将数据送入反归一化层。该层接收每一条输出的多模态序列,还原各个原始通道对应的统计参数:均值与方差。这就把经过标准化处理的token,转换回我们熟悉的业务量纲。
//--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count * prev_out; descr.layers = 1; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
因此,编码器输出一整套完整的多模态序列预测值,单位为交易者熟悉的行情量纲:价格、成交量以及各类指标。该部分数据支持可视化展示、与真实报价做对比,也可用于解读交易智能体的行为逻辑。
此外,借助大量无标签历史数据预测未来走势,我们能够以自监督学习模式训练编码器。这一步可以生成富含信息的预测token隐表征。我们正是打算将该表征送入Actor、Director和Critic模块,以此保障模型的稳定性、自适应能力以及泛化性能。
CLayerDescription *latent = encoder. At(LatentLayer);
接下来,我们开始介绍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; }
经过预处理的账户数据,在多样化多头交叉注意力模块中融合市场信息上下文。该模块能够捕捉账户状态与全局市场模式之间的关联关系,辅助模型做出更合理的交易策略选择。
//--- Layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; { int temp[] = {AccountDescr, // Input window latent.window // Cross window }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { int temp[] = {1, // Input units latent.variables // 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; }
模型采用三层多层感知机(MLP)输出最终交易决策,层与层之间搭配不同激活函数,以此引入所需的非线性表达能力。第一层使用双曲正切函数(tanh),可以将信号影响区分为正向与负向。第二层之后采用SoftPlus激活函数,放大正向响应,同时对小数值做平滑处理。最后的输出层使用sigmoid函数做约束,将Actor的动作空间归一化,输出值限定在区间[0, 1],便于对交易参数做缩放计算。
//--- 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; }
Director与Critic模型架构大体相似,区别在于:输入不再是账户状态张量,而是Actor输出的动作向量,同时输出层尺寸也做了修改。因此,本文不对二者展开详细讨论,留给读者自行研究。全部模型的完整架构定义见附件。
环境编码器训练程序
现在介绍模型训练环节。整个训练流程划分为三个阶段。第一阶段,将环境状态编码器的训练独立出来。为尽可能使用全部可用数据开展训练,该流程无需预先构建显式训练集,全部所需数据直接从交易终端实时读取。
该训练阶段通过EA脚本实现,文件路径:…\Experts\TimeFound\StudyEncoder.mq5。这里,CreateBuffers方法负责缓冲区管理;入参包含起始状态索引,以及两个缓冲区指针:一个用于存放待分析源数据,另一个存放预测走势的目标真值。该方案可以实现编码器的自监督训练,无需耗费额外成本对历史时间序列做人工标注。
bool CreateBuffers(const int start_bar, CBufferFloat* state, CBufferFloat *time, CBufferFloat* forecast) { if(!state || !time || start_bar < 0 || (start_bar + HistoryBars + NForecast) >= int(Rates.Size())) return false; //--- vector<float> vState = vector<float>::Zeros(HistoryBars * BarDescr); vector<float> vForecast = vector<float>::Zeros(NForecast * BarDescr); time.Clear(); time.Reserve(HistoryBars);
在该方法体内,我们会校验已加载的数据是否信息量充足,并为存储数值准备内部缓冲区。随后建立数据准备循环。从终端加载得到的原始时间序列,存储格式为最新数据置于缓冲区头部,历史更久远的数据放在尾部,同时保持时序逻辑关系不变。为完成编码器参数调优,每一步训练迭代都需要获取一段完整时间区间,该区间同时覆盖历史数据窗口与设定的预测视界。
首先,构造待分析状态向量:从入参给定的索引位置出发,向后偏移指定的预测视界长度,再把后续全部元素拷贝至源数据缓冲区。这一套操作流程与时间序列的时间顺序相匹配,最大限度降低数据预处理带来的开销,保障模型实时运行的效率。
int bar = start_bar + NForecast; for(int b = 0; b < (int)HistoryBars; b++) { float open = (float)Rates[b + bar].open; float rsi = (float)RSI.Main(b + bar); float cci = (float)CCI.Main(b + bar); float atr = (float)ATR.Main(b + bar); float macd = (float)MACD.Main(b + bar); float sign = (float)MACD.Signal(b + bar); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) return false; //--- int shift = b * BarDescr; vState[shift] = (float)(Rates[b + bar].close - open); vState[shift + 1] = (float)(Rates[b + bar].high - open); vState[shift + 2] = (float)(Rates[b + bar].low - open); vState[shift + 3] = (float)(Rates[b + bar].tick_volume / 1000.0f); vState[shift + 4] = rsi; vState[shift + 5] = cci; vState[shift + 6] = atr; vState[shift + 7] = macd; vState[shift + 8] = sign; if(!time.Add(float(Rates[b + bar].time))) return false; }
接下来,我们进行目标真值的准备工作。由于目标真值属于预测任务的未来数据,为正确构建目标缓冲区,需要对数值顺序做反转处理。在数据搬运时采用逆序索引:预测视界内的最后一个元素放到缓冲区首位,其余元素按顺序依次排布。该技术保证每一轮的训练迭代中,模型输入都可以和对应的未来真值正确对齐。
bar--; for(int b = 0; b < (int)NForecast; b++) { float open = (float)Rates[bar - b].open; float rsi = (float)RSI.Main(bar - b); float cci = (float)CCI.Main(bar - b); float atr = (float)ATR.Main(bar - b); float macd = (float)MACD.Main(bar - b); float sign = (float)MACD.Signal(bar - b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) return false; //--- int shift = (NForecast - b - 1) * BarDescr; vForecast[shift] = (float)(Rates[bar - b].close - open); vForecast[shift + 1] = (float)(Rates[bar - b].high - open); vForecast[shift + 2] = (float)(Rates[bar - b].low - open); vForecast[shift + 3] = (float)(Rates[bar - b].tick_volume / 1000.0f); vForecast[shift + 4] = rsi; vForecast[shift + 5] = cci; vForecast[shift + 6] = atr; vForecast[shift + 7] = macd; vForecast[shift + 8] = sign; }
我们将已经处理完毕的时间序列写入数据缓冲区,把运算得到的布尔结果返回给调用方程序,以此结束该方法。
if(!state.AssignArray(vState)) return false; if(!forecast.AssignArray(vForecast)) return false; if(time.GetIndex() >= 0) if(!time.BufferWrite()) return false; //--- return true; }
训练流程主体逻辑在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);
依据上述时间边界,计算K线的总数量,并为数据缓冲区分配所需大小的内存。
if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; }
接下来,从交易终端动态加载指定品种与时间周期的历史数据。
int count = -1; bool load = false; do { RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); count++; load = (RSI.BarsCalculated() >= bars && CCI.BarsCalculated() >= bars && ATR.BarsCalculated() >= bars && MACD.BarsCalculated() >= bars ); Sleep(100); count++; } while(!load && count < 100); if(!load) { PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__); ExpertRemove(); return; } //--- 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; }
当训练数据准备完成后,我们正式进入训练流程,训练逻辑采用嵌套循环结构实现。外层循环用于管控训练迭代轮次,迭代总次数由用户通过程序参数进行设定。
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; }
在外层训练循环内部,从训练集中采样单个状态的索引,以此作为基准来构建输入数据与目标数据。该索引连同对应缓冲区的指针一并传入CreateBuffers方法,使得每一次迭代都能获取对齐无误的历史序列与预测序列,保障有效开展自监督学习。
接下来,我们开启内层循环,循环迭代次数由用户通过Repeats参数指定。此处您需要格外留意:在该循环内,模型会复用同一组输入数据与目标数据反复训练。编码器内部带噪声的归一化层提供所需的数据增强效果:让模型学习关注数据的内在结构,而非拘泥于具体数值。但如果重复次数设置过高,在目标真值保持不变的情况下,模型存在风险:会忽略输入数据,反复输出一模一样的结果。因此,我们建议将Repeats参数设置为大约5次迭代。
for(int r = 0; r < Repeats; r++) { //--- Feed Forward if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } //--- Study if(!cEncoder.backProp(Result, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
循环体内执行模型的前向传播与反向传播运算。之后我们向用户输出训练进度信息,随即进入循环的下一次迭代。
if(GetTickCount() - ticks > 500) { double percent = double(iter) * 100.0 / (Iterations); string str = StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Encoder", percent, cEncoder.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } }
全部迭代执行完毕后,将训练结果写入日志文件,程序随即结束运行。
Comment(""); //--- PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Encoder", cEncoder.getRecentAverageError()); ExpertRemove(); //--- }
Actor、Director和Critic模型的离线训练以及在线微调程序,完整复用自以往项目,未做任何修改。此外,构建训练集与模型测试均采用经过验证的成熟方案。这样我们就可以把精力集中在架构创新上,无需耗费资源重新开发配套组件。
本文所用程序的全部源代码均收录于附件中。
测试
如前文所述,模型训练分为三个连续阶段,能够以逻辑化、分步的方式搭建整套系统,最大限度保障可靠性。
第一阶段,使用欧元兑美元(EURUSD)货币对一分钟周期的五年历史数据训练编码器。如此庞大且精细的数据量,足以生成对当前市场状态具备深刻且有意义的隐表征。编码器学习识别关键模式、挖掘市场规律,并将市场行情编码为紧凑且富含信息的向量,供给其余全部模块使用。
第二阶段,对架构中的核心交互组件 —— Actor、Director和Critic开展离线训练。为此,基于2024年市场数据构建训练集,同时完整保留编码器训练阶段使用的全部参数。训练过程引入近最优轨迹(near‑perfect trajectory)理念:智能体的动作并非随机选取,而是基于后续价格走势分析确定。换言之,由于可以拿到完整的价格变化轨迹,我们能够预先获知哪些动作会产生最优结果,并将这些动作用于训练。该方法相当于直接向模型示范如何交易,而不是让模型在环境中如同无地图摸索一般,依靠反复试错盲目寻找有效策略。最终智能体从经过验证的样本中学习,这些样本结果清晰、逻辑充分,尽可能趋近理想效果。这样不仅简化了训练流程,还让训练目标更聚焦,具备实际交易价值。
最后一个阶段为在线微调,直接在策略测试器内执行。在此,模型以尽可能贴近实盘交易的模式读取历史数据,依据真实市场动态调整自身参数。这一步尤为关键:可以针对训练集中体现不出来的环境变化、市场噪声与随机扰动,进一步优化智能体的行为。
整套训练流水线完成之后,模型使用全新数据 —— 2025年1月的报价数据开展测试。完整保留训练阶段的全部参数与配置,以此保证评估过程完全客观公正。测试结果呈现如下。

测试结果可以说是令人鼓舞,但也伴随着一些明显的保留意见。月度总收益率为+26.5%,不过资金曲线并未呈现持续上行趋势。与之相反,大部分时间资金曲线处于横盘震荡,期间伴随运行不稳定阶段以及幅度较大的回撤。最大回撤达到58%,说明在部分市场环境下,模型决策波动剧烈且稳定性不足。
该现象表明:模型仍处在摸索稳定交易风格的阶段,尚未形成清晰可盈利的运行轨迹。即便如此,能够取得正向结果本身已是积极信号:基础框架已经搭建完成,接下来的工作目标是提升稳定性。
结论
在本次研究工作中,我们从通用跨域时间序列预测理论出发,完成了在MetaTrader 5环境的实现。我们详细剖析了编码器 的内部结构,掌握多尺度分块机制,并将输出的隐表征接入Actor–Director–Critic架构。从数据预处理、编码器自监督训练,再到核心模块的离线训练与在线微调,每一个步骤都展示:将前沿学术研究与工程方案合理结合,能够提升对市场动态的分析能力。
基于2025年1月行情报价的回测情况表明,我们的系统具备盈利能力,但同时会出现深度回撤。这也凸显出需要进一步完善风控体系与自适应止损机制。与此同时,在没有额外调参优化的前提下依旧实现正向收益,足以证明所选架构具备稳健的核心架构,可以复用已经学习到的知识完成预测任务。
参考文献
文中所用的程序
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Research.mq5 | EA | 用于收集示例的EA |
| 2 | ResearchRealORL.mq5 | EA | 通过Real-ORL方法收集示例的EA |
| 3 | StudyEncoder.mq5 | EA | 用于训练环境编码器的EA |
| 4 | Study.mq5 | EA | 用于离线模型训练的EA |
| 5 | StudyOnline.mq5 | EA | 用于在线模型训练的EA |
| 6 | Test.mq5 | EA | 模型测试EA |
| 7 | Trajectory.mqh | 类库 | 用于描述系统状态与模型架构的结构 |
| 8 | NeuroNet.mqh | 类库 | 用于构建神经网络的类库 |
| 9 | NeuroNet.cl | 库 | OpenCL程序代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/18447
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5中的量子神经网络(第一部分):创建包含文件
市场模拟(第 24 部分):仓位视图(二)
新手在交易中的10个基本错误
交易中的神经网络:一种跨领域时间序列预测框架(TimeFound)