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

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

(2/3)· 短周期乱跳长周期躺平,RSI到底该用哪个周期和水平值才不靠拍脑袋

进阶 第 2/3 篇
把RSI周期写死在代码里是常见坑:2周期天天假信号,70周期半年不动。不少人靠肉眼试周期和30/70水平,回测好看实盘崩。本篇接着上篇,让算法自己选周期和阈值。

◍ 用脚本把 RSI 差值落盘做交叉验证

想把自写 RSI 类和平台内置 iRSI 的差异看清,最直接的方法是一次性把两组数据写进 CSV 对比。下面这段脚本在 MT5 里拉取当前品种、当前周期的 5 周期收盘价 RSI,同时用自定义类算原始值与 10 根偏移的差值序列。 input int size = 3000; 先框定要导出的 K 线量,3000 根足够覆盖多数品种近半年的 M5 以上周期样本。脚本里 fetch = size + (2 * HORIZON) 多取了前瞻缓冲,避免差值计算在数组头部越界。 写文件时表头包含 Time、RSI 5、RSI 5 Class、RSI 5 Difference、RSI 5 Class Difference 五列,循环从 i=size 递减到 1,把内置函数读数和类方法读数并排落盘。外汇与贵金属杠杆高、滑点跳空频繁,这类回测数据仅反映历史统计关系,实盘信号概率会随波动率漂移。 跑完用 Excel 或 Python 读 CSV,重点看两列 Difference 是否一致;若偏差超过浮点容差,说明类的差分逻辑或 HORIZON 定义和内置口径不同,需回类里查 GetDifferencedReadingAt 实现。

MQL5 / C++
input class="type">int size = class="num">3000;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our script execution                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Testing the RSI Class
class=class="str">"cmt">//--- Initialize the class
   RSI my_rsi(Symbol(),PERIOD_CURRENT,class="num">5,PRICE_CLOSE);
   my_rsi.SetIndicatorValues(size,true);
   my_rsi.SetDifferencedIndicatorValues(size,class="num">10,true);
class=class="str">"cmt">//---Setup our technical indicators
   rsi_5_handle   = iRSI(Symbol(),PERIOD_CURRENT,class="num">5,PRICE_CLOSE);
   class="type">int fetch = size + (class="num">2 * HORIZON);
class=class="str">"cmt">//---Set the values as series
   CopyBuffer(rsi_5_handle,class="num">0,class="num">0,fetch,rsi_5_reading);
   ArraySetAsSeries(rsi_5_reading,true);
class=class="str">"cmt">//---Write to file
   class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   for(class="type">int i=size;i>=class="num">1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","RSI class="num">5","RSI class="num">5 Class","RSI class="num">5 Difference","RSI class="num">5 Class Difference");
        }
      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   rsi_5_reading[i],
                   my_rsi.GetReadingAt(i),
                   rsi_5_reading[i]  - rsi_5_reading[i + HORIZON],
                   my_rsi.GetDifferencedReadingAt(i)
                   );
        }
     }
