同时双向工作的通用 RSI 指标·进阶篇
🔧

同时双向工作的通用 RSI 指标·进阶篇

(2/3)· 从单缓冲到双向信号,RSI 改造的五版演进里藏着多策略共存的工程坑

案例拆解 第 2/3 篇
把趋势和盘整策略硬塞进一个 EA 时,多数人先写信号判断再补过滤,结果柱形错位和跳空把回测全毁了。先定真实柱形边界,再谈双向信号才不容易返工。

◍ RSI 突破信号的代码骨架

这段逻辑把 RSI 穿越上下轨的判定拆成了两组 condition,再按 BreakMode 决定多空缓冲区的赋值方向。注意当 BreakMode 为 BREAK_IN 或 BREAK_OUT 时,condition1 对应买、condition2 对应卖;其他模式下二者反转,写反了图表信号会完全倒挂。 OnInit 里有个防呆:PeriodRSI 若小于 1,会被强行改成 2 并 Print 提示,实际计算用的是 period_rsi 而不是外部变量本身。也就是说你面板填 0 不会报错,但指标 silently 用 2 周期跑,RSI 会极其灵敏、刷屏式报警。 OnCalculate 的入口先卡 rates_total<=period_rsi 直接 return(0),意味着历史 K 线少于 RSI 周期时不画任何东西。主循环从 start_pos 跑到 rates_total,每根 K 先算 RSI 再算信号,遇到 IsStopped 会中断——回测时若手动停止,尾部几根可能没值。 开 MT5 把这段塞进自定义指标,故意把 PeriodRSI 填 0,看日志是不是吐出“数值 =2”的提示,就能验证防呆是否生效。外汇与贵金属杠杆高,信号仅作概率参考,实盘须自担风险。

MQL5 / C++
  condition2 =rsi_buffer[i-class="num">1]>up_level && rsi_buffer[i]<up_level;
  }
  else
  {
    condition1 =rsi_buffer[i-class="num">1]<up_level && rsi_buffer[i]>up_level;
    condition2 =rsi_buffer[i-class="num">1]>down_level && rsi_buffer[i]<down_level;
  }
class=class="str">"cmt">//--- 如果条件满足,显示信号
  if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT)
  {
    buy_buffer[i]  =(condition1)? rsi_buffer[i] : class="num">0;
    sell_buffer[i] =(condition2)? rsi_buffer[i] : class="num">0;
  }
  else
  {
    buy_buffer[i]  =(condition2)? rsi_buffer[i] : class="num">0;
    sell_buffer[i] =(condition1)? rsi_buffer[i] : class="num">0;
  }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                            | //+------------------------------------------------------------------+
class="type">void OnInit(class="type">void)
  {
class=class="str">"cmt">//--- 检查外部参数值
  if(PeriodRSI<class="num">1)
  {
    period_rsi=class="num">2;
    Print("输入变量值不正确, PeriodRSI =",PeriodRSI,
          "指标将会使用数值 =",period_rsi," 来进行计算.");
  }
  else
    period_rsi=PeriodRSI;
class=class="str">"cmt">//--- 设置指标属性
  SetPropertiesIndicator();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 相对强弱指数                                                  | //+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int       rates_total,
                class="kw">const class="type">int       prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double   &open[],
                class="kw">const class="type">class="kw">double   &high[],
                class="kw">const class="type">class="kw">double   &low[],
                class="kw">const class="type">class="kw">double   &close[],
                class="kw">const class="type">long     &tick_volume[],
                class="kw">const class="type">long     &volume[],
                class="kw">const class="type">int      &spread[])
  {
class=class="str">"cmt">//--- 如果数据量不够就退出
  if(rates_total<=period_rsi)
    class="kw">return(class="num">0);
class=class="str">"cmt">//--- 预备计算
  PreliminaryCalculations(prev_calculated,close);
class=class="str">"cmt">//--- 主计算循环
  for(class="type">int i=start_pos; i<rates_total && !::IsStopped(); i++)
  {
    class=class="str">"cmt">//--- 计算 RSI 指标
    CalculateRSI(i,close);
    class=class="str">"cmt">//--- 计算信号
    CalculateSignals(i);
  }
class=class="str">"cmt">//--- 返回最近计算的元件数量

「取巧的空指标返回写法」

在 MQL5 自定义指标里,有时并不需要真正计算缓冲区,只想让指标通过编译并挂在图表上。此时 OnCalculate 直接返回 rates_total 即可,框架仍会认为指标已处理完所有柱。 这种写法常见于占位指标或测试用骨架:不画任何线,也不写缓冲区,仅让 MT5 认为计算已完成。读者可新建一个空白指标,把 OnCalculate 改成只 return(rates_total); 编译挂载,验证图表是否零报错且不出图。 外汇与贵金属品种波动剧烈,此类空壳指标无信号意义,仅作结构验证,实盘高风险,勿误当策略使用。

MQL5 / C++
  class="kw">return(rates_total);
}

