《数据科学与机器学习(第25部分):使用循环神经网络(RNN)进行外汇时间序列预测》·综合运用
📘

《数据科学与机器学习(第25部分):使用循环神经网络(RNN)进行外汇时间序列预测》·综合运用

第 3/3 篇

「把RNN模型塞进EA的第一步」

在MT5里跑一个训练好的循环神经网络,第一步是把模型文件和标准化参数作为资源编译进EA。ONNX格式的RNN权重、均值向量、缩放向量三者缺一不可,否则Init阶段就会返回INIT_FAILED。 训练时只用了10个自变量,实盘取数绝不能多也不能少,否则张量维度对不上,推理直接报错。下面这段代码演示了资源声明与OnInit里的加载动作,注意scaler的均值和标准差是从二进制文件读的,不是写死在代码里。 GetInputData函数负责按bars数量抓OHLC加上均线、标准差和三个时间字段,共10列。start_bar默认1意味着从倒数第二根K线开始取,避免用到还没收盘的当前柱。外汇和贵金属杠杆高,这类模型信号只是概率倾向,实盘前务必在策略测试器用D1数据跑一遍。

MQL5 / C++
class="macro">#resource "\Files\rnn.EURUSD.D1.onnx" as class="type">uchar onnx_model[]; class=class="str">"cmt">//rnn model in onnx format
class="macro">#resource "\Files\standard_scaler_mean.bin" as class="type">class="kw">double standardization_mean[];
class="macro">#resource "\Files\standard_scaler_scale.bin" as class="type">class="kw">double standardization_std[];
class="macro">#include <MALE5\Recurrent Neural Networks(RNNs)\RNN.mqh>
CRNN rnn;
class="macro">#include <MALE5\preprocessing.mqh>
StandardizationScaler *scaler;
vector classes_in_data_ = {class="num">0,class="num">1}; class=class="str">"cmt">//we have to assign the classes manually | it is very important that their order is preserved as they can be seen in python code, HINT: They are usually in ascending order
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {

class=class="str">"cmt">//--- Initialize ONNX model

   if (!rnn.Init(onnx_model))
     class="kw">return INIT_FAILED;

class=class="str">"cmt">//--- Initializing the scaler with values loaded from binary files 
   scaler = new StandardizationScaler(standardization_mean, standardization_std);

class=class="str">"cmt">//--- Initializing the CTrade library for executing trades
   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(Symbol());

   lotsize = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);

class=class="str">"cmt">//--- Initializing the indicators
   ma_handle = iMA(Symbol(),timeframe,class="num">30,class="num">0,MODE_SMA,PRICE_WEIGHTED); class=class="str">"cmt">//The Moving averaege for class="num">30 days
   stddev_handle = iStdDev(Symbol(), timeframe, class="num">7,class="num">0,MODE_SMA,PRICE_WEIGHTED); class=class="str">"cmt">//The standard deviation for class="num">7 days

   class="kw">return(INIT_SUCCEEDED);
  }
matrix GetInputData(class="type">int bars, class="type">int start_bar=class="num">1)
{
   vector open(bars),
         high(bars),
         low(bars),
         close(bars),
         ma(bars),
         stddev(bars),
         dayofmonth(bars),
         dayofweek(bars),
         dayofyear(bars),
         month(bars);
class=class="str">"cmt">//--- Getting OHLC values

   open.CopyRates(Symbol(), timeframe, COPY_RATES_OPEN, start_bar, bars);
   high.CopyRates(Symbol(), timeframe, COPY_RATES_HIGH, start_bar, bars);
   low.CopyRates(Symbol(), timeframe, COPY_RATES_LOW, start_bar, bars);
   close.CopyRates(Symbol(), timeframe, COPY_RATES_CLOSE, start_bar, bars);

◍ 把K线与日历特征塞进RNN输入矩阵

这段逻辑干的事很直接:从当前图表抓取最近若干根K线的开高低收,再叠加重算的MA与标准差,最后把时间拆成日、周几、年中第几天、月这四个日历维度,拼成固定 10 列的矩阵喂给模型。 先看数据收集部分。time_vector 用 CopyRates 只拉时间戳,ma 和 stddev 分别用 CopyIndicatorBuffer 取指标缓冲区 0 列;循环里把秒级时间转成 string 再转回 datetime 结构,把 day/day_of_week/day_of_year/mon 分别写进四个数组。这里 10 个输入列是写死的,改特征就得同步改 matrix 声明里的列数。 数据矩阵按列塞入:0~3 是 OHLC,4~5 是均线和中轨偏离,6~9 是上面四个时间特征。GetInputData 返回的 matrix 在 OnTick 里先过 StandardScaler 再做 predict_bin,信号 1 倾向多、0 倾向空,并在图表用 Comment 打印。 外汇与贵金属杠杆高,RNN 给的信号只是概率倾向,实盘前务必用策略测试器跑样本外数据验证 scaler 与模型是否匹配当前品种波动结构。

MQL5 / C++
  vector time_vector;
  time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars);

