在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)(基础篇)
📘

在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)(基础篇)

第 1/3 篇

RSI 自适应规则的起点与未解问题

在 MQL5 里做自优化 EA,第六部分接着聊自适应交易规则。上一节已经点出:RSI 在不同周期、时间框架和具体品种上,输出并不总呈标准化分布,传统 70/30 超买超卖线经常哑火。 当时的解法是把 RSI 的中值重锚定到观测区间的真实中点,再按偏离中值的平均偏差倍数来发信号,保证总有信号可交易。这套做法在 2025-11-17 发布的示例里被用作基线框架。 但上一节留了两个坑:一是默认「远大于平均偏差的偏移更赚钱」,却没回测验证;二是把 RSI 周期写死,方便理解但脱离实战。今天这节先把问题摆清——周期该选多少,交易者自己往往也没谱。

「RSI 周期长短对信号形态的撕裂」

以防有读者刚接触,先放一张 EURUSD 日线截图:图里叠了 2 周期 RSI,信号频繁翻转;同区间再叠 70 周期 RSI,线几乎压平在 50 附近不动。 这段行情里 EURUSD 从 2024 年 9 月 18 日的 1.121 跌到 12 月 2 日的 1.051 低点,跨度约 700 点。可 2 周期 RSI 上下乱跳、70 周期 RSI 纹丝不化,两者都没给出能跟住这波单边的信息密度。 这引出一个实在问题:RSI 周期是不是只能困在某个窄区间才管用?能不能写个算法,不靠人手试错,自动挑出当下合适的 RSI 周期,顺带把交易触发水平也算出来,不管初始周期设成多少。外汇与贵金属杠杆高,信号失真时踩错节奏的回撤可能远超单边幅度,MT5 里把这两张图调出来比对是最快的验证方式。

◍ 用 MQL5 封装可复用的 RSI 类

想做多周期 RSI 网格优化,用 Python 库算读数会踩坑:不同库对 RSI 的计算细节可能和 MT5 原生不一致,回测结论会偏。直接在 MQL5 里写一个 RSI 类,把指标句柄、缓冲区拷贝和差分计算都封装进去,才能和平台内置 iRSI 输出逐字节对齐。 类的私有成员放了两个布尔标志:indicator_values_initialized 和 indicator_differenced_values_initialized,专门拦住「缓冲区还没拷贝就读值」这种低级错。还有个 user_feedback(int flag) 方法,按报错位置打印终端提示,比如 flag==0 时校验句柄有效性,没加载成功就甩出 GetLastError() 的具体码。 默认构造函数把对象绑死在 EURUSD 日线、周期 5,并打印「我是默认构造出来的」;真用的时候你得调参数化构造函数指定品种和周期。析构函数负责释放句柄,不然数组里批量 new 对象容易漏内存。 核心交互方法是 CopyBuffer 的封装:用户传拷贝长度和是否按时间排序,函数结束前测空值;GetReadingAt() 按索引取数且做越界检查,GetDifference() 算窗口内 RSI 变化并排除全零向量。 验证手段很直白:脚本里手写一个同样参数的 iRSI 实例,和类输出同时写进「EURUSD Testing RSI Class.csv」。原文图 3 显示两列读数完全重复,说明类没写错。通过后把它挪进 include 文件,EA 和其他练习直接 import 调方法,省得反复造轮子。 下一步要拉 5 到 70、步长 5 的 14 个周期实例,指针存自定义类型数组。MQL5 允许运行时自动生成对象,代价是你必须手动清理,否则内存泄漏会在长周期跑批时拖垮终端。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ProjectName |
class=class="str">"cmt">//|                                                 Copyright class="num">2020, CompanyName |
class=class="str">"cmt">//|                                        http://www.companyname.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//--- The RSI class will manage our indicator settings and provide useful transformations we need
class RSI
  {
  class=class="str">"cmt">//--- Private members
class="kw">private:
  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">//--- Give the user feedback
class="type">class="kw">string              user_feedback(class="type">int flag)
  {
   class="type">class="kw">string message;
   class=class="str">"cmt">//--- Check if the RSI indicator loaded correctly
   if(flag == class="num">0)
     {
      class=class="str">"cmt">//--- Check the indicator was loaded correctly
      if(IsValid())
         message = "RSI Indicator Class Loaded Correcrtly \n";
      class="kw">return(message);
      class=class="str">"cmt">//--- Something went wrong
      message = "Error loading RSI Indicator: [ERROR] " + (class="type">class="kw">string) GetLastError();
      class="kw">return(message);
     }
   class=class="str">"cmt">//--- User tried getting indicator values before setting them

RSI 封装类的状态回报与成员布局

这段 MQL5 片段把 RSI 预测类的内部状态机摊开了:用 flag 变量区分四种运行时异常,比直接抛错更容易在 EA 日志里定位。flag=1 是内存里还没写指标值就想去读;flag=2 是差分指标值写入成功;flag=3 是写入失败且把 GetLastError() 转成字符串回传;flag=4 是用户索取的索引越过了缓冲区上限,必须先扩缓冲区。 protected 区暴露了类的关键字段:rsi_handler 存指标句柄,rsi_symbol 默认 "EURUSD",rsi_period 默认 5,rsi_time_frame 默认 PERIOD_D1,rsi_price 默认 PRICE_CLOSE,还有两个 buffer size 变量分别跟踪原始与差分序列长度。 默认构造函数把这些值一次性初始化,并立刻用 iRSI() 拿到句柄。IsValid() 只做一件事——比对 rsi_handler 是否等于 INVALID_HANDLE,调用方在每次取数前应先跑这句校验。外汇与贵金属市场杠杆高、滑点大,这类句柄失效在跨品种切换时概率明显上升,实盘前务必在 MT5 策略测试器里用不同品种跑一遍。 别把默认周期当最优解 类里写死 rsi_period=5 和 PERIOD_D1 只是占位,黄金 15 分钟图上套用同参数可能信号密度过高。开 MT5 改两行构造参数,比盲信封装默认值更实在。

MQL5 / C++
if(flag == class="num">1)
  {
   message = "Please set the indicator values before trying to fetch them from memory";
   class="kw">return(message);
  }
class=class="str">"cmt">//--- We sueccessfully set our differenced indicator values
if(flag == class="num">2)
  {
   message = "Succesfully set differenced indicator values.";
   class="kw">return(message);
  }
class=class="str">"cmt">//--- Failed to set our differenced indicator values
if(flag == class="num">3)
  {
   message = "Failed to set our differenced indicator values: [ERROR] " + (class="type">class="kw">string) GetLastError();
   class="kw">return(message);
  }
class=class="str">"cmt">//--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first
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);
  }
