在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易·进阶篇
(2/3)·指标周期选小撞噪音、选大晚信号,这篇用全周期并行与降维给出进阶解法
「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),就能验证句柄是否拿到。外汇和贵金属杠杆高,指标初始化失败会直接让后续信号逻辑失效,实盘前务必在策略测试器跑一遍。
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返回空向量。
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。
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() 看能否正常吐出报价。
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 分钟波幅,外汇和贵金属杠杆高,波幅验证前先确认品种已开盘。
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。这意味着报价永远新鲜,但高频循环里会多耗一点内核调用;若策略是秒级扫单,倾向把报价也缓存起来定时刷新。 析构函数目前为空实现,说明这个类没有在堆上持资或开句柄,销毁时无清理负担。外汇与贵金属杠杆高,实盘前务必在策略测试器用不同品种验证手数边界,爆仓风险始终存在。
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() { }