class=class="str">"cmt">//---

  ma.CopyIndicatorBuffer(ma_handle, class="num">0, start_bar, bars); class=class="str">"cmt">//getting moving avg values 
  stddev.CopyIndicatorBuffer(stddev_handle, class="num">0, start_bar, bars); class=class="str">"cmt">//getting standard deviation values

  class="type">class="kw">string time = "";
  for (class="type">int i=class="num">0; i<bars; i++) class=class="str">"cmt">//Extracting time features 
    {
      time = (class="type">class="kw">string)class="type">class="kw">datetime(time_vector[i]); class=class="str">"cmt">//converting the data from seconds to date then to class="type">class="kw">string
      TimeToStruct((class="type">class="kw">datetime)StringToTime(time), date_time_struct); class=class="str">"cmt">//convering the class="type">class="kw">string time to date then assigning them to a structure
      
      dayofmonth[i] = date_time_struct.day;
      dayofweek[i] = date_time_struct.day_of_week;
      dayofyear[i] = date_time_struct.day_of_year;
      month[i] = date_time_struct.mon;
    }

  matrix data(bars, class="num">10); class=class="str">"cmt">//we have class="num">10 inputs from rnn | this value is fixed

class=class="str">"cmt">//--- adding the features into a data matrix

  data.Col(open, class="num">0);
  data.Col(high, class="num">1);
  data.Col(low, class="num">2);
  data.Col(close, class="num">3);
  data.Col(ma, class="num">4);
  data.Col(stddev, class="num">5);
  data.Col(dayofmonth, class="num">6);
  data.Col(dayofweek, class="num">7);
  data.Col(dayofyear, class="num">8);
  data.Col(month, class="num">9);

  class="kw">return data;
}
class="type">void OnTick()
  {
class=class="str">"cmt">//---

  if (NewBar()) class=class="str">"cmt">//Trade at the opening of a new candle
    {
      matrix input_data_matrix = GetInputData(rnn_time_step);
      input_data_matrix = scaler.transform(input_data_matrix); class=class="str">"cmt">//applying StandardSCaler to the input data
      
      class="type">int signal = rnn.predict_bin(input_data_matrix, classes_in_data_); class=class="str">"cmt">//getting trade signal from the RNN model

      Comment("Signal==",signal);

  class=class="str">"cmt">//---

      class="type">MqlTick ticks;
      SymbolInfoTick(Symbol(), ticks);
      
      if (signal==class="num">1) class=class="str">"cmt">//if the signal is bullish
       {
        if (!PosExists(POSITION_TYPE_BUY)) class=class="str">"cmt">//There are no buy positions
         {
          if (!m_trade.Buy(lotsize, Symbol(), ticks.ask, ticks.bid-stoploss*Point(), ticks.ask+takeprofit*Point())) class=class="str">"cmt">//Open a buy trade
            printf("Failed to open a buy position err=%d",GetLastError());
         }
       }
      else if (signal==class="num">0) class=class="str">"cmt">//Bearish signal
        {
        if (!PosExists(POSITION_TYPE_SELL)) class=class="str">"cmt">//There are no Sell positions

下空单时止损止盈的挂法

在 MT5 里用 CTrade 对象发卖单,止损和止盈要以点数乘以 Point 换算到绝对价格,不能直接填整数点数。上面这段代码片段把卖单止损放在 ask 加上 stoploss 个点、止盈放在 bid 减去 takeprofit 个点,符合卖单亏损在上方、盈利在下方的逻辑。 如果 Sell 返回 false,说明下单失败,此时用 GetLastError 把错误码打印出来,便于排查点差超限或保证金不足。注意外汇和贵金属杠杆高,实盘点差跳动可能让挂单价瞬间失效,任何信号都只是概率倾向,不是确定性。 开 MT5 把这段接进你的 EA,把 stoploss、takeprofit 先设成 50、100 点跑回测,看成交拒绝率再决定是否收紧。

MQL5 / C++
if (!m_trade.Sell(lotsize, Symbol(), ticks.bid, ticks.ask+stoploss*Point(), ticks.bid-takeprofit*Point())) class=class="str">"cmt">//open a sell trade
   printf("Failed to open a sell position err=%d",GetLastError());
   }
else class=class="str">"cmt">//There was an error
   class="kw">return;
  }
 }

