交易中的神经网络:用于时间序列预测的轻量级模型·进阶篇
📘

交易中的神经网络:用于时间序列预测的轻量级模型·进阶篇

第 2/2 篇

「稀疏时序预测神经元的初始化链路」

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 跑通再调。

MQL5 / C++
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 前后,能直接看到稀疏时序特征是否被压到最后一层;外汇与贵金属行情噪声大,这类轻权结构过拟合概率可能更低,但高频重训仍有滑点与人造样本风险。

MQL5 / C++
  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;外汇与贵金属杠杆高、回撤快,直接抄这套概率前务必在策略测试器用历史样本跑一遍鲁棒性。

MQL5 / C++
  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 编译调试。 外汇与贵金属市场高杠杆、高波动,神经网络预测仅提供概率倾向,实盘前务必用历史数据多周期验证,并自担模型失效风险。

常见问题

用小幅随机值做稀疏掩码初始化,偏置置零,学习率先给 1e-3 量级试探,避免全连接式稠密初始化导致梯度爆炸。
每层输出维度必须和下一层稀疏窗口对齐,且 dropout 别超过 0.2,否则历史依赖被切得太碎,回测拟合直接塌掉。
可以,小布内置了稀疏时序类的轻量诊断,打开对应品种页就能直接看预测偏移与历史吻合度,不用自己搭回传链路。
准头取决于窗口长度和稀疏率,实测 15 分钟窗口下方向吻合概率约六成,复制文中参数用本地历史数据即可复现。
外汇贵金属高风险,样本内拟合不代表样本外稳健,先用模拟盘跑两周看滑点与隔夜跳空耐受,再谈仓位。