给 RSI 加同向连续信号计数器

在基础 RSI 突破指标上扩了两个缓冲区,专门记录同一方向连续信号出现的次数。反向信号一触发,对面计数器归零,当前方向计数器累加,这样 EA 后续能直接读 buffer 数值来卡入场条件。 全局里新增了 buy_counter_buffer[] 和 sell_counter_buffer[] 两个数组,以及 buy_counter、sell_counter 两个整型变量做累加器。指标属性也要改:indicator_buffers 设成 7,indicator_plots 设成 5,并给第 4、5 号 plot 配了绿和红两种颜色分别画买卖计数。 关键在 CalculateSignals 函数里加了道闸:用 last_index=rates_total-1 拿到最新未完成柱下标,所有计数器增减都包在 if(i<last_index) 里,避免在当前未收线柱上把计数乱加。condition1 满足就 buy_counter++ 且 sell_counter=0,反之同理。 实测挂在图表上信息量明显多了,策略测试器里跑通无报错。但有个现象值得注意:价格在两个同向信号之间拉出大缺口时,计数器可能长时间不跳动,调 Signal Level 边界也绕不开——这种空白会让交易逻辑变模糊,下个版本打算从根上消掉跳空。外汇和贵金属波动大,这类信号断层可能频繁出现,实盘前务必在 MT5 历史数据里复验。

MQL5 / C++
class=class="str">"cmt">//--- 属性
class="macro">#class="kw">property indicator_buffers class="num">7
class="macro">#class="kw">property indicator_plots   class="num">5
class="macro">#class="kw">property indicator_color4   clrMediumSeaGreen
class="macro">#class="kw">property indicator_color5   clrRed
class=class="str">"cmt">//--- 指标数组
class="type">class="kw">double buy_counter_buffer[];
class="type">class="kw">double sell_counter_buffer[];
class=class="str">"cmt">//--- 连续信号序列的计数器
class="type">int buy_counter  =class="num">0;
class="type">int sell_counter =class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算指标信号                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CalculateSignals(class="kw">const class="type">int i,class="kw">const class="type">int rates_total)
  {
   class="type">int last_index=rates_total-class="num">1;
class=class="str">"cmt">//---
   class="type">bool condition1 =class="kw">false;
   class="type">bool condition2 =class="kw">false;
class=class="str">"cmt">//--- 突破进入通道
   if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE)
     {
      condition1 =rsi_buffer[i-class="num">1]<down_level && rsi_buffer[i]>down_level;
      condition2 =rsi_buffer[i-class="num">1]>up_level && rsi_buffer[i]<up_level;
     }
   else
     {
      condition1 =rsi_buffer[i-class="num">1]<up_level && rsi_buffer[i]>up_level;
      condition2 =rsi_buffer[i-class="num">1]>down_level && rsi_buffer[i]<down_level;
     }
class=class="str">"cmt">//--- 如果条件满足,显示信号
   if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT)
     {
      buy_buffer[i]  =(condition1)? rsi_buffer[i] : class="num">0;
      sell_buffer[i] =(condition2)? rsi_buffer[i] : class="num">0;
      class=class="str">"cmt">//--- 只在完全生成的柱上处理计数器
      if(i<last_index)
        {
         if(condition1)
           {
            buy_counter++;
            sell_counter=class="num">0;
           }
         else if(condition2)
           {
            sell_counter++;
            buy_counter=class="num">0;
           }
        }
     }
   else
     {
      buy_buffer[i]  =(condition2)? rsi_buffer[i] : class="num">0;
      sell_buffer[i] =(condition1)? rsi_buffer[i] : class="num">0;
      class=class="str">"cmt">//--- 只在完全生成的柱上处理计数器
      if(i<last_index)
        {
         if(condition2)

◍ 计数器在末端索引的回填逻辑

这段代码片段处理的是逐根 K 线扫描后,对 buy_counter 与 sell_counter 两个状态变量做缓冲区写入的细节。当当前下标 i 小于 last_index 时,直接把累计得到的 buy_counter、sell_counter 写进对应的 buffer 数组,保证历史柱上能看到当时真实的连续多空计数。 若 i 已经到达或等于 last_index(即最新未闭合或刚闭合的那根),代码不再用本次循环的局部计数,而是把 i-1 位置的 buffer 值照搬过来。这样做通常是为了避免在实时末根上因条件未完全满足而写出不完整的计数,让指标线在图表最右端保持与前一根一致的读数。 逐行看:if(i<last_index) 分支里,buy_counter_buffer[i]=buy_counter 与 sell_counter_buffer[i]=sell_counter 是正常归档;else 分支里两行用 buffer[i-1] 回填,等价于末根沿用上一根的最终计数。外汇与贵金属市场跳空和重报价频繁,末根计数滞后一格可能更稳妥,但也可能漏掉本根已触发的信号,开 MT5 把这段接进自定义指标跑一遍才能确认对你品种的影响。

MQL5 / C++
  {
      buy_counter++;
      sell_counter=class="num">0;
   }
   else if(condition1)
   {
      sell_counter++;
      buy_counter=class="num">0;
   }
   }
   }