「RNN EA 在测试器里的真实成绩」

把前面定好的策略丢进 MT5 策略测试器,我用的是和 LightGBM 那版相同的止损止盈与测试器配置,方便直接对比。RNN 模型跑完 561 笔交易,盈利占比 44.56%,净利润只有 100 美元;而同条件下 LightGBM 净利润到了 572 美元,时间序列预测上简单 RNN 大概率打不过树模型。 我顺手做了一次优化扫描找更合适的止损止盈组合,其中一组较优解是止损 1000 点、止盈 700 点,比初始的 500/700 更扛波动。外汇与贵金属杠杆高,这类回测结果只代表历史样本,实盘可能明显偏离。 下面这段是 EA 里 RNN 相关的输入参数,注意 rnn_time_step 必须和 python 训练时一致,否则加载的权重对不上: input group "rnn"; // 输入参数分组标记为 rnn input uint rnn_time_step = 7; // 时间步长,必须和 python 训练脚本里用的数值相同 input ENUM_TIMEFRAMES timeframe = PERIOD_D1; // 所用周期,这里设成日线 input int magic_number = 1945; // 订单魔法码,用于区分本 EA 开的仓 input int slippage = 50; // 允许最大滑点(点) input int stoploss = 500; // 初始止损距离(点) input int takeprofit = 700; // 初始止盈距离(点)

MQL5 / C++
input group "rnn";
input class="type">uint rnn_time_step = class="num">7; 
class=class="str">"cmt">//this value must be the same as the one used during training in a python script
input ENUM_TIMEFRAMES timeframe = PERIOD_D1;
input class="type">int magic_number = class="num">1945;
input class="type">int slippage = class="num">50;
input class="type">int stoploss = class="num">500;
input class="type">int takeprofit = class="num">700;

◍ 简单RNN为什么适合盯盘序列

做价格行为分析时,K线本质是带时间顺序的序列,简单RNN天生为这种数据设计,比把每根bar当独立样本喂进普通网络更贴合盘面逻辑。它在不同时间步共享同一套权重,参数规模远小于逐根单独建模的架构,跑在本地MT5配套的Python环境里也不会轻易吃光内存。 这类网络能抓时间依赖性,对短期节奏尤其敏感——比如欧盘开盘后三根15分钟K的连贯拉伸,RNN倾向把这种局部惯性当成特征而不是噪声。序列长度可变,你用30根或120根历史去推下一根都行,不用提前统一截断。 架构本身不复杂,几行核心循环就能搭出来,适合先跑通再换LSTM。但外汇和贵金属杠杆高、跳空频繁,RNN在历史分布外容易失效,实盘前务必用 Tick 级回测验证。

RNN和LightGBM的对照别当真

这篇把简单循环神经网络在 MQL5 里的部署讲透了,过程中反复拿它和前一篇的 LightGBM 做对比。但得泼盆冷水:这两种模型结构和预测逻辑根本不同,文中任何对比结论都只是作者或读者一时的直觉,不能当真拿去用。 具体到输入数据,RNN 这版砍掉了 DIFF_LAG1_OPEN、DIFF_LAG1_HIGH、DIFF_LAG1_LOW、DIFF_LAG1_CLOSE 这几个滞后价差变量。原始数据集里本就没有非滞后的开高低收差值,所以干脆不喂给网络,也没让它自己学滞后性。 随文给的仓库里能拽到可直接跑的件:RNN timeseries forecasting.mq5 是加载 ONNX 模型测策略的 EA,rnn.EURUSD.D1.onnx 是训练好的模型,standard_scaler_mean.bin 配 standard_scaler_scale.bin 做标准化,preprocessing.mqh 和 RNN.mqh 是库,rnns-for-forex-forecasting-tutorial.ipynb 收了全部 Python 代码。开 MT5 把 EA 拖上 EURUSD 日线,就能验证这套 RNN 推理链通不通。 外汇和贵金属杠杆高、滑点狠,模型回测飘红不代表实盘能活,跑之前先想清楚自己扛不扛得住回撤。

常见问题

先导出训练好的模型权重和归一化参数,再在EA初始化时加载,避免每次 tick 重新推理。
把OHLC和星期、小时编码后按时间步横向拼接,每根K线作为一行,时间窗决定矩阵高度。
小布可在品种页直接叠出RNN倾向的概率带,你只需核对与价格行为是否共振,不用自己跑脚本。
只能看概率倾向,外汇高风险,实盘前需用最近三个月数据重验,且点差要按真实账户设。
别当真对照结论,RNN抓序列依赖更顺手,LightGBM偏静态特征,实盘各有失效场景。