交易中的神经网络:面向准确分类的有效特征提取(最终部分)
引言
我们对Mantis框架的介绍已进入最后阶段。首先,我们详细研究了它的理论基础:模型如何处理多通道时间序列、为什么需要差分信号,以及局部token是如何生成的。接下来,我们进一步深入架构,了解分块(patch)的构成方式,以及模型如何从市场噪声中提取稳定模式。现在,我们把全部内容整合到一起。
Mantis框架由一组相互独立但逻辑上互相关联的模块顺序串联而成。每个模块承担一项专门任务 —— 从输入数据的初步处理,到token生成,再到信息聚合。其底层设计原则是:在尽可能减少先验偏置的前提下,从数据本身挖掘尽可能多的模式。
整个流程始于原始市场特征输入模型。模型不会将全部数据合并为单一矩阵,而是把每一类数据划分到独立通道。针对每一个原始通道,额外生成对应的一阶差分通道,让模型可以显式捕捉短期动态特征。这类差分通道提升了模型对价格转向变化的敏感度。例如,差分通道数值突然加速,往往预示着一段脉冲式行情即将启动,这会早于传统指标给出信号。
将所有通道数据送入卷积模块,开启初步特征分析。卷积运算通过学习识别稳定的微观形态,提取局部特征。每个通道输出256维特征,用于表征该通道的局部行情行为。
卷积处理完成后进入下一阶段:数据被切分为32个无重叠的分块,每个分块对应时间序列中的一段特定区间。对每个分块按通道进行平均池化,得到32个token,每个token包含256维特征。这一点十分关键。分块就是行情行为的局部片段,压缩后的迷你图表片段。它能够捕捉各类行情阶段:趋势推动、盘整震荡、回调、背离等。这种分段处理让模型行为更加稳定,同时具备自适应能力。模型开始不再把市场仅仅视为一串数字,而是识别为一系列可辨识的行情场景。
在此阶段,对每一路输入通道,分别获取来自原始数据流与差分数据流的token。将两类token缩放至统一取值范围,做归一化后进行融合。再通过线性投影层生成汇总token,用于表征该通道在当前片段内的整体行为。该汇总token同时包含行情状态、变化速率以及波动强度,囊括交易者用来直观判断市场情绪的全部关键信息。
接下来,完整的token序列送入注意力模块。这是整个架构最核心的层级,各个分块在此发生交互关联。模型会评估历史哪些片段对当前行情状态具备重要参考价值。譬如,20根K线之前出现的信号,到当下评估反转时可能变得至关重要。这种捕捉远距离片段之间关联的能力,让该模型尤其适合分析复杂市场结构。
这背后蕴含一个简洁而强大的设计思想:兼顾局部细节精度,同时不丢失全局上下文。在交易场景中这一点尤为重要。例如,震荡休整阶段的一串小K线,本身信息量很低。但如果这串K线出现在成交量剧烈拉升之后,且处于关键价位,整体市场含义就会完全改变。Mantis可以自动捕捉这类上下文信息。
Mantis框架不只是一套网络架构,更是一套灵活工具,能够适配各类交易策略。
作者给出的Mantis框架可视化示意图如下。