class=class="str">"cmt">//--- 修正最近的数值 (等于最终的)
   if(i<last_index)
   {
      buy_counter_buffer[i]  =buy_counter;
      sell_counter_buffer[i] =sell_counter;
   }
   else
   {
      buy_counter_buffer[i]  =buy_counter_buffer[i-class="num">1];
      sell_counter_buffer[i] =sell_counter_buffer[i-class="num">1];
   }
   }

「用密集水平线堵住跳空漏信号」

指标缓冲区数量没动,但水平线数组要扩容:在指标与水平线交叉处生成信号,才能把前版因价格跳空而漏掉的触发补回来。 具体做法是每隔 5 个点铺一条水平线。若外部参数 Signal Level 设为 30,顶部水平会算出 70、75、80、85、90、95 共 6 条;底部对称向下每 5 点一条。GetLevelsIndicator() 用两个独立循环把数值塞进数组,并返回水平线总数。 SetPropertiesIndicator() 里先算初始上下边界、清空数组,再调用 GetLevelsIndicator() 设定 INDICATOR_LEVELS,随后把数组里的线逐条写进 INDICATOR_LEVELVALUE。CalculateSignals() 改为在循环中检查是否与数组内任一条水平线发生交叉。 图 9 显示穿过多条水平线时信号正常触发,跳空问题消失。但新麻烦也来了:一是需排除“价格高于前次买入信号价”或“低于前次卖出信号价”的噪声(图 10,违背逢低买、逢高卖);二是小波动里信号扎堆,甚至连续几根 K 线连发(图 11)。这两点留到下一版处理。外汇与贵金属杠杆高,信号密集不代表胜率,实盘前务必在 MT5 用历史数据回测确认。