class=class="str">"cmt">//--- Close the file
   FileClose(file_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef  HORIZON
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                          RSI.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">//+------------------------------------------------------------------+

把 RSI 封装成一个可复用的类

在 MT5 里反复手写 iRSI 句柄和 CopyBuffer 很拖累策略迭代速度,直接把 RSI 包成一个 class 更顺手。下面这段类声明把指标句柄、品种、周期、应用价、缓冲区和差分序列都收进成员变量,外部只要调几个 public 方法就能拿读数。 类里默认构造函数给了一组可立即验证的参数:品种 EURUSD、周期 PERIOD_D1、RSI 周期 5。你在 MT5 导航栏新建 EA 把这段代码贴进 include,不传参实例化就能看到日线 5 周期 RSI 的初始状态。 protected 区里的 forecast_horizon 和 rsi_differenced_values 是为后续做差分预测留的口子,本类暂不计算预测值,只负责把原始读数和差分读数缓存好。外汇与贵金属波动受杠杆和消息面影响大,RSI 超买超卖只是概率信号,实盘前务必在策略测试器跑历史数据。 别把默认周期当最优解 类里写死的 rsi_period=5 只是示例,黄金 15 分钟和欧美日线的合理周期差异很大,开 MT5 把周期参数改成 14 或 21 对比一下缓冲区形状再定。

MQL5 / C++
class RSI
  {
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="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="kw">public:
                     RSI();
                     RSI(class="type">class="kw">string user_symbol,ENUM_TIMEFRAMES user_time_frame,class="type">int user_period,ENUM_APPLIED_PRICE user_price);
                    ~RSI();
   class="type">bool               SetIndicatorValues(class="type">int buffer_size,class="type">bool set_as_series);
   class="type">bool               IsValid(class="type">void);
   class="type">class="kw">double             GetCurrentReading(class="type">void);
   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">void RSI::RSI()
  {
   indicator_values_initialized = false;
   rsi_symbol                  = "EURUSD";
   rsi_time_frame              = PERIOD_D1;
   rsi_period                  = class="num">5;

「RSI 类的构造、析构与读数抓取」

把 RSI 封装成类,核心就是三件事:初始化句柄、释放资源、取数。默认构造函数里把价格类型写死为 PRICE_CLOSE,再调 iRSI 拿句柄;带参构造则让用户从外部传 symbol、周期、长度和应用价格,灵活性明显更高。 析构函数别漏掉 IndicatorRelease(rsi_handler)。不少自制指标内存泄漏,就是忘了在对象销毁时释放句柄,MT5 跑久了终端变卡,这类隐患在高频加载 EA 时尤其明显。 取当前值用 CopyBuffer 拷 1 根到临时数组再返回 temp[0],而 SetIndicatorValues 一次拉 buffer_size 根,还能用 ArraySetAsSeries 把索引 0 对齐到最新 K 线。实盘前建议把 buffer_size 设成 100 左右,既能覆盖回看又不占太多内存。 类里还留了 user_feedback(0) 和 user_feedback(5) 的打印,初始化和释放各打一条。你开 MT5 挂上这类 EA,专家日志里能看到构造与析构的调用痕迹,用来确认对象生命周期是否正常。

MQL5 / C++
  rsi_price                    = PRICE_CLOSE;
  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">//--- 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 RSI class                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void RSI::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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our destructor for our RSI class                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void RSI::~RSI()
  {
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(user_feedback(class="num">5));
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get our current reading from the RSI indicator                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double RSI::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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set our indicator values and our buffer size                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool            RSI::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);

◍ RSI差分序列的落地与取值护栏

把原始 RSI 做差分,本质是用 rsi[i-1] - rsi[i-1+period] 抹掉趋势项,留下摆动残差。下面这段类方法先把缓冲长度记进 rsi_differenced_buffer_size,再用 vector::Zeros() 预分配全零容器,避免后续写入越界。 取数时 fetch = buffer_size + 2*differencing_period,多拉两段周期是为了差分窗口右端不缺数据;CopyBuffer 拿到临时数组后,若 set_as_series 为真就 ArraySetAsSeries 把时间轴倒序,契合 MT5 指标缓冲习惯。 循环从 i = buffer_size 递减到 i > 1,逐位填差分。填完用 Norm(VECTOR_NORM_P) 判零:向量范数非 0 才说明有实质内容,标记 indicator_differenced_values_initialized = true 并返回;否则置 false 并打印反馈,调用方应及时拦住。 对外取数走 GetDifferencedReadingAt(index):先卡 index > buffer_size 防越界,再卡未初始化标记,两道护栏失败都回 -1e10 这种明显脏值,方便上层识别异常。外汇与贵金属市场波动剧烈、杠杆风险高,这类差分缓冲使用前务必确认初始化成功,否则信号可能完全失真。

MQL5 / C++
class="type">bool RSI::SetDifferencedIndicatorValues(class="type">int buffer_size,class="type">int differencing_period,class="type">bool set_as_series)
  {
  rsi_differenced_buffer_size = buffer_size;
  rsi_differenced_values = vector::Zeros(rsi_differenced_buffer_size);
  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);
  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];
     }
  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="type">class="kw">double RSI::GetDifferencedReadingAt(class="type">int index)
   {
   if(index > rsi_differenced_buffer_size)
     {
     Print(user_feedback(class="num">4));
     class="kw">return(-class="num">1e10);
     }
   if(!indicator_differenced_values_initialized)
     {
     Print(user_feedback(class="num">1));
     class="kw">return(-class="num">1e10);
     }
   if((indicator_differenced_values_initialized) && (index < rsi_differenced_buffer_size))
     class="kw">return(rsi_differenced_values[index]);
   class="kw">return(-class="num">1e10);
   }

RSI 类取数越界与句柄校验的兜底写法

在自写 RSI 封装类里,取数接口必须先拦住越界访问。若调用方传进来的 index 大于 rsi_buffer_size,直接 Print 反馈并 return -1e10,避免读到非法内存。 当指标已初始化且 index 在缓冲范围内,才返回 rsi_reading[index] 的真实读数;否则提示「先调 SetIndicatorValues()」并返回 -1e10。这一处 -1e10 的哨兵值,能在 EA 逻辑里和正常 RSI 数值(通常 0~100)清晰区分。 IsValid() 只做一件事:比对 this.rsi_handler 是否等于 INVALID_HANDLE。句柄无效时,上层就不该再发起任何 CopyBuffer 或读数请求,否则可能触发 4802 类错误。 user_feedback(int flag) 用 0~5 的整型标记拼装诊断字符串:0 查加载状态并带出 Symbol 与 Period,1~4 分别对应未设值、差值设成功、差值设失败、越界,5 是析构告别。注意 flag==0 分支里有一行 message 赋值在 return 之后,属不可达死代码,实盘前建议删掉。 外汇与贵金属波动剧烈,这类指标封装的任何越界或句柄失效都可能让信号滞后,复盘时优先看日志里的 flag 编号。

MQL5 / C++
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
   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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if our indicator handler is valid                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RSI::IsValid(class="type">void)
  {
   class="kw">return((this.rsi_handler != INVALID_HANDLE));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Give the user feedback on the actions he is performing              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string RSI::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 \nSymbol: " + (class="type">class="kw">string) rsi_symbol + "\nPeriod: " + (class="type">class="kw">string) rsi_period;
       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
   if(flag == class="num">1)
     {
       message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()";
       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">//--- The class has been deactivated by the user
   if(flag == class="num">5)
     {
       message = "Goodbye.";
       class="kw">return(message);
     }
class=class="str">"cmt">//--- No feedback
   else
       class="kw">return("");

「把多周期 RSI 批量落进 CSV 的脚本骨架」

这段脚本做的是一件事:把 14 组不同周期的 RSI 指针一次性建好,再准备把数据导出来。周期从 5 开始,按 (i+1)*5 递增,最大到 70,共覆盖 14 个档位。 全局里先定义了 HORIZON 为 10,用来在抓取数据时多留 2*HORIZON 根 K 线余量,避免边界计算越界。实际抓取量 fetch = size + 20,默认 size 是 3000,也就是拉 3020 根。 文件名用 Symbol() 拼接 ' RSI Algorithmic Input Selection.csv',意味着每份品种会生成独立文件。外汇与贵金属波动剧烈,这类批量取数只解决输入准备,信号有效性仍需在 MT5 里回测验证,杠杆品种风险偏高。 下面这段是脚本头与核心声明,逐行拆完就能直接抄进 MQ5 文件编译。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System constants                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define HORIZON class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <VolatilityDoctor\Indicators\RSI.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
RSI *my_rsi_array[class="num">14];
class="type">class="kw">string file_name = Symbol() + " RSI Algorithmic Input Selection.csv";
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int size = class="num">3000;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Our script execution                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- How much data should we store in our indicator buffer?
   class="type">int fetch = size + (class="num">2 * HORIZON);
class=class="str">"cmt">//--- Store pointers to our RSI objects
   for(class="type">int i = class="num">0; i <= class="num">13; i++)
     {
      class=class="str">"cmt">//--- Create an RSI object
      my_rsi_array[i] = new RSI(Symbol(),PERIOD_CURRENT,((i+class="num">1) * class="num">5),PRICE_CLOSE);
把周期扫描交给小布
这些多周期RSI扫描与水平重标定,小布盯盘的AIGC诊断已内置,打开对应品种页即可看到偏离中值的倍数信号,你只管定规则。

常见问题

市场波动结构随阶段变化,短周期对噪声过度敏感,长周期则钝化,固定值无法兼顾不同regime,信号质量波动大。
可以,品种页已内置多周期RSI偏离中值诊断,并标出平均偏差倍数触发的区域,省去自己写扫描循环。
用样本内记录信号后的盈亏分布与触发频率对比,看新水平是否在高倍偏差时给出更稀疏但更倾向有效的信号。
概率上存在,需做walk-forward验证,让选周期逻辑在滚动窗口外推,避免把某段行情的偏好当成通用规律。