交易中的神经网络:用于时间序列预测的轻量级模型·进阶篇
「稀疏时序预测神经元的初始化链路」
CNeuronSparseTSF 这个类专门处理时间序列的稀疏预测,在 MT5 的 OpenCL 环境里跑。它的 Init 方法先把基类 CNeuronBaseOCL 拉起来,传入的神经元宽度不是直接的 forecast,而是 forecast * variables——这意味着输出维度按变量数摊开,而不是单序列点。 初始化里连续挂了四组转置操作 acTranspose[0]~[3] 和两组核心层 cConvolution、cForecast。卷积层窗口直接取 2*period 宽、步长 period,把长度为 sequence 的序列切成 sequence/period 段;cForecast 用 TANH 激活,卷积层却设成 None,这种不对称是有意压住中间表示的稀疏性。 feedForward 的第一步只调用 acTranspose[0].FeedForward(NeuronOCL),后面几组转置和预测层在前向传播里是链式触发的,任一环节返回 false 整个前向就断。你在 EA 里若拿到 false,优先查 acTranspose[0] 的 sequence 与 variables 是否和卷积层期望的对得上。 外汇与贵金属行情跳空多,period 设太大时 sequence/period 可能落到 1 以下导致整数截断,初始化会静默失败。建议先在 EURUSD 的 M5 上用 period=5、sequence=60 跑通再调。
class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint sequence, class="type">uint variables, class="type">uint period, class="type">uint forecast, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronSparseTSF; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronSparseTSF::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint sequence, class="type">uint variables, class="type">uint period, class="type">uint forecast, ENUM_OPTIMIZATION optimization_type, class="type">uint batch ) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, forecast * variables, optimization_type, batch)) class="kw">return false; if(!acTranspose[class="num">0].Init(class="num">0, class="num">0, OpenCL, sequence, variables, optimization, iBatch)) class="kw">return false; if(!cConvolution.Init(class="num">0, class="num">1, OpenCL, class="num">2 * period, period, period, sequence / period, variables, optimization, iBatch)) class="kw">return false; cConvolution.SetActivationFunction(None); if(!acTranspose[class="num">1].Init(class="num">0, class="num">2, OpenCL, variables * sequence / period, period, optimization, iBatch)) class="kw">return false; if(!cForecast.Init(class="num">0, class="num">3, OpenCL, sequence / period, sequence / period, forecast / period, variables, period, optimization, iBatch)) class="kw">return false; cForecast.SetActivationFunction(TANH); if(!acTranspose[class="num">2].Init(class="num">0, class="num">4, OpenCL, period, variables * forecast / period, optimization, iBatch)) class="kw">return false; if(!acTranspose[class="num">3].Init(class="num">0, class="num">5, OpenCL, variables, forecast, optimization, iBatch)) class="kw">return false; if(!SetOutput(acTranspose[class="num">3].getOutput()) || !SetGradient(acTranspose[class="num">3].getGradient()) ) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronSparseTSF::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!acTranspose[class="num">0].FeedForward(NeuronOCL)) class="kw">return false;
◍ 稀疏时序网络的逐层前向回传链路
下面这段 CNeuronSparseTSF 的成员函数,把卷积、转置与预测三层串成一条前向链:先拿 acTranspose[0] 的输出喂给卷积层,再做一次求和归一化(缩放系数 1、不翻转),随后依次穿过 acTranspose[1]、cForecast、acTranspose[2]、acTranspose[3]。任意一步 FeedForward 返回 false 就直接退出,保证整网前向不出现半残状态。 反向传播 calcInputGradients 的顺序正好反过来:从 acTranspose[2] 往输入侧逐层算隐藏梯度,遇到激活函数非 None 时调用 DeActivation 做梯度校正。注意 cForecast 与 NeuronOCL 都做了激活判断,说明这套结构允许末端挂不同激活,梯度链式法则不会硬套一种。 权重更新 updateInputWeights 只动了卷积层和预测层,转置层本身无独立可学权重——这点和标准自编码器不同,参数量倾向更轻。CreateEncoderDescriptions 里若 encoder 指针为空会 new 一个 CArrayObj,随后 Clear() 清空再填输入层描述,说明编码器结构是运行时动态装配的。 开 MT5 把这段贴进自定义神经网络类的 .mqh,断点打在 acTranspose[3].FeedForward 前后,能直接看到稀疏时序特征是否被压到最后一层;外汇与贵金属行情噪声大,这类轻权结构过拟合概率可能更低,但高频重训仍有滑点与人造样本风险。
if(!cConvolution.FeedForward(acTranspose[class="num">0].AsObject())) class="kw">return false; if(!SumAndNormilize(cConvolution.getOutput(), acTranspose[class="num">0].getOutput(), cConvolution.getOutput(), class="num">1, false)) class="kw">return false; if(!acTranspose[class="num">1].FeedForward(cConvolution.AsObject())) class="kw">return false; if(!cForecast.FeedForward(acTranspose[class="num">1].AsObject())) class="kw">return false; if(!acTranspose[class="num">2].FeedForward(cForecast.AsObject())) class="kw">return false; if(!acTranspose[class="num">3].FeedForward(acTranspose[class="num">2].AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronSparseTSF::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!acTranspose[class="num">2].calcHiddenGradients(acTranspose[class="num">3].AsObject())) class="kw">return false; if(!cForecast.calcHiddenGradients(acTranspose[class="num">2].AsObject())) class="kw">return false; if(cForecast.Activation() != None && !DeActivation(cForecast.getOutput(), cForecast.getGradient(), cForecast.getGradient(), cForecast.Activation())) class="kw">return false; if(!acTranspose[class="num">1].calcHiddenGradients(cForecast.AsObject())) class="kw">return false; if(!cConvolution.calcHiddenGradients(acTranspose[class="num">1].AsObject())) class="kw">return false; if(!acTranspose[class="num">0].calcHiddenGradients(cConvolution.AsObject())) class="kw">return false; if(!SumAndNormilize(cConvolution.getGradient(), acTranspose[class="num">0].getGradient(), acTranspose[class="num">0].getGradient(), class="num">1, false)) class="kw">return false; if(!NeuronOCL || !NeuronOCL.calcHiddenGradients(acTranspose[class="num">0].AsObject())) class="kw">return false; if(NeuronOCL.Activation() != None && !DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(), NeuronOCL.Activation())) class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronSparseTSF::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cConvolution.UpdateInputWeights(acTranspose[class="num">0].AsObject())) class="kw">return false; if(!cForecast.UpdateInputWeights(acTranspose[class="num">1].AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false;
编码器四层堆叠的写法与参数坑
这段 MT5 神经网络编码器用 CLayerDescription 逐层往 encoder 里塞结构,任何一层 Add 失败就 delete 并 return false,调用方必须检查返回值,否则悬空描述符会拖垮后续训练。 第一层基底用 defNeuronBaseOCL,节点数直接等于 HistoryBars * BarDescr,优化器选了 ADAM_MINI;紧接的 BatchNorm 层 batch 设成 1e4,在样本不足时可能让归一化统计量偏脆。 SparseTSF 层把窗口 window=BarDescr、步长 step=24 写死,输出长度交给 NForecast,这种稀疏时间采样对 24 根 bar 周期明显的贵金属行情可能更抓得住节律,但外汇秒级噪声下倾向过拟合。 末端的 FreDFOCL 层把 probability 固定为 0.7f、step 用 int(true) 即 1,属于频率域 dropout;外汇与贵金属杠杆高、回撤快,直接抄这套概率前务必在策略测试器用历史样本跑一遍鲁棒性。
descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM_MINI; if(!encoder.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(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSparseTSF; descr.count = HistoryBars; descr.window = BarDescr; descr.step = class="num">24; descr.window_out = NForecast; descr.activation = None; descr.optimization = ADAM_MINI; if(!encoder.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 = defNeuronRevInDenormOCL; descr.count = BarDescr * NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers = class="num">1; if(!encoder.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 = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
「用 EURUSD 实盘历史检验 SparseTSF 预测成色」
前面几节把 SparseTSF 的理论和 MQL5 实现铺完了,这一节直接拿真实历史数据撞一下预测能力,顺便看能不能用预测值反推 Actor 的出场进场策略。构建新模型时没动输入结构和预测目标,所以之前的环境交互、训练流程原封不动复用,连经验回放缓冲区都接着用,Encoder 只吃价格运动和不依赖 Actor 的指标,等于把整段历史一视同仁,轻量特性让训练资源和时间砍得很狠。 Encoder 训出来的状态预测谈不上多精准,但横向比那些吃更多算力的复杂模型,质量基本在同一档,只能算部分达成预期。真正意外在第二步:拿这些看着平平无奇的预测去迭代训 Actor,定期刷训练集保持行为分布跟手,结果无论在训练集还是测试集,策略都能挤出利润。 具体跑法是 EURUSD H1,指标全默认,用 2023 全年训,2024 年 1 月不变参测,尽量贴真实环境。测试期模型跑了 81 笔,多空 39/42 几乎对称,超 60% 胜率,盈利因子 1.33。SparseTSF 按原始周期单步预测,我们 Encoder 吃的是 24 小时周期的小时线,所以按小时拆很有看头。 图表里欧洲时段前半段 9:00–12:00 几乎零亏损,平均持仓 1 小时 6 分,进场到兑现延迟极小;美国时段开门 15:00–16:00 盈利性最猛。外汇高杠杆高风险,这套结果只是历史回测倾向,换周期或品种可能失效,开 MT5 用同参数复跑最实在。
◍ 轻量模型跑通了但别直接上实盘
SparseTSF 这套思路靠极简参数结构吃到了便宜:把周期内的每一步拆开、各自独立预测,模型在资源吃紧、决策窗口短的终端上也能跑。我们在 MT5 里用 MQL5 落地了这套路径,拿真实历史数据训完测完,训练集和测试集都出了正收益,说明方法本身不是玩具。 但必须泼盆冷水:这次写的程序只是验证「这条路能走通」的一种实现样本,不是拿去真金白银怼外汇或贵金属的成品。这类品种杠杆高、跳空频繁,轻量模型对突变样本的泛化能力还没经过压力验证。 如果你打算自己复现,建议先开 MT5 用历史中心数据跑一遍回测,把周期步长从默认调小一档,看测试集利润是否塌方——塌了就说明独立预测单元过拟合了。
收束
这套 LSTM 预测方案落地到 MT5,依赖八个文件协同:Research.mq5 与 ResearchRealORL.mq5 负责样本采集,Study.mq5 与 StudyEncoder.mq5 分别训练模型和编码器,Test.mq5 跑预测回测,Trajectory.mqh 定义系统状态,NeuroNet.mqh 与 NeuroNet.cl 提供神经网络类库和 OpenCL 内核。随文附带的 MQL5.zip 约 1522.93 KB,解压后可直接在 MetaEditor 编译调试。 外汇与贵金属市场高杠杆、高波动,神经网络预测仅提供概率倾向,实盘前务必用历史数据多周期验证,并自担模型失效风险。