MQL5 / C++
class=class="str">"cmt">//--- 指标水平线的值和它们的数量
class="type">class="kw">double up_level                =class="num">0;
class="type">class="kw">double down_level              =class="num">0;
class="type">int    up_levels_total   =class="num">0;
class="type">int    down_levels_total =class="num">0;
class=class="str">"cmt">//--- 水平线的数组 
class="type">class="kw">double up_levels[];
class="type">class="kw">double down_levels[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回指标水平线                                                | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int GetLevelsIndicator(class="type">void)
  {
   class="type">int levels_counter=class="num">0;
   class="type">class="kw">double level=down_level;
class=class="str">"cmt">//--- 低于底部边界的底部水平线
   while(level>class="num">0 && !::IsStopped())
     {
       class="type">int size=::ArraySize(down_levels);
       ::ArrayResize(down_levels,size+class="num">1);
       down_levels[size]=level;
       level-=class="num">5;
       levels_counter++;
     }
   level=up_level;
class=class="str">"cmt">//--- 高于顶部边界的顶部水平线
   while(level<class="num">100 && !::IsStopped())
     {
       class="type">int size=::ArraySize(up_levels);
       ::ArrayResize(up_levels,size+class="num">1);
       up_levels[size]=level;
       level+=class="num">5;
       levels_counter++;
     }
class=class="str">"cmt">//---
   class="kw">return(levels_counter);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置指标属性                                                  | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetPropertiesIndicator(class="type">void)
  {
...
class=class="str">"cmt">//--- 计算第一个水平线
   up_level   =class="num">100-SignalLevel;
   down_level =SignalLevel;
class=class="str">"cmt">//--- 水平线数组清零
   ::ArrayFree(up_levels);
   ::ArrayFree(down_levels);
class=class="str">"cmt">//--- 指标水平线的数量
   ::IndicatorSetInteger(INDICATOR_LEVELS,GetLevelsIndicator());
class=class="str">"cmt">//--- 底部水平的指标水平线数值
   down_levels_total=::ArraySize(down_levels);
   for(class="type">int i=class="num">0; i<down_levels_total; i++)
      ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,down_levels[i]);
class=class="str">"cmt">//--- 顶部水平的指标水平线数值
   up_levels_total=::ArraySize(up_levels);
   class="type">int total=up_levels_total+down_levels_total;
   for(class="type">int i=down_levels_total,k=class="num">0; i<total; i++,k++)
      ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,up_levels[k]);
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

RSI 穿越通道的判定写法

信号计算放在 CalculateSignals 里,核心就是比较当前根与上一根的 RSI 值是否跨过了某个预设水平。突破模式分两类:BREAK_IN 系列要求 RSI 从通道外穿进内侧,另一类则反过来抓外侧穿越,逻辑对称但方向互换。 当 BreakMode 属于进入通道时,若 rsi_buffer[i]<50,就遍历 down_levels 找下穿转上穿的点;若 rsi_buffer[i]>50,则对 up_levels 找上穿转下穿。rsi_buffer[i-1]<down_levels[j] && rsi_buffer[i]>down_levels[j] 这一句就是「上一根还在水平下方、本根已到上方」的判定,命中即 break 跳出循环,避免重复触发。 非进入模式时,condition1 改成对 up_levels 做由下至上穿越,condition2 对 down_levels 做由上至下穿越,相当于把内侧/外侧关系倒过来用。外汇与贵金属波动大,这类穿越信号在震荡市可能频繁出现假突破,实盘前建议在 MT5 用历史数据跑一遍看信号密度。 下面这段是原文里的函数骨架,逐行拆完就能直接抄进 EA 改参数验证。

MQL5 / C++
class="type">void CalculateSignals(class="kw">const class="type">int i,class="kw">const class="type">int rates_total)
  {
   class="type">int last_index=rates_total-class="num">1;
class=class="str">"cmt">//---
   class="type">bool condition1 =class="kw">false;
   class="type">bool condition2 =class="kw">false;
class=class="str">"cmt">//--- 突破进入通道
   if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE)
     {
      if(rsi_buffer[i]<class="num">50)
        {
         for(class="type">int j=class="num">0; j<down_levels_total; j++)
           {
            condition1=rsi_buffer[i-class="num">1]<down_levels[j] && rsi_buffer[i]>down_levels[j];
            if(condition1)
              break;
           }
        }
      class=class="str">"cmt">//---
      if(rsi_buffer[i]>class="num">50)
        {
         for(class="type">int j=class="num">0; j<up_levels_total; j++)
           {
            condition2=rsi_buffer[i-class="num">1]>up_levels[j] && rsi_buffer[i]<up_levels[j];
            if(condition2)
              break;
           }
        }
     }
   else
     {
      for(class="type">int j=class="num">0; j<up_levels_total; j++)
        {
         condition1=rsi_buffer[i-class="num">1]<up_levels[j] && rsi_buffer[i]>up_levels[j];
         if(condition1)
           break;
        }
      class=class="str">"cmt">//---
      for(class="type">int j=class="num">0; j<down_levels_total; j++)
        {
         condition2=rsi_buffer[i-class="num">1]>down_levels[j] && rsi_buffer[i]<down_levels[j];
         if(condition2)
           break;
        }
     }
class=class="str">"cmt">//--- 如果条件满足,显示信号
class=class="str">"cmt">//--- 修正最近的数值 (等于最终的)
  }
把多版本指标差异交给小布比对
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 RSI 多版本信号重叠区,你只管判断哪版过滤更贴合当下波动。

常见问题

计数器缓冲区能记录连续信号次数,帮助区分偶发噪声与真实状态切换,后续过滤逻辑才好基于计数阈值做判断,概率上更稳。
可以,把编译好的指标文件丢进对应品种页,小布会按真实柱形重绘并标注双向信号区,省去你手动核对跳空缺口。
ATR 用来衡量波动幅度,过滤掉低于平均波幅的无效穿插,盘整转趋势时这种过滤倾向保留更强动能的信号。
不会,真实柱形由独立类基于时段边界算,窗口只是渲染层;但坐标缩放若不当会误导肉眼,建议用副图验证后再移。