在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)·进阶篇
(2/3)· 短周期乱跳长周期躺平,RSI到底该用哪个周期和水平值才不靠拍脑袋
◍ 用脚本把 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 实现。
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 对比一下缓冲区形状再定。
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,专家日志里能看到构造与析构的调用痕迹,用来确认对象生命周期是否正常。
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 这种明显脏值,方便上层识别异常。外汇与贵金属市场波动剧烈、杠杆风险高,这类差分缓冲使用前务必确认初始化成功,否则信号可能完全失真。
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 编号。
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 文件编译。
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);