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

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

第 1/3 篇

「用全周期替代单一参数猜谜」

技术指标天然带滞后,这是老问题。更隐蔽的坑在周期参数:周期太小,指标啃进来的全是市场噪音;周期太大,等信号出来行情已经跑完大半。两种极端都会让交易机会溜走,实盘绩效直接掉档。 原文作者给出的路数不挑一个「最优周期」,而是把可用周期全用上。具体靠降维算法 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 的签名不同,所以各自保留自己的构造。析构函数负责重置状态并释放指标句柄,资源清理交给类,比手动重复写释放代码稳。 下面这段是父类的头文件骨架,注意四个公共方法签名就是上面说的读写入口,没有暴露原始缓冲区成员——强制走方法才符合封装初衷。

MQL5 / C++
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)——这能拦掉句柄失效但拷贝‘假装’成功的坑。外汇与贵金属行情跳空频繁,这类双重校验在实盘高风险环境下尤其值得保留。 下面这段是类声明与取值函数的核心片段,逐行拆开看更直观。

MQL5 / C++
  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 检查的版本跑一遍,确认差分序列长度符合预期。

MQL5 / C++
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 跑之前先确认指标句柄在周末或断线后可能失效。

MQL5 / C++
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,覆盖默认值。外汇和贵金属波动大、杠杆高,实盘前先在策略测试器用不同周期回测,确认信号延迟在你的容忍范围内再上真仓。

MQL5 / C++
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;

常见问题

可以用全周期框架替代单一参数猜谜,把多个时间周期信号合并判断,减少主观猜错概率,具体可在代码里用多周期缓冲区统一接口。
建一个单缓冲区父类,让WPR与RSI都继承它,统一取数和写数方法,可以避免重复写两套缓冲区逻辑。
小布可以替你加载多周期诊断,直接标出各周期WPR/RSI方向是否打架,你只看结论就行。
在写入前做索引边界拦截,超出有效范围直接丢弃或钳位,代码里加一层判断即可避免越界报错。
在类内部维护状态并暴露统一取值方法,外部只调方法拿数,不碰裸数组,能降低出错和维护成本。