class=class="str">"cmt">//--- No feedback
else
   class="kw">return("");
 }
  class=class="str">"cmt">//--- Protected members
class="kw">protected:
  class=class="str">"cmt">//--- The handler for our RSI
  class="type">int               rsi_handler;
  class=class="str">"cmt">//--- The Symbol our RSI should be applied on
  class="type">class="kw">string            rsi_symbol;
  class=class="str">"cmt">//--- Our RSI period
  class="type">int               rsi_period;
  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 RSI indicator
  class="type">class="kw">double            rsi_reading[];
  vector            rsi_differenced_values;
  class=class="str">"cmt">//--- The current size of the buffer the user last requested
  class="type">int               rsi_buffer_size;
  class="type">int               rsi_differenced_buffer_size;
  class=class="str">"cmt">//--- The time frame our RSI should be applied on
  ENUM_TIMEFRAMES   rsi_time_frame;
  class=class="str">"cmt">//--- The price should the RSI be applied on
  ENUM_APPLIED_PRICE rsi_price;
class=class="str">"cmt">//--- Now, we can define class="kw">public members:
class="kw">public:
  class=class="str">"cmt">//--- Check if our indicator handler is valid
  class="type">bool              IsValid(class="type">void)
   {
    class="kw">return((this.rsi_handler != INVALID_HANDLE));
   }
class=class="str">"cmt">//--- Our class="kw">default constructor
class="type">void                RSI(class="type">void):
                     indicator_values_initialized(false),
                     rsi_symbol("EURUSD"),
                     rsi_time_frame(PERIOD_D1),
                     rsi_period(class="num">5),
                     rsi_price(PRICE_CLOSE),
                     rsi_handler(iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price))
 {

「RSI 封装类的构造与读数接口」

把 iRSI 句柄包进一个类,能省掉 EA 里反复写指标调用的脏活。下面这段实现了默认构造、参数构造和析构,参数构造里直接拿 symbol、周期、RSI 周期和应用价格去换句柄,并打一行反馈。 析构函数里用 IndicatorRelease 释放句柄,成功才把两个初始化标志复位并打印离线日志;漏掉这步,MT5 跑久了可能句柄泄漏导致策略卡顿。 SetIndicatorValues 负责把缓冲区拷进 rsi_reading,可指定是否倒序(ArraySetAsSeries)。它还用 vector 的 Sum()==0 做一道校验:若全 0 说明拷贝失败,返回 false,实战里这能拦掉大半『指标没数据却硬算信号』的坑。 GetCurrentReading 只拷 1 根返回最新值;GetReadingAt 做了越界保护,超 buffer_size 就报反馈码 4 并返回 -1e10,调用方靠这个特殊值就能判断无效读数。

MQL5 / C++
   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">//--- Parametric constructor
    class="type">void                RSI(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame,class="type">int user_period,ENUM_APPLIED_PRICE user_price)
    {
     indicator_values_initialized = false;
     rsi_symbol                   = user_symbol;
     rsi_time_frame               = user_time_frame;
     rsi_period                   = user_period;
     rsi_price                    = user_price;
     rsi_handler                  = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price);
     class=class="str">"cmt">//--- Give the user feedback on initilization
     Print(user_feedback(class="num">0));
    }
