在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)(基础篇)
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 允许运行时自动生成对象,代价是你必须手动清理,否则内存泄漏会在长周期跑批时拖垮终端。
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 改两行构造参数,比盲信封装默认值更实在。
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,调用方靠这个特殊值就能判断无效读数。
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 根预测窗,改它前先确认差分缓冲长度够用。
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";