模型架构
在之前的文章中,我们已经完成Mantis框架主要组件的开发,如今来到关键阶段:设计一套可训练模型架构,使其能够实时输出交易决策。一名经验丰富的交易者会观察价格走势、成交量、K线形态以及整体市场情绪,结合上下文之后才会做出开仓决策。与之类似,我们的模型不能只把市场看作一串数字,而要学会感知相互关联的市场运行过程。我们的目标是复现一套行为模型:能够识别市场形态、感知行情阶段切换,并可以适配全新的市场场景。整套Mantis框架的设计逻辑正是围绕这一目标构建。
需要强调的是,Mantis框架最初是作为时间序列分类器开发的。其架构的设计目标是对复杂序列做切分,并识别其中隐藏的模式。该方法在金融场景中效果尤为突出,因为价格信号往往被层层市场噪声所掩盖。经过卷积、局部分块构建、通道聚合等多级处理,模型学会从行情数据中识别内在结构,而不是只看到随机波动。Mantis不仅可以捕捉交易信号,还能够识别不同的市场机制状态。
然而,针对我们的当前任务,Mantis不再作为独立的分类器使用,而是作为交易智能体感知层的基础。我们基于Mantis架构生成嵌入向量,也就是对市场状态做紧凑且信息丰富的表征,再将该表征送入模型的控制模块。如此一来,分类器就充当了智能体的“眼睛”,使其能够读懂市场的行为特征。
这份嵌入表征会继续传递给架构的下一部分,该部分基于Actor–Director–Critic(行动者‑调度器‑评估器)机制搭建。该机制可以把职责拆分到不同模块,保障智能体行为的稳定性。 Actor接收嵌入向量,输出交易决策。Director充当结构过滤器与行为校正器,对Actor输出的行为做判定,区分有效行为与错误行为,并输出强反馈信号。它的作用是过滤明显不合理、不稳定的决策,尤其在市场剧烈震荡阶段。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; }
基于该方法的参数,我们获取四个动态数组的指针,每个数组用于存放对应模型组件的架构描述:编码器、行动者、调度器和评估器。这些数组作为容器,逐个存入用于描述神经网络模块结构的对象。
在方法函数体内部,第一步校验传入的指针。如果任意一个指针未初始化,或者指向无效内存地址,则会自动创建对应对象的新实例。该设计免去了手动预先准备数组的工作,提升代码模块化程度,更适合可扩展的解决方案。
接下来,我们正式定义各个模型组件的架构。在这套链路中首先配置编码器是合乎逻辑的,它是模型感知市场的核心模块。将原始时间序列转换为具备业务含义的市场状态表征,这一过程由此开始。
第一阶段使用全连接层。在本场景下,该层不承担计算任务,仅起到接口作用。它的作用是提供便捷的缓冲区接口,将输入数据加载进模型。
//--- 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; }
将量纲和尺度不同的输入数据送入批归一化模块。这里,全部数值被映射至可相互对比的统一尺度,消除尺度差异带来的影响,这在处理多维金融时间序列时尤为关键。归一化处理让模型不再把数据看作一堆无序数字,而是将其理解为逻辑连贯的信息流。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormWithNoise; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
为在训练过程中实现温和的数据增强,我们使用加入可控噪声的批归一化层。该方式能够提升模型对市场噪声的鲁棒性,防止模型对无意义的小幅波动发生过拟合。该方案让系统可以更加灵活地解读市场信息,在自适应能力与稳定性之间取得平衡。
接下来的重要步骤是构建一阶差分通道。该层生成差分特征,也就是相邻时间点之间的数值变化量,帮助模型捕捉市场行情的动态与运行方向。在交易场景中,这类变化量往往是理解趋势反转或者趋势延续的关键。某一时刻价格的涨跌,有时会比价格的绝对数值更加重要。构建差分通道能够显著提升原始数据的信息密度,增强模型对局部趋势与快速波动的敏感度。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatDiff; prev_count = descr.count = HistoryBars; descr.layers = BarDescr; descr.step = 1; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
生成一阶差分通道之后,向扩展后的数据集加入时间编码谐波分量。该步骤至关重要,它让模型能够理解时间依赖关系的上下文,以及市场运行过程的周期性特征。谐波相当于一类时间定位标识,模型借助它不仅可以识别具体时间点,还能够识别重复形态、季节性波动以及不同时间尺度下的行情规律。基于谐波实现的时间编码,赋予模型对时间结构的深度理解能力。
//--- 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 = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
为有效挖掘数据当中跨通道的关联与相互依赖关系,我们采用多窗口尺寸的卷积模块。这种多窗口设计,可以让模型同时捕捉局部短期形态,以及多通道动态数据里同步显现的中长期趋势。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
数据预处理的最后阶段为分块层。该层将处理完成的多通道张量切分为相互独立、无重叠的分块,本质上就是时间序列的片段,每一个分块都聚合了对应区间的局部市场状态信息。
这种切分方式让模型可以聚焦于各个独立数据片段,降低计算复杂度,同时更便于识别局部行情形态。在每个分块内部会执行进一步聚合操作,生成紧凑且信息完备的特征表征。
最终,每一个分块都会成为独立的token,承载一段有限时间区间内、经过压缩且结构化的市场状态描述。该机制大幅提升模型捕捉局部形态的能力,也为后续处理与决策环节打下坚实基础。
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMantisPatching; descr.count = prev_count; descr.layers = prev_out; descr.window = EmbeddingSize; descr.window_out = NSkills; descr.step = Segments; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_count = descr.step; prev_out = descr.layers;
我们编码器内部的注意力模块被封装为单个对象:CNeuronMantisAttentionUnit。该组件的核心作用是捕捉各分块与特征通道之间的重要关联。通过对象参数,我们可以设定内部交叉注意力模块的数量,以此灵活调节信息分析的深度与广度。
每一个交叉注意力模块,致力于筛选出数据流中最关键的元素,并将其与可学习的类别token建立关联。该架构能够实现有效的噪声过滤,让模型专注于那些真正会左右交易决策的信号。
//--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMantisAttentionUnit; descr.step = 4; descr.count = prev_count; { int temp[] = {EmbeddingSize, EmbeddingSize}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.layers = 3; descr.window_out = NSkills; descr.window = prev_out; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
交叉注意力模块的输出会生成一个token,这是对环境分析状态的紧凑且富含信息的表征。在我们的任务场景下,对市场状态做精准分类并非首要目标。更为重要的是获取足够清晰且区分度良好的隐层表征,以此让Actor能够做出经过充分考量、信息完备的交易决策。
正因如此,我们刻意不去过度复杂化编码器的架构。该方法在模型感知深度与计算效率之间维持平衡,使其可以灵活适配真实市场环境,同时不降低决策质量。
接下来,我们介绍Actor的架构。与编码器类似,输入阶段采用全连接层搭配批归一化模块。二者共同作为接口,接收输入数据并完成初步标准化处理。然而,此时主信息通道承载的数据有所不同:传入交易账户的当前状态数据,包括账户余额、持仓、回撤水平,以及其他反映交易智能体内部运行上下文的各项指标。
//--- CLayerDescription *latent = encoder.At(7); //--- 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 = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
与此同时,通过一条辅助通道,编码器生成的市场环境隐层表征被送入模型。通过融合两路数据流 —— 账户内部状态与外部市场环境,Actor做出的决策就已经融入了策略思考与风险管理要素。模型在训练过程中,不仅需要考量当下市场行情,还需要顾及自身可用资金、风险承受能力,以及当前持仓的动态变化。该方法令智能体的行为具备审慎性与鲁棒性,即便在市场剧烈震荡的环境下也能稳定运行。
来自两路信息流(市场环境和交易账户状态)的数据,会在拼接层完成合并。该模块承担简单但十分关键的功能:将编码器输出的嵌入向量,与交易账户的内部特征融合为一份统一表征。借此,模型得以掌握完整的全局状态信息。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = AccountDescr; // Inputs window descr.step = latent.windows[0]; // Cross window descr.batch = 1e4; descr.activation = TANH; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
数据完成拼接合并之后,开始深度特征处理,该部分由三层全连接层依次实现。第一层负责提取并归纳关键特征。第二层强化各参数之间的重要关联,挖掘潜在的模式规律。第三层输出最终的交易动作。经过这一系列变换,模型不再是简单地对信号做出反应,而是能够输出经过深思熟虑、具备策略合理性的决策。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.batch = 1e4; 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 = 1e4; 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 = 1e4; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Director与Critic的模型架构几乎完全复刻>Actor的结构。主要区别在于输入数据的维度。智能体输出的动作张量通过主通道送入这两个模型,输出端会生成对这些动作的评估结果:Director输出分类评估,Critic输出策略层面的价值评估。由于二者内部逻辑相近,本文不再重复赘述,避免内容冗余。想要深入研究的读者,可以查阅本文附带的代码,其中包含系统全部可训练组件的完整架构定义。
对比学习
完成各模型架构搭建后,进入下一个重要环节:模型训练。正如理论部分详细介绍的,Mantis框架的核心特性之一就是自监督对比学习。该机制可以生成信息丰富、区分度高的token,以紧凑且表现力强的形式刻画环境状态。就像经验丰富的交易者一眼就能分辨盘整阶段与趋势推动行情,我们的模型也需要学会识别各类典型市场状态,并将其压缩表征,供给系统其他组件调用。
在本方案中,对比学习是让模型形成市场直觉的底层基础。在没有标签监督的条件下,编码器通过理解两两市场状态之间的差异,学习构建特征表征,尽可能把那些外观相似但本质完全不同的市场场景区分开来。
对应算法实现在EA文件:"…\MQL5\Experts\Mantis\StudyContrast.mq5"。该EA负责构建正负样本对、执行训练迭代、统计指标,并保存训练完成的参数。
这里有一处重要的、毫不夸张地说属于概念层面的差异:我们实现的对比学习与传统方案有所不同。我们刻意省去了训练阶段耗时的离线数据集构建步骤。取而代之,充分利用MetaTrader 5平台强大的灵活性,在训练过程中动态生成训练数据。
用户仅需要在EA参数中设置训练时间段的起止日期。所有需要的行情数据都会从终端内部实时自动拉取。该方式不仅简化操作流程,还大幅拓展了模型训练的可行空间。
//+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input datetime Start = D'2020.01.01'; input datetime End = D'2025.01.01'; input int Iterations = 100000; input int Batch = 50; input group "---- Indicators ----" input ENUM_TIMEFRAMES TimeFrame = PERIOD_M1; //--- input group "---- RSI ----" input int RSIPeriod = 14; //Period input ENUM_APPLIED_PRICE RSIPrice = PRICE_CLOSE; //Applied price //--- input group "---- CCI ----" input int CCIPeriod = 14; //Period input ENUM_APPLIED_PRICE CCIPrice = PRICE_TYPICAL; //Applied price //--- input group "---- ATR ----" input int ATRPeriod = 14; //Period //--- input group "---- MACD ----" input int FastPeriod = 12; //Fast input int SlowPeriod = 26; //Slow input int SignalPeriod = 9; //Signal input ENUM_APPLIED_PRICE MACDPrice = PRICE_CLOSE; //Applied price
接下来,我们深入了解该流程在Train方法中的具体实现。该方法启动完整的对比学习循环,封装了数据预处理、缓冲区管理,以及网络前向传播与反向传播的全部逻辑。
流程首先划定历史数据的区间边界。调用iBarShift函数,计算从当前K线位置到训练区间起点、终点的K线偏移量。
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; }
下一阶段为数据加载。为此程序实现了循环逻辑,依次调用所有指标的Refresh方法,并校验已计算出的数据条数。该循环在数据加载成功后结束,或是尝试达到100次的上限次数时终止。
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; }
数据准备流程的末尾,我们设置报价数组所需的索引方向(ArraySetAsSeries),并声明所需的局部变量。
if(!ArraySetAsSeries(Rates, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } bars -= end + HistoryBars; if(bars < 0) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } //--- vector<float> result, target, neg_target; bool Stop = false;
主训练循环正式开启。每次迭代都会在整个历史数据范围内随机选取一个位置,也就是K线索引,用来生成基准样本状态。
uint ticks = GetTickCount(); //--- for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch) { int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars); if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; }
针对该基准状态,创建原始数据缓冲区,并执行编码器的前向传播。
//--- Feed Forward if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } cEncoder.getResults(Result);
我们保存前向传播的输出结果作为参考基准,也就是一份样本市场状态,模型需要向该基准做特征对齐。接下来有一个关键步骤:使用完全相同的输入数据,再次对编码器执行一遍前向传播。但这里存在一处细节:我们架构内置的带噪声批归一化层,起到温和数据增强的作用。也就是说,第二次前向传播时,模型不会简单复现上一轮输出,而是生成一份经过轻微扰动、带有噪声的结果。
正是利用该特性,我们构造出正样本对。以之前保存的参考基准作为目标值,执行反向传播。通过这种方式训练模型,使其忽略随机噪声,聚焦于有实际意义的稳定上下文特征。简言之,模型学会穿透噪声干扰,对市场核心特征保持稳定的表征。
//--- Positive if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime)) || !cEncoder.backProp(Result, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
接下来就到了或许最有意思的环节 —— 构造负样本对。负样本对的数量由EA参数中的Batch变量指定。为生成所需数量的负样本对,我们搭建嵌套循环,事先先将基准token存入向量之中。
//--- Negative if(!Result.GetData(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } for(int b = 0; b < Batch; b++) { int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars); int count = 0; while(negot == posit) { negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars); count++; if(count > 100) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } if(Stop) break;
在嵌套循环体内,我们从训练数据区间中随机选取另一个状态,该状态需要区别于之前使用的基准状态。这里有一点需要着重说明:只要不等于基准状态,其余任意状态均可选用。理论上,两个时序窗口甚至允许时间范围发生重叠。
正是该特性让训练过程更具动态,也更贴合真实场景。模型学习去识别高度相似状态之间的差别。当窗口发生重叠时,模型的任务就是精准挖掘出可以把两种状态区分开来的细微特征。在真实市场环境里,往往正是这些细微差异,决定了交易决策的成败。
因此,这套对比学习训练会促使编码器不只是单纯识别市场状态,而是提取关键特征,生成信息丰富、区分度高的token,供后续Actor模块使用。
针对当前选中的状态,生成原始数据缓冲区,并执行编码器的前向传播。
if(!CreateBuffers(negot + end, GetPointer(bState), GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } //--- Feed Forward if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
接下来进入为负样本构造训练目标的环节。为此,我们计算当前步骤得到的token,与之前保存的基准表征二者之间的差值。其核心逻辑是:让当前token远离基准表征,以此生成负样本的目标值。
为放大该效果,我们将基准状态与当前状态之间的距离翻倍,以此增大排斥力。最终效果是和基准状态相近的样本只会受到微弱排斥,而差异巨大的样本则会受到更强的排斥作用。这样能够扩大基准表征周围的特征空间余量,让token拥有更好的区分度,同时提升抗噪声能力。
这套机制为训练过程引入动态调节能力,保障隐空间内各类样本可以清晰分离开。模型不只是单纯识别各类市场状态,而是学会在基准状态周围构建出一块安全隔离区,让其他不同的状态与之保持足够的距离。这会显著提升后续决策环节的输出质量与可靠性。
cEncoder.getResults(result); neg_target = result * 2 - target; neg_target = neg_target - neg_target.Max(); if(!neg_target.Activation(neg_target, AF_SOFTMAX) || !Result.AssignArray(neg_target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
我们还剩下最后一步,但同样至关重要:使用生成好的负样本目标值,对编码器执行反向传播。正是这一步完成模型参数的优化,对网络进行调整,使其能够在隐特征空间中将不同的市场机制状态有效地相互推远。
if(!cEncoder.backProp(Result, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
当全部训练操作执行完成后,系统会向用户输出信息,更新运行状态。程序通过显示完成百分比以及模型误差指标,实时反馈当前训练进度。这种透明化的处理方式,不仅可以提升算法效果的可信度,也便于快速监控训练的实际成效。
之后,进入训练循环的下一轮迭代,使用新的数据重复上述全部流程。该循环让模型得到充分且持续的优化,逐步生成精度越来越高、信息含量充足的特征表征,为做出严谨可靠的交易决策提供支撑。
if(GetTickCount() - ticks > 500) { double percent = double(iter + b) * 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)M1周期过去五年的历史数据。如此的数据量级与细节粒度,让编码器能够学习出高质量、信息丰富的市场状态隐特征表征,作为整个系统后续运行的底层基础。
第二阶段:系统核心组件的离线训练,包括Actor、Director和Critic。该阶段使用2024年采集的数据集,保留全部前期设定参数。训练采用近乎完美轨迹的理念,让模型从可信度最高的动作样本与价值样本中学习。该阶段用于固化基础交易策略与决策判别标准。
第三阶段:模型在线微调,直接在策略测试器中,在相同历史区间完成。以此让模型适配不断变化的市场环境,尽可能精细地调优参数。
全部训练阶段结束后,使用2025年1至3月的数据开展模型测试。训练阶段用到的所有参数在此过程中保持固定不变。该方式保证在从未见过的新数据集上,对模型性能做出公平客观的评估。测试结果呈现如下。

测试期间,模型一共执行881笔交易,其中盈利订单447笔,盈利率为50.74%。该数据说明盈利与亏损订单整体处于中性均衡水平。利润因子(Profit Factor)为1.25。
整体来看,该策略取得了正向收益,风险水平适中,在测试周期的前半段权益稳定增长。然而,在2月中旬之后,盈利能力出现下滑,权益曲线明显转入横盘震荡,并伴随回落。
因此,该策略具备可行性,在测试区间表现为正向收益,但仍有若干参数有待优化。
结论
通过本次研究工作,我们已经将Mantis框架完整集成至定制化算法交易模型中。该模型能够从高频时间序列中提取有效特征,并实时输出具备实际参考意义的交易决策。
本文重点介绍了编码器的自监督对比学习阶段,对应实现在EA的StudyContrast.mq5中。我们摒弃静态数据集方案,直接从终端动态加载历史数据,使用欧元兑美元M1周期过去五年中的动态获取的历史样本。借助柔和数据增强与可控噪声构造正负样本对,让模型在实践中学会穿透市场噪声,识别底层真实行情特征。
模型在2025年1至3月的最终测试证明本方案具备盈利潜力。该策略具备可行性,在测试区间取得正向表现,但部分参数仍有待进一步改进。
参考文献
文中所用的程序
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Research.mq5 | EA | 用于收集示例的EA |
| 2 | ResearchRealORL.mq5 | EA | 使用Real-ORL方法收集示例的EA |
| 3 | StudyContrast.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/18329
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:对象事件(四)
新手在交易中的10个基本错误
交易中的神经网络:用于精确分类的高效特征提取(构建对象)