class=class="str">"cmt">//--- Destructor
class="type">void                ~RSI(class="type">void)
 {
  class=class="str">"cmt">//--- Free up resources we don&class="macro">#x27;t need and reset our flags
  if(IndicatorRelease(rsi_handler))
   {
    indicator_differenced_values_initialized = false;
    indicator_values_initialized = false;
    Print("RSI System logging off");
   }
 }
class=class="str">"cmt">//--- Copy readings for our RSI indicator
class="type">bool                SetIndicatorValues(class="type">int buffer_size,class="type">bool set_as_series)
 {
  rsi_buffer_size = buffer_size;
  CopyBuffer(this.rsi_handler,class="num">0,class="num">0,buffer_size,rsi_reading);
  if(set_as_series)
    ArraySetAsSeries(this.rsi_reading,true);
  indicator_values_initialized = true;
  class=class="str">"cmt">//--- Did something go wrong?
  vector rsi_test;
  rsi_test.CopyIndicatorBuffer(rsi_handler,class="num">0,class="num">0,buffer_size);
  if(rsi_test.Sum() == class="num">0)
    class="kw">return(false);
  class=class="str">"cmt">//--- Everything went fine.
  class="kw">return(true);
 }
class=class="str">"cmt">//--- Get the current RSI reading
class="type">class="kw">double              GetCurrentReading(class="type">void)
 {
  class="type">class="kw">double temp[];
  CopyBuffer(this.rsi_handler,class="num">0,class="num">0,class="num">1,temp);
  class="kw">return(temp[class="num">0]);
 }
class=class="str">"cmt">//--- Get a specific RSI reading
class="type">class="kw">double              GetReadingAt(class="type">int index)
 {
  class=class="str">"cmt">//--- Is the user trying to call indexes beyond the buffer?
  if(index > rsi_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

◍ 差分序列的越界与空向量防护

把 RSI 原始读数转成差分序列后,取数函数必须先卡两道关:索引越界、向量未初始化。任一不满足就吐 -1e10 并打日志,避免下游把脏值当信号。 SetDifferencedIndicatorValues 里 fetch 故意多取 2*differencing_period 根 K 线,给滑动减法留余量;循环从 buffer_size 倒序填到 i>1,差分周期偏移才不会踩到数组头以外的内存。 Norm(VECTOR_NORM_P) 不等于 0 才把 indicator_differenced_values_initialized 置 true。外汇与贵金属波动跳空频繁,差分向量全零往往意味着行情冻结或副本缺失,这时返回 false 比硬算更稳。 GetDifferencedReadingAt 对 index>rsi_differenced_buffer_size 直接拦掉,调用层若拿到 -1e10 就该查自己传的索引而不是怀疑指标。HORIZON 宏写死 10,代表向后看 10 根预测窗,改它前先确认差分缓冲长度够用。

MQL5 / C++
if((indicator_values_initialized) && (index < rsi_buffer_size))
      class="kw">return(rsi_reading[index]);
   class=class="str">"cmt">//--- User is trying to get values that were not set prior
   else
     {
       Print(user_feedback(class="num">1));
       class="kw">return(-class="num">1e10);
     }
 }
class=class="str">"cmt">//--- Let&class="macro">#x27;s set the conditions for our differenced data
class="type">bool                 SetDifferencedIndicatorValues(class="type">int buffer_size,class="type">int differencing_period,class="type">bool set_as_series)
   {
    class=class="str">"cmt">//--- Internal variables
    rsi_differenced_buffer_size = buffer_size;
    rsi_differenced_values = vector::Zeros(rsi_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 = (rsi_differenced_buffer_size + (class="num">2 * differencing_period));
    CopyBuffer(rsi_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 = rsi_differenced_buffer_size;i > class="num">1; i--)
      {
       rsi_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(rsi_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              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 > rsi_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 < rsi_differenced_buffer_size))
      class="kw">return(rsi_differenced_values[index]);
   class=class="str">"cmt">//--- Something went wrong.
   class="kw">return(-class="num">1e10);
  }
};
class=class="str">"cmt">//--- How far we want to forecast
class="macro">#define HORIZON class="num">10
class=class="str">"cmt">//--- Our handlers for our indicators
class="type">int rsi_5_handle;
class=class="str">"cmt">//--- Data structures to store the readings from our indicators
class="type">class="kw">double rsi_5_reading[];
class=class="str">"cmt">//--- File name
class="type">class="kw">string file_name = Symbol() + " Testing RSI Class.csv";

常见问题

短周期 RSI 抖动频繁、假突破多;长周期平滑但滞后。实盘建议用品种历史回测对比两种周期的信号撕裂程度,再定参数。
在取差分前先判断输入序列长度是否大于 1,空或长度为 1 时直接返回空向量或默认值,避免下标访问越界。
可以。小布能按你设定的多周期 RSI 自动标注信号冲突区,并把背离点推送到对应品种页,省去手动切周期。
周期、价格类型、当前读数应做公开接口;内部缓冲数组和中间差分结果设为私有,只通过方法返回状态。
用构造函数初始化、之后通过读数接口取缓存值更好,新 K 线才重算,能降 CPU 占用并避免重复逻辑出错。