在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易·进阶篇
⚙️

在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易·进阶篇

(2/3)·指标周期选小撞噪音、选大晚信号,这篇用全周期并行与降维给出进阶解法

含代码示例实战向 第 2/3 篇
把 WPR 或 RSI 锁死在单周期,是多数 EA 钝化的隐形根因。周期调小追着噪音来回打脸,调大等信号出来行情已跑完一半。本篇直接绕开择周期,把所有可用周期一起喂给模型。

「WPR 与 Time 两个类的初始化拆解」

这段 MT5 代码把威廉指标(WPR)和时间对象各自封装成类,构造时就把句柄和当前 K 线时间抓到手。WPR 构造函数里把外部传入的 user_period 赋给 indicator_period,再调用 iWPR 拿指标句柄,最后 Print(user_feedback(0)) 给初始化回执。 析构函数 WPR::~WPR 用 IndicatorRelease 释放句柄,成功后将两个初始化标志位复位成 false 并打印 user_feedback(5),避免残留资源占用。 Time 类更轻量:构造时记录 selected_symbol 与 selected_time_frame,并用 iTime(symbol,tf,0) 把 current_time 和 time_stamp 都设为 0 号 K 线开盘时间;它的析构函数是空的,目前不做事。 开 MT5 建个 EA 把这两段粘进去,把 user_period 改成 14,编译后看初始化日志有没有打出 feedback(0),就能验证句柄是否拿到。外汇和贵金属杠杆高,指标初始化失败会直接让后续信号逻辑失效,实盘前务必在策略测试器跑一遍。

MQL5 / C++
  indicator_period                  = user_period;
  indicator_handler                  = iWPR(indicator_symbol,indicator_time_frame,indicator_period);
