在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易(基础篇)
「用全周期替代单一参数猜谜」
技术指标天然带滞后,这是老问题。更隐蔽的坑在周期参数:周期太小,指标啃进来的全是市场噪音;周期太大,等信号出来行情已经跑完大半。两种极端都会让交易机会溜走,实盘绩效直接掉档。 原文作者给出的路数不挑一个「最优周期」,而是把可用周期全用上。具体靠降维算法 UMAP(一致流形逼近与投影)把多周期描述的数据压成更有信息量的低维表示,比裸数据能看出更多结构。 这套实现要在 MQL5 里写 4 个类,其中有一个专门封装 OOP 逻辑,用来快速接 OCPP 之外的 ONNX 模型推理。外汇和贵金属杠杆高、滑点跳空频繁,多周期并行计算对内存和命名空间管理要求更硬,直接抄单周期写法容易爆内存。 开 MT5 建个空 EA,先按文中思路把 Period 参数从单值改成数组传入,跑一晚回测看噪音信号占比变化,比空谈滞后更有用。
用单缓冲区父类统一 WPR 与 RSI 的接口
把 RSI 换成威廉指标(WPR)后,第一件事不是重写逻辑,而是抽出一个叫 SingleBufferIndicator 的父类。WPR 和 RSI 都只有 1 个缓冲区,让两个指标类都继承它,公共方法(取读数、取差分、判句柄有效性)就只在父类写一次。以后要加新功能,改 SingleBufferIndicator.mqh 再编译子类即可,不必逐个动子类。 WPR 数值区间固定为 0 到 -100:0 到 -20 通常偏空,-80 到 -100 通常偏多。它的核心是把现价和周期内最高价比。父类里 SetIndicatorValues 有两个参数,buffer_size 控制拷贝多少根,set_as_series 为 true 时数据按过去到当前排;SetDifferencedIndicatorValues 则多一个 differencing_period,把指标做差分后再入缓冲区——机器学习里变量的变化量往往比原始值更有信息量。 读数的出口也分三层:GetReadingAt 拿原始显示值,GetDifferencedReadingAt 拿差分后的值,GetCurrentReading 专门返回索引 0 的当前值。句柄是否加载成功由类内标志判断,这层安全检查能挡掉不少运行时报错。 继承语法在 MQL5 里就是类名后加冒号接父类。WPR 的构造函数按品种、周期、WPR 周期参数化,和 RSI 的签名不同,所以各自保留自己的构造。析构函数负责重置状态并释放指标句柄,资源清理交给类,比手动重复写释放代码稳。 下面这段是父类的头文件骨架,注意四个公共方法签名就是上面说的读写入口,没有暴露原始缓冲区成员——强制走方法才符合封装初衷。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SingleBufferIndicator.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 SingleBufferIndicator { class="kw">public: class=class="str">"cmt">//--- Class methods class="type">bool SetIndicatorValues(class="type">int buffer_size,class="type">bool set_as_series); class="type">class="kw">double GetReadingAt(class="type">int index); class="type">bool SetDifferencedIndicatorValues(class="type">int buffer_size,class="type">int differencing_period,class="type">bool set_as_series); class="type">class="kw">double GetDifferencedReadingAt(class="type">int index); class="type">class="kw">double GetCurrentReading(class="type">void);
◍ 单缓冲指标类的内部状态与取值实现
在 MT5 里封装一个单缓冲指标类,先得把它的内部状态声明清楚。除了指标句柄 indicator_handler,还要记录缓冲区大小、时间框架、应用价格与品种,以及两份布尔标记 indicator_values_initialized 和 indicator_differenced_values_initialized,用来判断数据是否真正拷进数组。 SetIndicatorValues 是实际把指标值拉进缓冲区的入口。它接收 buffer_size 与 set_as_series 两个参数,先写死 indicator_buffer_size,再调用 CopyBuffer 从句柄偏移 0 拷贝 buffer_size 根 K 线数据到 indicator_reading。若 set_as_series 为真,则用 ArraySetAsSeries 把数组按时间序列排布,最后把 initialized 标记置 true。 别只信 CopyBuffer 返回成功就完事。代码里额外用 vector 的 CopyIndicatorBuffer 再取一遍同区间数据,若 Sum() 等于 0 直接 return(false)——这能拦掉句柄失效但拷贝‘假装’成功的坑。外汇与贵金属行情跳空频繁,这类双重校验在实盘高风险环境下尤其值得保留。 下面这段是类声明与取值函数的核心片段,逐行拆开看更直观。
class=class="str">"cmt">//--- Have the indicator values been copied to the buffer? class="type">bool indicator_values_initialized; class="type">bool indicator_differenced_values_initialized; class=class="str">"cmt">//--- How far into the future we wish to forecast class="type">int forecast_horizon; class=class="str">"cmt">//--- The buffer for our indicator class="type">class="kw">double indicator_reading[]; vector indicator_differenced_values; class=class="str">"cmt">//--- The current size of the buffer the user last requested class="type">int indicator_buffer_size; class="type">int indicator_differenced_buffer_size; class=class="str">"cmt">//--- The handler for our indicator class="type">int indicator_handler; class=class="str">"cmt">//--- The time frame our indicator should be applied on ENUM_TIMEFRAMES indicator_time_frame; class=class="str">"cmt">//--- The price should the indicator be applied on ENUM_APPLIED_PRICE indicator_price; class=class="str">"cmt">//--- Give the user feedback class="type">class="kw">string user_feedback(class="type">int flag); class=class="str">"cmt">//--- The Symbol our indicator should be applied on class="type">class="kw">string indicator_symbol; class=class="str">"cmt">//--- Our period class="type">int indicator_period; class=class="str">"cmt">//--- Is our indicator valid? class="type">bool IsValid(class="type">void); class=class="str">"cmt">//---- Testing the Single Buffer Indicator Class class=class="str">"cmt">//--- This method should be deleted in production class="kw">virtual class="type">void Test(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set our indicator values and our buffer size | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SingleBufferIndicator::SetIndicatorValues(class="type">int buffer_size,class="type">bool set_as_series) { class=class="str">"cmt">//--- Buffer size indicator_buffer_size = buffer_size; CopyBuffer(this.indicator_handler,class="num">0,class="num">0,buffer_size,indicator_reading); class=class="str">"cmt">//--- Should the array be set as series? if(set_as_series) ArraySetAsSeries(this.indicator_reading,true); indicator_values_initialized = true; class=class="str">"cmt">//--- Did something go wrong? vector indicator_test; indicator_test.CopyIndicatorBuffer(indicator_handler,class="num">0,class="num">0,buffer_size); if(indicator_test.Sum() == class="num">0) class="kw">return(false); class=class="str">"cmt">//--- Everything went fine. class="kw">return(true); }
「RSI差分值的写入与越界拦截」
把 RSI 原始缓冲做成「差分序列」时,先按 buffer_size 加 2 倍 differencing_period 的长度去 CopyBuffer,多取的那段正是为了给滞后相减留窗口。比如周期取 14、缓冲要 100 根,实际得拉 100+28=128 个数据点,否则前面几根差分算不出来。 SetDifferencedIndicatorValues 里用 temp_buffer[i-1] 减 temp_buffer[i-1+differencing_period] 完成迭代,写进 vector 容器。最后用 Norm(VECTOR_NORM_P) 判零:向量范数不为 0 才把 initialized 置 true,等于顺手排除了空数据导致的误调用。 对外取数走 GetDifferencedReadingAt,索引超 buffer_size 或尚未初始化都返回 -1e10 并打日志,这种哨兵值比抛异常更适合 EA 里静默容错。GetReadingAt 同理守着 indicator_buffer_size 这道线,越界直接拦下。 外汇与贵金属行情跳空频繁,差分窗口拉太长会让首根有效值滞后数十根,回测前先在 MT5 用注释掉 Norm 检查的版本跑一遍,确认差分序列长度符合预期。
class="type">bool SingleBufferIndicator::SetDifferencedIndicatorValues(class="type">int buffer_size,class="type">int differencing_period,class="type">bool set_as_series) { class=class="str">"cmt">//--- Internal variables indicator_differenced_buffer_size = buffer_size; indicator_differenced_values = vector::Zeros(indicator_differenced_buffer_size); class=class="str">"cmt">//--- Prepare to record the differences in our RSI readings class="type">class="kw">double temp_buffer[]; class="type">int fetch = (indicator_differenced_buffer_size + (class="num">2 * differencing_period)); CopyBuffer(indicator_handler,class="num">0,class="num">0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); class=class="str">"cmt">//--- Fill in our values iteratively for(class="type">int i = indicator_differenced_buffer_size;i > class="num">1; i--) { indicator_differenced_values[i-class="num">1] = temp_buffer[i-class="num">1] - temp_buffer[i-class="num">1+differencing_period]; } class=class="str">"cmt">//--- If the norm of a vector is class="num">0, the vector is empty! if(indicator_differenced_values.Norm(VECTOR_NORM_P) != class="num">0) { Print(user_feedback(class="num">2)); indicator_differenced_values_initialized = true; class="kw">return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(class="num">3)); class="kw">return(false); } class=class="str">"cmt">//--- Get a differenced value at a specific index class="type">class="kw">double SingleBufferIndicator::GetDifferencedReadingAt(class="type">int index) { class=class="str">"cmt">//--- Make sure we&class="macro">#x27;re not trying to call values beyond our index if(index > indicator_differenced_buffer_size) { Print(user_feedback(class="num">4)); class="kw">return(-class="num">1e10); } class=class="str">"cmt">//--- Make sure our values have been set if(!indicator_differenced_values_initialized) { class=class="str">"cmt">//--- The user is trying to use values before they were set in memory Print(user_feedback(class="num">1)); class="kw">return(-class="num">1e10); } class=class="str">"cmt">//--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < indicator_differenced_buffer_size)) class="kw">return(indicator_differenced_values[index]); class=class="str">"cmt">//--- Something went wrong. class="kw">return(-class="num">1e10); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get a reading at a specific index from our RSI buffer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double SingleBufferIndicator::GetReadingAt(class="type">int index) { class=class="str">"cmt">//--- Is the user trying to call indexes beyond the buffer? if(index > indicator_buffer_size) { Print(user_feedback(class="num">4)); class="kw">return(-class="num">1e10); } class=class="str">"cmt">//--- Get the reading at the specified index if((indicator_values_initialized) && (index < indicator_buffer_size)) class="kw">return(indicator_reading[index]); class=class="str">"cmt">//--- User is trying to get values that were not set prior else { Print(user_feedback(class="num">1));
单缓冲指标类的取值与反馈方法
在封装 RSI 这类单缓冲指标时,取当前值只需从缓冲区第 0 位拷贝 1 根 K 线。下面这段 GetCurrentReading 用 CopyBuffer 把句柄、缓冲号 0、起始 0、数量 1 读入临时数组,直接返回 temp[0],实盘里你改一下缓冲号就能套到别的单线指标。 IsValid 只做一件事:判断 this.indicator_handler 是否等于 INVALID_HANDLE。句柄无效时后续所有取数都会报错,所以每次调用取数前先跑一遍这个判断能省掉大量无故闪退。 user_feedback 用 flag 0~5 区分加载成功、取数越界、差值写入失败等状态。注意 flag==0 的分支里,若 IsValid 为真直接 return,后面那行错误提示其实永远走不到——这是原代码的逻辑死角,你抄写时要把 else 补上才能覆盖加载失败提示。 外汇与贵金属行情受杠杆和跳空影响大,这类封装仅降低编码复杂度,不预示任何方向;上 MT5 跑之前先确认指标句柄在周末或断线后可能失效。
class="type">class="kw">double SingleBufferIndicator::GetCurrentReading(class="type">void) { class="type">class="kw">double temp[]; CopyBuffer(this.indicator_handler,class="num">0,class="num">0,class="num">1,temp); class="kw">return(temp[class="num">0]); } class="type">bool SingleBufferIndicator::IsValid(class="type">void) { class="kw">return((this.indicator_handler != INVALID_HANDLE)); } class="type">class="kw">string SingleBufferIndicator::user_feedback(class="type">int flag) { class="type">class="kw">string message; if(flag == class="num">0) { if(IsValid()) message = "Indicator Class Loaded Correcrtly \nSymbol: " + (class="type">class="kw">string) indicator_symbol + "\nPeriod: " + (class="type">class="kw">string) indicator_period; class="kw">return(message); message = "Error loading Indicator: [ERROR] " + (class="type">class="kw">string) GetLastError(); class="kw">return(message); } if(flag == class="num">1) { message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()"; class="kw">return(message); } if(flag == class="num">2) { message = "Succesfully set differenced indicator values."; class="kw">return(message); } if(flag == class="num">3) { message = "Failed to set our differenced indicator values: [ERROR] " + (class="type">class="kw">string) GetLastError(); class="kw">return(message); } if(flag == class="num">4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; class="kw">return(message); } if(flag == class="num">5) { message = "Goodbye."; class="kw">return(message); } else class="kw">return(""); }
◍ 把 WPR 封装成可复用的类
在 MT5 里写指标不要每次都从头调 iWPR,把 Williams Percent Range 包一层类,后面做波动分析能直接复用。下面这段头文件把 WPR 继承自 SingleBufferIndicator,默认盯 EURUSD 日线、周期 5,初始化时打印反馈。 默认构造函数里写死了 symbol 为 "EURUSD"、时间帧 PERIOD_D1、period 为 5,并用 iWPR 拿到句柄。如果你只写 WPR() 不传参,日志会提示走了默认构造,方便排查是不是漏了品种或周期。 带参构造允许外部塞入 user_symbol、user_time_frame、user_period,覆盖默认值。外汇和贵金属波动大、杠杆高,实盘前先在策略测试器用不同周期回测,确认信号延迟在你的容忍范围内再上真仓。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WPR.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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Load the parent class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <VolatilityDoctor\Indicators\SingleBuffer\SingleBufferIndicator.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| This class will provide us with usefull functionality for the WPR| class=class="str">"cmt">//+------------------------------------------------------------------+ class WPR : class="kw">public SingleBufferIndicator { class="kw">public: WPR(); WPR(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame,class="type">int user_period); ~WPR(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Our class="kw">default constructor for our Indicator class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void WPR::WPR() { indicator_values_initialized = false; indicator_symbol = "EURUSD"; indicator_time_frame = PERIOD_D1; indicator_period = class="num">5; 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">//--- Remind the user they called the class="kw">default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Our parametric constructor for our Indicator class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void WPR::WPR(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame,class="type">int user_period) { indicator_values_initialized = false; indicator_symbol = user_symbol; indicator_time_frame = user_time_frame;