class=class="str">"cmt">//--- Give the user feedback on initilization
  Print(user_feedback(class="num">0));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our destructor for our Indicator class                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void WPR::~WPR()
  {
class=class="str">"cmt">//--- Free up resources we don&class="macro">#x27;t need and reset our flags
  if(IndicatorRelease(indicator_handler))
    {
      indicator_differenced_values_initialized = false;
      indicator_values_initialized = false;
      Print(user_feedback(class="num">5));
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Time.mqh |
class=class="str">"cmt">//|                                                       Gamuchirai Ndawana |
class=class="str">"cmt">//|                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class Time
  {
class="kw">private:
   class="type">class="kw">datetime       time_stamp;
   class="type">class="kw">datetime       current_time;
   class="type">class="kw">string         selected_symbol;
   ENUM_TIMEFRAMES   selected_time_frame;
class="kw">public:
                     Time(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame);
   class="type">bool           NewCandle(class="type">void);
                    ~Time();
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create our time object                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
Time::Time(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame)
  {
   selected_time_frame = user_time_frame;
   selected_symbol = user_symbol;
   current_time = iTime(user_symbol,selected_time_frame,class="num">0);
   time_stamp   = iTime(user_symbol,selected_time_frame,class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our destructor is currently empty                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
Time::~Time()
  {
  }

K线闭合判定与ONNX浮点模型封装

在MT5里跑任何基于Bar的AI推理,第一步都得确认当前K线是否已经走完,否则用半截收盘价喂给模型只会得到漂移的预测。下面这段NewCandle逻辑就是干这事:拿iTime取0号K线(最新一根)的开盘时间,跟上次记录的time_stamp比对,不一致就说明新蜡烛完全成形,更新戳并返回true。 bool Time::NewCandle(void) { current_time = iTime(selected_symbol,selected_time_frame,0); if(time_stamp != current_time) { time_stamp = current_time; return(true); } return(false); } 逐行看:第3行取指定品种和周期的最新K线时间;第5行比对旧时间戳;若不同,第7行刷新时间戳、第8行返回真,代表新K线闭合;否则第11行返回假。实盘里把selected_time_frame设成PERIOD_M15,就能在15分钟线每根收线后触发一次推理。 紧接着是一个ONNXFloat类骨架,专门用来在MQL5里加载并调用浮点精度的ONNX模型。私有成员onnx_model是模型句柄(long型),onnx_outputs记录输出数量;公开方法覆盖了模型有效性检查、输入输出形状定义(按index/stacks/params三维指定)、以及核心的Predict——接收vectorf输入向量返回同类型预测值。 构造函数ONNXFloat(const uchar &user_proto[])接收序列化后的模型字节流,意味着你可以把.onnx文件用FileReadArray读进uchar数组再传进来,不依赖外部路径。外汇与贵金属杠杆高、滑点随机,这类本地推理只降低滞后,不消除方向误判概率,参数shape填错会直接让Predict返回空向量。

MQL5 / C++
class="type">bool Time::NewCandle(class="type">void)
  {
   current_time = iTime(selected_symbol,selected_time_frame,class="num">0);
   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      class="kw">return(true);
     }
   class="kw">return(false);
  }

class ONNXFloat
  {
class="kw">private:
   class="type">long               onnx_model;
   class="type">int                onnx_outputs;
class="kw">public:
   class="type">bool               OnnxModelIsValid(class="type">void);
   class="type">bool               DefineOnnxInputShape(class="type">int n_index,class="type">int n_stacks,class="type">int n_input_params);
   class="type">bool               DefineOnnxOutputShape(class="type">int n_index,class="type">int n_stacks, class="type">int n_output_params);
   vectorf            Predict(const vectorf &model_inputs);
                       ONNXFloat(const class="type">uchar &user_proto[]);
                      ~ONNXFloat();
  };

ONNXFloat::ONNXFloat(const class="type">uchar &user_proto[])
  {

◍ 把 ONNX 模型接进 MT5 的骨架代码

在 MT5 里跑外部训练的波动率模型,第一步是把模型缓冲载入为浮点 ONNX 句柄,并立刻校验有效性。下面这段类方法用 OnnxCreateFromBuffer 从内存原型创建模型,成功会打印函数签名与对象地址,失败则抛出 GetLastError 码,方便你定位是模型格式不对还是缓冲区损坏。 析构函数只做一件事:OnnxRelease 释放句柄,避免 EA 重载时句柄泄漏。中间 OnnxModelIsValid 用 INVALID_HANDLE 判断,比捕获异常更轻量,适合高频调用。 输入输出维度必须显式设定。DefineOnnxInputShape 接收 (n_stacks, n_input_params) 二维形状,DefineOnnxOutputShape 则把 (n_output_params, n_stacks) 写回并把输出列数存进 onnx_outputs 成员。注意原文输出函数里打印文案把 input/output 写反了,复制时建议自己改对,不然日志排查会绕弯。 外汇与贵金属杠杆高、滑点随机,这类模型推理只是概率参考,实盘前务必在策略测试器用历史 tick 跑通形状设定,确认不会在 OnnxSetInputShape 返回 false。

MQL5 / C++
  onnx_model = OnnxCreateFromBuffer(user_proto,ONNX_DATA_TYPE_FLOAT);
  if(OnnxModelIsValid())
     Print("Volatility Doctor ONNXFloat Class Loaded Correctly: ",__FUNCSIG__," ",&this);
  else
     Print("Failed To Create The specified ONNX model: ",GetLastError());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our ONNXFloat class destructor                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
ONNXFloat::~ONNXFloat()
  {
  OnnxRelease(onnx_model);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| A method that returns true if our ONNXFloat model is valid          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ONNXFloat::OnnxModelIsValid(class="type">void)
  {
class=class="str">"cmt">//--- Check if the model is valid
  if(onnx_model != INVALID_HANDLE)
     class="kw">return(true);
class=class="str">"cmt">//--- Something went wrong
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the input shape of our ONNXFloat model                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ONNXFloat::DefineOnnxInputShape(class="type">int n_index,class="type">int n_stacks,class="type">int n_input_params)
  {
  const class="type">ulong model_input_shape[] = {n_stacks,n_input_params};
  if(OnnxSetInputShape(onnx_model,n_index,model_input_shape))
    {
     Print("Succefully specified ONNX model output shape: ",__FUNCTION__," ",&this);
     class="kw">return(true);
    }
class=class="str">"cmt">//--- Something went wrong
  Print("Failed to set the passed ONNX model output shape: ",GetLastError());
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the output shape of our model                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ONNXFloat::DefineOnnxOutputShape(class="type">int n_index,class="type">int n_stacks,class="type">int n_output_params)
  {
  const class="type">ulong model_output_shape[] = {n_output_params,n_stacks};
  onnx_outputs = n_output_params;
  if(OnnxSetOutputShape(onnx_model,n_index,model_output_shape))
    {
     Print("Succefully specified ONNX model input shape: ",__FUNCSIG__," ",&this);
     class="kw">return(true);
    }
class=class="str">"cmt">//--- Something went wrong
  Print("Failed to set the passed ONNX model input shape: ",GetLastError());
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get a prediction from our model                                    |

「ONNX预测失败兜底与交易品种信息封装」

把 ONNX 模型跑批的核心入口写成一个 Predict 方法,输入是 float 向量、输出也是 float 向量。OnnxRun 成功就直接回传模型输出;一旦失败,代码不走异常而是填一个 {10e8} 的向量当哨兵值,同时往图表和日志丢错误提示。这个 10e8 在后续逻辑里可以当成『模型不可用』的硬判断条件,避免拿垃圾推理结果去下单。 下面这段 TradeInfo 类解决的是另一个务实问题:EA 启动时把品种的最小/最大手数、手数步长、ask/bid/收盘价一次性缓存下来。外汇和贵金属杠杆高、点差跳变快,每次下单前现查 SymbolInfoDouble 不如构造时存好,回测和实盘切换品种时只需传 selected_symbol 和 timeframe。 构造函数里只做了两件事——把外部传入的 symbol 和周期赋给私有成员,具体手数参数在别的方法里懒加载。想验证的话,开 MT5 新建 EA,把这两段贴进头文件,在 OnTick 里调 TradeInfo("XAUUSD",PERIOD_M5).GetBid() 看能否正常吐出报价。

MQL5 / C++
vectorf ONNXFloat::Predict(const vectorf &model_inputs)
  {
   vectorf model_output(onnx_outputs);
   if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output))
     {
       vectorf res = model_output;
       class="kw">return(res);
     }
   Comment("Failed to get a prediction from our ONNX model");
   Print("ONNX Run Failed: ",GetLastError());
   vectorf res = {class="num">10e8};
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TradeInfo.mqh |
class=class="str">"cmt">//|                                                                 Gamuchirai Ndawana |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class TradeInfo
  {
class="kw">private:
                        class="type">class="kw">string user_symbol;
                        ENUM_TIMEFRAMES user_time_frame;
                        class="type">class="kw">double min_volume,max_volume,volume_step;
class="kw">public:
                        TradeInfo(class="type">class="kw">string selected_symbol,ENUM_TIMEFRAMES selected_time_frame);
                        class="type">class="kw">double MinVolume(class="type">void);
                        class="type">class="kw">double MaxVolume(class="type">void);
                        class="type">class="kw">double VolumeStep(class="type">void);
                        class="type">class="kw">double GetAsk(class="type">void);
                        class="type">class="kw">double GetBid(class="type">void);
                        class="type">class="kw">double GetClose(class="type">void);
                        class="type">class="kw">string GetSymbol(class="type">void);
                        ~TradeInfo();
  };

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The constructor will load our symbol information                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
TradeInfo::TradeInfo(class="type">class="kw">string selected_symbol,ENUM_TIMEFRAMES selected_time_frame)
  {
   class=class="str">"cmt">//--- Which symbol are you interested in?
   user_symbol = selected_symbol;
   user_time_frame = selected_time_frame;
  }

把品种交易参数和OHLC抓进类里

在 MT5 自定义类里,先要用 SymbolSelect 把目标品种拉进市场观察表,失败就打印错误码,成功才去读成交量边界。这一步决定了后面所有 iClose / iOpen 调用会不会返回乱值。 下面这段是核心装载逻辑:SymbolSelect 返回 true 后,用 SymbolInfoDouble 取 SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX、SYMBOL_VOLUME_STEP 三个双精度值,分别对应最小开仓量、最大开仓量和手数步长。以 XAUUSD 为例,常见 min_volume=0.01、volume_step=0.01,若硬填 0.015 会被服务端拒单。 读取完参数,类方法直接封装 iClose / iOpen / iHigh / iLow,全部带 user_symbol 和 user_time_frame 以及偏移 0(当前未完成 K 线)。GetSymbol 则原样吐回品种名,方便上层做多品种调度。 让小布替你跑这套 把上面代码贴进 MT5 的 TradeInfo.mqh,在 EA 里 new 一个对象并传 EURUSD、PERIOD_M15,Print 一下 GetHigh()-GetLow(),就能立刻看到当前 15 分钟波幅,外汇和贵金属杠杆高,波幅验证前先确认品种已开盘。

MQL5 / C++
if(SymbolSelect(user_symbol,true))
  {
   class=class="str">"cmt">//--- Load symbol details
   min_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MIN);
   max_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MAX);
   volume_step = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_STEP);
   Print("Trade Info Loaded Successfully: ",__FUNCSIG__);
  }
else
  {
   Print("Error Symbol Information Could Not Be Found For: ",selected_symbol," ",GetLastError());
  }

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the close of the selected symbol                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double TradeInfo::GetClose(class="type">void)
  {
   class="type">class="kw">double res = iClose(user_symbol,user_time_frame,class="num">0);
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the open of the selected symbol                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double TradeInfo::GetOpen(class="type">void)
  {
   class="type">class="kw">double res = iOpen(user_symbol,user_time_frame,class="num">0);
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the high of the selected symbol                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double TradeInfo::GetHigh(class="type">void)
  {
   class="type">class="kw">double res = iHigh(user_symbol,user_time_frame,class="num">0);
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the low of the selected symbol                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double TradeInfo::GetLow(class="type">void)
  {
   class="type">class="kw">double res = iLow(user_symbol,user_time_frame,class="num">0);
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the selected symbol                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string TradeInfo::GetSymbol(class="type">void)
  {
   class="type">class="kw">string res = user_symbol;
   class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把交易品种的量价边界封装进类方法

在 MT5 自建交易辅助类时,把品种的量价约束和实时报价收敛成几个轻量成员函数,能让上层逻辑直接调用而不必反复查终端属性。下面这组方法覆盖了手数步长、最小最大手数,以及即时买价卖价三类最常见查询。 VolumeStep 直接返回缓存好的 volume_step 字段,MinVolume 与 MaxVolume 同理分别吐出 min_volume、max_volume。若你在 EA 里要校验下单手数,用这几个返回值做区间夹取即可,避免触发『无效交易量』报错。 GetAsk 与 GetBid 没有走缓存,而是每次调用 SymbolInfoDouble 抓 SYMBOL_ASK / SYMBOL_BID。这意味着报价永远新鲜,但高频循环里会多耗一点内核调用;若策略是秒级扫单,倾向把报价也缓存起来定时刷新。 析构函数目前为空实现,说明这个类没有在堆上持资或开句柄,销毁时无清理负担。外汇与贵金属杠杆高,实盘前务必在策略测试器用不同品种验证手数边界,爆仓风险始终存在。

MQL5 / C++
class="type">class="kw">double TradeInfo::VolumeStep(class="type">void)
  {
    class="type">class="kw">double res = volume_step;
    class="kw">return(res);
  }
class="type">class="kw">double TradeInfo::MinVolume(class="type">void)
  {
    class="type">class="kw">double res = min_volume;
    class="kw">return(res);
  }
class="type">class="kw">double TradeInfo::MaxVolume(class="type">void)
  {
    class="type">class="kw">double res = max_volume;
    class="kw">return(res);
  }
class="type">class="kw">double TradeInfo::GetAsk(class="type">void)
  {
    class="kw">return(SymbolInfoDouble(GetSymbol(),SYMBOL_ASK));
  }
class="type">class="kw">double TradeInfo::GetBid(class="type">void)
  {
    class="kw">return(SymbolInfoDouble(GetSymbol(),SYMBOL_BID));
  }
TradeInfo::~TradeInfo()
  {
  }
把多周期特征提取交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多周期动量热力,你只需确认降维后的簇结构是否清晰,把重复劳动交给小布,你专注决策。

常见问题

WPR 以 0 到 -100 固定边界表达动量,跨周期归一更直观,子类继承同一缓冲区父类后,多周期取值无需重写计算逻辑。
目前小布品种页提供的是特征可视化与预警,模型推理需在 MT5 端跑;但导出的多周期 WPR 矩阵可贴进小布做人工复核。
公共缓冲区与生命周期集中在父类管理,子类只持差异参数,EA 在切换品种时析构更干净,避免历史句柄堆积拖慢回测。
看低维流形里看涨簇与看跌簇的分离度,重叠区大说明周期间矛盾强,应降仓或观望,外汇贵金属杠杆高,这种冲突期亏损概率偏大。