English Русский Español Deutsch 日本語
preview
价格行为分析工具包开发(第十三部分):RSI 哨兵工具

价格行为分析工具包开发(第十三部分):RSI 哨兵工具

MetaTrader 5交易系统 |
539 1
Christian Benjamin
Christian Benjamin

内容


引言

背离是技术分析中的一个概念,指的是动量或震荡指标等指标的走势与价格的走势发生偏离。本质上,当价格形成未被指标反映的新高或新低时,这可能预示着当前趋势正在减弱,并可能预示着反转或动量的变化。RSI 背离是一种发现潜在市场反转的简单方法。当价格朝一个方向移动,而 RSI 却朝另一个方向移动时,这可能预示着趋势的变化。然而,手动在图表上扫描这些信号既缓慢又容易出错。这就是自动化发挥作用的地方。

在本文中,我们将构建一个 MQL5 EA,它可以自动检测 RSI 背离信号。该 EA 将在您的图表上用清晰的箭头标记这些信号,并提供简要摘要,以便您能快速了解正在发生的情况。无论您是初学者还是经验丰富的交易者,此工具都能帮助您发现交易机会,您可以在执行交易前进行验证,而无需花费数小时进行手动分析。让我们深入了解这个 RSI 背离 EA 如何简化您的交易过程。


策略概览

理解 RSI 背离

当相对强弱指数(RSI)的走势与资产价格的走势不同时,就会发生 RSI 背离,这预示着价格动量可能发生转变。RSI 与价格行为之间的这种对比是交易者用来预测市场反转或趋势延续的关键指标。通常,RSI 会跟随价格的动量,从而确认 prevailing trends(主导趋势)。然而,当背离出现时,它揭示了一种差异,这种差异通常发生在重大价格变动之前。及早识别这些信号对于把握市场进出场时机至关重要。

在 RSI 背离的背景下,主要有两种类型:

1. 常规 RSI 背离

常规 RSI 背离通常被视为反转信号。它表明当前趋势正在失去动力,可能即将反转。

  • 常规看涨 RSI 背离

当价格形成更低的低点(LL),而 RSI 形成更高的低点(HL)时发生。这表明,尽管价格在下跌,但动量开始向上转变,暗示着可能反转至上升趋势。

看涨背离

图 1. 看涨背离

  • 常规看跌 RSI 背离

当价格形成更高的高点(HH),而 RSI 形成更低的高点(LH)时发生。尽管价格在上涨,但动量的减弱(如 RSI 所示)表明下跌可能即将到来。

卖出信号

图 2. 看跌背离

2.隐藏 RSI 背离

隐藏 RSI 背离被解释为趋势延续的信号,而不是即将到来的反转。它证实了当前趋势仍然具有力量,即使 RSI 和价格暂时出现背离。

  • 隐藏看涨 RSI 背离:在上升趋势中,如果价格形成更高的低点(HL),而 RSI 形成更低的低点(LL),这表明修正只是暂时的,上升趋势很可能继续。

隐藏看涨背离

图 3. 隐藏看涨背离

  • 隐藏看跌 RSI 背离:在下降趋势中,当价格形成更低的高点(LH),而 RSI 形成更高的高点(HH)时,这证实了下降趋势的强度,并表明下行运动很可能持续。

看跌背离

图 4. 隐藏看跌背离

下表总结了 RSI 背离类型之间的主要区别:

RSI 背离类型 价格行为 RSI 行为 信号类型 预期
常规看涨 更低低点(LL) 更高低点(HL) 向上反转 下降趋势转上升趋势
常规看跌 更高高点(HH)  更低高点(LH) 向下反转 上升趋势转下降趋势
隐藏看涨 更高低点(HL) 更低低点(LL) 连续上涨 上升趋势延续
隐藏看跌 更低高点 (LH) 更高高点(HH) 连续下跌 下降趋势延续

总而言之,该 EA 在一个定义的回溯期内持续扫描价格和 RSI 数据,以检测它们走势之间的差异,我们称之为 RSI 背离。

以下是它的功能:

1. 数据收集与准备

该 EA 收集 RSI 值以及来自近期 K 线的相应价格数据(低点、高点、收盘价和时间)。这确保分析始终基于最新、完整的信息。

2. 识别摆动点

然后,它确定价格和 RSI 数据中的局部摆动高点和低点。这些摆动点作为我们背离分析的参考标记。
3. 检测常规背离

  • 常规看涨背离:该 EA 寻找价格创出更低低点(LL),而 RSI 形成更高低点(HL)的情况,这预示着下降趋势可能正在失去动力,并可能向上反转。
  • 常规看跌背离:它还检查价格创出更高高点(HH),而 RSI 形成更低高点(LH)的情况,这表明随着动量减弱,上升趋势可能即将结束。

4. 检测隐藏背离
  • 隐藏看涨背离:在上升趋势中,如果价格形成更高的低点(HL),但 RSI 记录了更低的低点(LL),该 EA 将此识别为整体上升趋势尽管暂时回调但仍然强劲的迹象。
  • 隐藏看跌背离:相反,在下降趋势期间,如果价格创出更低的高点(LH),而 RSI 显示更高的高点(HH),这证实了下降趋势很可能继续。

5. 视觉和日志信号生成

一旦检测到背离(无论是常规还是隐藏),EA 会在图表上直观地标记该事件(使用箭头和标签),并记录信号的详细信息以供进一步分析或回测。关于它如何执行上述过程的更多信息,请查看下面的代码剖析部分。


MQL5代码

//+--------------------------------------------------------------------+
//|                                                RSI Divergence.mql5 |
//|                                 Copyright 2025, Christian Benjamin |
//|                                               https://www.mql5.com |
//+--------------------------------------------------------------------+
#property copyright "2025, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.0"
#property strict

//---- Input parameters
input int    InpRSIPeriod             = 14;    // RSI period
input int    InpSwingLeft             = 1;     // Bars to the left for swing detection (relaxed)
input int    InpSwingRight            = 1;     // Bars to the right for swing detection (relaxed)
input int    InpLookback              = 100;   // Number of bars to scan for divergence
input int    InpEvalBars              = 5;     // Bars after which to evaluate a signal
input int    InpMinBarsBetweenSignals = 1;     // Minimum bars between same-type signals (allows frequent re-entry)
input double InpArrowOffset           = 3.0;   // Arrow offset (in points) for display
input double InpMinSwingDiffPct       = 0.05;  // Lower minimum % difference to qualify as a swing
input double InpMinRSIDiff            = 1.0;   // Lower minimum difference in RSI between swing points

// Optional RSI threshold filter for bullish divergence (disabled by default)
input bool   InpUseRSIThreshold       = false; // If true, require earlier RSI swing to be oversold for bullish divergence
input double InpRSIOversold           = 30;    // RSI oversold level
input double InpRSIOverbought         = 70;    // RSI overbought level (if needed for bearish)

//---- Global variables
int      rsiHandle;         // Handle for the RSI indicator
double   rsiBuffer[];       // Buffer for RSI values
double   lowBuffer[];       // Buffer for low prices
double   highBuffer[];      // Buffer for high prices
double   closeBuffer[];     // Buffer for close prices
datetime timeBuffer[];       // Buffer for bar times
int      g_totalBars = 0;   // Number of bars in our copied arrays
datetime lastBarTime = 0;   // Time of last closed bar

//---- Structure to hold signal information
struct SignalInfo
  {
   string            type;         // e.g. "RegBearish Divergence", "HiddenBullish Divergence"
   int               barIndex;     // Bar index where the signal was generated
   datetime          signalTime;   // Time of the signal bar
   double            signalPrice;  // Price used for the signal (swing high for bearish, swing low for bullish)
  };

SignalInfo signals[];  // Global array to store signals

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
   if(rsiHandle == INVALID_HANDLE)
     {
      Print("Error creating RSI handle");
      return(INIT_FAILED);
     }
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(rsiHandle != INVALID_HANDLE)
      IndicatorRelease(rsiHandle);
   EvaluateSignalsAndPrint();
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
// Process once per new closed candle (using bar1's time)
   datetime currentBarTime = iTime(_Symbol, _Period, 1);
   if(currentBarTime == lastBarTime)
      return;
   lastBarTime = currentBarTime;

//--- Copy RSI data
   ArrayResize(rsiBuffer, InpLookback);
   ArraySetAsSeries(rsiBuffer, true);
   if(CopyBuffer(rsiHandle, 0, 0, InpLookback, rsiBuffer) <= 0)
     {
      Print("Error copying RSI data");
      return;
     }

//--- Copy price and time data
   ArrayResize(lowBuffer, InpLookback);
   ArrayResize(highBuffer, InpLookback);
   ArrayResize(closeBuffer, InpLookback);
   ArraySetAsSeries(lowBuffer, true);
   ArraySetAsSeries(highBuffer, true);
   ArraySetAsSeries(closeBuffer, true);
   if(CopyLow(_Symbol, _Period, 0, InpLookback, lowBuffer) <= 0 ||
      CopyHigh(_Symbol, _Period, 0, InpLookback, highBuffer) <= 0 ||
      CopyClose(_Symbol, _Period, 0, InpLookback, closeBuffer) <= 0)
     {
      Print("Error copying price data");
      return;
     }

   ArrayResize(timeBuffer, InpLookback);
   ArraySetAsSeries(timeBuffer, true);
   if(CopyTime(_Symbol, _Period, 0, InpLookback, timeBuffer) <= 0)
     {
      Print("Error copying time data");
      return;
     }

   g_totalBars = InpLookback;

//--- Identify swing lows and swing highs
   int swingLows[];
   int swingHighs[];
   int startIndex = InpSwingLeft;
   int endIndex   = g_totalBars - InpSwingRight;
   for(int i = startIndex; i < endIndex; i++)
     {
      if(IsSignificantSwingLow(i, InpSwingLeft, InpSwingRight))
        {
         ArrayResize(swingLows, ArraySize(swingLows) + 1);
         swingLows[ArraySize(swingLows) - 1] = i;
        }
      if(IsSignificantSwingHigh(i, InpSwingLeft, InpSwingRight))
        {
         ArrayResize(swingHighs, ArraySize(swingHighs) + 1);
         swingHighs[ArraySize(swingHighs) - 1] = i;
        }
     }

//--- Bearish Divergence (using swing highs)
   if(ArraySize(swingHighs) >= 2)
     {
      ArraySort(swingHighs); // ascending order: index 0 is most recent
      int recent   = swingHighs[0];
      int previous = swingHighs[1];

      // Regular Bearish Divergence: Price makes a higher high while RSI makes a lower high
      if(highBuffer[recent] > highBuffer[previous] &&
         rsiBuffer[recent] < rsiBuffer[previous] &&
         (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff)
        {
         Print("Regular Bearish Divergence detected at bar ", recent);
         DisplaySignal("RegBearish Divergence", recent);
        }
      // Hidden Bearish Divergence: Price makes a lower high while RSI makes a higher high
      else
         if(highBuffer[recent] < highBuffer[previous] &&
            rsiBuffer[recent] > rsiBuffer[previous] &&
            (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff)
           {
            Print("Hidden Bearish Divergence detected at bar ", recent);
            DisplaySignal("HiddenBearish Divergence", recent);
           }
     }

//--- Bullish Divergence (using swing lows)
   if(ArraySize(swingLows) >= 2)
     {
      ArraySort(swingLows); // ascending order: index 0 is most recent
      int recent   = swingLows[0];
      int previous = swingLows[1];

      // Regular Bullish Divergence: Price makes a lower low while RSI makes a higher low
      if(lowBuffer[recent] < lowBuffer[previous] &&
         rsiBuffer[recent] > rsiBuffer[previous] &&
         (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff)
        {
         // Optionally require the earlier swing's RSI be oversold
         if(!InpUseRSIThreshold || rsiBuffer[previous] <= InpRSIOversold)
           {
            Print("Regular Bullish Divergence detected at bar ", recent);
            DisplaySignal("RegBullish Divergence", recent);
           }
        }
      // Hidden Bullish Divergence: Price makes a higher low while RSI makes a lower low
      else
         if(lowBuffer[recent] > lowBuffer[previous] &&
            rsiBuffer[recent] < rsiBuffer[previous] &&
            (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff)
           {
            Print("Hidden Bullish Divergence detected at bar ", recent);
            DisplaySignal("HiddenBullish Divergence", recent);
           }
     }
  }

//+------------------------------------------------------------------------+
//| IsSignificantSwingLow: Determines if the bar at 'index' is a swing low |
//+------------------------------------------------------------------------+
bool IsSignificantSwingLow(int index, int left, int right)
  {
   double currentLow = lowBuffer[index];
// Check left side for a local minimum condition
   for(int i = index - left; i < index; i++)
     {
      if(i < 0)
         continue;
      double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * 100.0;
      if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct)
         return false;
     }
// Check right side for a local minimum condition
   for(int i = index + 1; i <= index + right; i++)
     {
      if(i >= g_totalBars)
         break;
      double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * 100.0;
      if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct)
         return false;
     }
   return true;
  }

//+--------------------------------------------------------------------------+
//| IsSignificantSwingHigh: Determines if the bar at 'index' is a swing high |
//+--------------------------------------------------------------------------+
bool IsSignificantSwingHigh(int index, int left, int right)
  {
   double currentHigh = highBuffer[index];
// Check left side for a local maximum condition
   for(int i = index - left; i < index; i++)
     {
      if(i < 0)
         continue;
      double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * 100.0;
      if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct)
         return false;
     }
// Check right side for a local maximum condition
   for(int i = index + 1; i <= index + right; i++)
     {
      if(i >= g_totalBars)
         break;
      double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * 100.0;
      if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct)
         return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| DisplaySignal: Draws an arrow on the chart and records the signal|
//+------------------------------------------------------------------+
void DisplaySignal(string signalText, int barIndex)
  {
// Prevent duplicate signals on the same bar (or too close)
   for(int i = 0; i < ArraySize(signals); i++)
     {
      if(StringFind(signals[i].type, signalText) != -1)
         if(MathAbs(signals[i].barIndex - barIndex) < InpMinBarsBetweenSignals)
            return;
     }

// Update a "LatestSignal" label for regular signals.
   if(StringFind(signalText, "Reg") != -1)
     {
      string labelName = "LatestSignal";
      if(ObjectFind(0, labelName) == -1)
        {
         if(!ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0))
           {
            Print("Failed to create LatestSignal label");
            return;
           }
         ObjectSetInteger(0, labelName, OBJPROP_CORNER, 0);
         ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, 10);
         ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, 20);
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrWhite);
        }
      ObjectSetString(0, labelName, OBJPROP_TEXT, signalText);
     }

// Create an arrow object for the signal.
   string arrowName = "Arrow_" + signalText + "_" + IntegerToString(barIndex);
   if(ObjectFind(0, arrowName) < 0)
     {
      int arrowCode = 0;
      double arrowPrice = 0.0;
      color arrowColor = clrWhite;
      double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

      if(StringFind(signalText, "Bullish") != -1)
        {
         arrowCode  = 233; // Wingdings up arrow
         arrowColor = clrLime;
         arrowPrice = lowBuffer[barIndex] - (InpArrowOffset * point);
        }
      else
         if(StringFind(signalText, "Bearish") != -1)
           {
            arrowCode  = 234; // Wingdings down arrow
            arrowColor = clrRed;
            arrowPrice = highBuffer[barIndex] + (InpArrowOffset * point);
           }

      if(!ObjectCreate(0, arrowName, OBJ_ARROW, 0, timeBuffer[barIndex], arrowPrice))
        {
         Print("Failed to create arrow object ", arrowName);
         return;
        }
      ObjectSetInteger(0, arrowName, OBJPROP_COLOR, arrowColor);
      ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode);
     }

// Record the signal for evaluation.
   SignalInfo sig;
   sig.type = signalText;
   sig.barIndex = barIndex;
   sig.signalTime = timeBuffer[barIndex];
   if(StringFind(signalText, "Bullish") != -1)
      sig.signalPrice = lowBuffer[barIndex];
   else
      sig.signalPrice = highBuffer[barIndex];
   ArrayResize(signals, ArraySize(signals) + 1);
   signals[ArraySize(signals) - 1] = sig;

   UpdateSignalCountLabel();
  }

//+------------------------------------------------------------------+
//| UpdateSignalCountLabel: Updates a label showing signal counts    |
//+------------------------------------------------------------------+
void UpdateSignalCountLabel()
  {
   int regCount = 0, hidCount = 0;
   for(int i = 0; i < ArraySize(signals); i++)
     {
      if(StringFind(signals[i].type, "Reg") != -1)
         regCount++;
      else
         if(StringFind(signals[i].type, "Hidden") != -1)
            hidCount++;
     }
   string countText = "Regular Signals: " + IntegerToString(regCount) +
                      "\nHidden Signals: " + IntegerToString(hidCount);
   string countLabel = "SignalCount";
   if(ObjectFind(0, countLabel) == -1)
     {
      if(!ObjectCreate(0, countLabel, OBJ_LABEL, 0, 0, 0))
        {
         Print("Failed to create SignalCount label");
         return;
        }
      ObjectSetInteger(0, countLabel, OBJPROP_CORNER, 0);
      ObjectSetInteger(0, countLabel, OBJPROP_XDISTANCE, 10);
      ObjectSetInteger(0, countLabel, OBJPROP_YDISTANCE, 40);
      ObjectSetInteger(0, countLabel, OBJPROP_COLOR, clrYellow);
     }
   ObjectSetString(0, countLabel, OBJPROP_TEXT, countText);
  }

//+--------------------------------------------------------------------+
//| EvaluateSignalsAndPrint: After backtesting, prints signal accuracy |
//+--------------------------------------------------------------------+
void EvaluateSignalsAndPrint()
  {
   double closeAll[];
   int totalBars = CopyClose(_Symbol, _Period, 0, WHOLE_ARRAY, closeAll);
   if(totalBars <= 0)
     {
      Print("Error copying complete close data for evaluation");
      return;
     }
   ArraySetAsSeries(closeAll, true);

   int totalEvaluated = 0, regTotal = 0, hidTotal = 0;
   int regEval = 0, hidEval = 0;
   int regCorrect = 0, hidCorrect = 0;

   for(int i = 0; i < ArraySize(signals); i++)
     {
      int evalIndex = signals[i].barIndex - InpEvalBars;
      if(evalIndex < 0)
         continue;
      double evalClose = closeAll[evalIndex];

      if(StringFind(signals[i].type, "Bullish") != -1)
        {
         if(StringFind(signals[i].type, "Reg") != -1)
           {
            regTotal++;
            regEval++;
            if(evalClose > signals[i].signalPrice)
               regCorrect++;
           }
         else
            if(StringFind(signals[i].type, "Hidden") != -1)
              {
               hidTotal++;
               hidEval++;
               if(evalClose > signals[i].signalPrice)
                  hidCorrect++;
              }
         totalEvaluated++;
        }
      else
         if(StringFind(signals[i].type, "Bearish") != -1)
           {
            if(StringFind(signals[i].type, "Reg") != -1)
              {
               regTotal++;
               regEval++;
               if(evalClose < signals[i].signalPrice)
                  regCorrect++;
              }
            else
               if(StringFind(signals[i].type, "Hidden") != -1)
                 {
                  hidTotal++;
                  hidEval++;
                  if(evalClose < signals[i].signalPrice)
                     hidCorrect++;
                 }
            totalEvaluated++;
           }
     }

   double overallAccuracy = (totalEvaluated > 0) ? (double)(regCorrect + hidCorrect) / totalEvaluated * 100.0 : 0.0;
   double regAccuracy = (regEval > 0) ? (double)regCorrect / regEval * 100.0 : 0.0;
   double hidAccuracy = (hidEval > 0) ? (double)hidCorrect / hidEval * 100.0 : 0.0;

   Print("----- Backtest Signal Evaluation -----");
   Print("Total Signals Generated: ", ArraySize(signals));
   Print("Signals Evaluated: ", totalEvaluated);
   Print("Overall Accuracy: ", DoubleToString(overallAccuracy, 2), "%");
   Print("Regular Signals: ", regTotal, " | Evaluated: ", regEval, " | Accuracy: ", DoubleToString(regAccuracy, 2), "%");
   Print("Hidden Signals:  ", hidTotal, " | Evaluated: ", hidEval, " | Accuracy: ", DoubleToString(hidAccuracy, 2), "%");
  }
//+------------------------------------------------------------------+


代码分解

1. 头部信息和输入参数

在我们脚本的顶部,有一个定义良好的头部,它提供了关于代码的关键信息。

文件和作者信息


头部指定了文件名(RSI Divergence.mql5)、版权声明和作者个人资料的链接。这确保了正确的署名,并为用户提供了参考点,以便他们在需要检查更新或额外文档时使用。

版本控制和编译指令

#property 指令设置了重要的属性,如版本号和使用严格编译规则 (#property strict)。这有助于在开发和部署过程中保持一致性并减少潜在错误。接下来,输入参数部分对于定制化至关重要。这些参数允许您或任何用户在不修改核心代码的情况下,微调背离检测逻辑的行为。以下是几个要点:

RSI 和摆动点检测参数

  • InpRSIPeriod: 设置 RSI 指标的周期。
  • InpSwingLeftInpSwingRight: 定义在检测摆动点时,每一侧需要考虑的 K 线数量。调整这些值可以使摆动点检测更宽松或更严格。

背离和信号评估设置

  • InpLookback: 决定脚本将扫描过去多少根 K 线来寻找背离。
  • InpEvalBars: 指定在评估一个信号是否成功之前需要等待的 K 线数量。
  • InpMinBarsBetweenSignals: 通过在相似信号之间强制设置最小的 K 线间隔,有助于避免重复信号。

显示自定义

  • InpArrowOffset: 设置箭头相对于摆动点的偏移距离(以点为单位),以增强图表上的视觉清晰度。

可选的 RSI 阈值过滤器

  • InpUseRSIThreshold,以及 InpRSIOversoldInpRSIOverbought,提供了一个额外的过滤层。这确保了,对于看涨背离,较早的 RSI 摆动点处于超卖区域——前提是用户选择启用此过滤器。

//+------------------------------------------------------------------+
//|                                           RSI Divergence.mql5    |
//|                               Copyright 2025, Christian Benjamin |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2025, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.0"
#property strict

//---- Input parameters
input int    InpRSIPeriod             = 14;    // RSI period
input int    InpSwingLeft             = 1;     // Bars to the left for swing detection (relaxed)
input int    InpSwingRight            = 1;     // Bars to the right for swing detection (relaxed)
input int    InpLookback              = 100;   // Number of bars to scan for divergence
input int    InpEvalBars              = 5;     // Bars after which to evaluate a signal
input int    InpMinBarsBetweenSignals = 1;     // Minimum bars between same-type signals (allows frequent re-entry)
input double InpArrowOffset           = 3.0;   // Arrow offset (in points) for display
input double InpMinSwingDiffPct       = 0.05;  // Lower minimum % difference to qualify as a swing
input double InpMinRSIDiff            = 1.0;   // Lower minimum difference in RSI between swing points

// Optional RSI threshold filter for bullish divergence (disabled by default)
input bool   InpUseRSIThreshold       = false; // If true, require earlier RSI swing to be oversold for bullish divergence
input double InpRSIOversold           = 30;    // RSI oversold level
input double InpRSIOverbought         = 70;    // RSI overbought level (if needed for bearish)

2. 初始化指标

在这一部分,我们初始化我们的 RSI 指标。OnInit() 函数使用诸如交易品种、时间框架和用户指定的 RSI 周期等参数,为 RSI 指标创建一个句柄。这一步至关重要,因为所有后续操作都依赖于拥有一个有效的 RSI 句柄来获取指标数据。

  • iRSI函数被调用并传入必要的参数。
  • 实现了错误处理以捕获创建句柄时的任何失败。
  • 初始化确保我们的指标已准备好进行数据采集和分析。
int OnInit()
{
   // Create the RSI indicator handle with the specified period
   rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
   if(rsiHandle == INVALID_HANDLE)
   {
      Print("Error creating RSI handle");
      return(INIT_FAILED);
   }
   return(INIT_SUCCEEDED);
}

3.在每根新 K 线上获取数据

OnTick() 函数中,我们在处理前检查是否有新的 K 线已经收盘。这确保了我们的分析始终基于已完成的完整数据。然后,我们复制在一个可配置的回溯期内的 RSI 值、低点、高点、收盘价和时间数据的数组。将数组设置为序列,确保数据按最近的 K 线在索引 0 的顺序排列。

  • 代码等待下一根已收盘的 K 线,以避免处理不完整的数据。
  • 使用 CopyBufferCopyLow/High/Close/Time 等函数来检索 RSI 和价格数据。
  • 使用 ArraySetAsSeries 保留了时间序列分析的正确顺序。

void OnTick()
{
   // Process only once per new closed candle by comparing bar times
   datetime currentBarTime = iTime(_Symbol, _Period, 1);
   if(currentBarTime == lastBarTime)
      return;
   lastBarTime = currentBarTime;
   
   // Copy RSI data for a given lookback period
   ArrayResize(rsiBuffer, InpLookback);
   ArraySetAsSeries(rsiBuffer, true);
   if(CopyBuffer(rsiHandle, 0, 0, InpLookback, rsiBuffer) <= 0)
   {
      Print("Error copying RSI data");
      return;
   }
   
   // Copy price data (lows, highs, closes) and time data for analysis
   ArrayResize(lowBuffer, InpLookback);
   ArrayResize(highBuffer, InpLookback);
   ArrayResize(closeBuffer, InpLookback);
   ArraySetAsSeries(lowBuffer, true);
   ArraySetAsSeries(highBuffer, true);
   ArraySetAsSeries(closeBuffer, true);
   if(CopyLow(_Symbol, _Period, 0, InpLookback, lowBuffer) <= 0 ||
      CopyHigh(_Symbol, _Period, 0, InpLookback, highBuffer) <= 0 ||
      CopyClose(_Symbol, _Period, 0, InpLookback, closeBuffer) <= 0)
   {
      Print("Error copying price data");
      return;
   }
   
   ArrayResize(timeBuffer, InpLookback);
   ArraySetAsSeries(timeBuffer, true);
   if(CopyTime(_Symbol, _Period, 0, InpLookback, timeBuffer) <= 0)
   {
      Print("Error copying time data");
      return;
   }
   
   g_totalBars = InpLookback;
   
   // (Further processing follows here...)
}

4. 摆动点检测(识别摆动低点和摆动高点)

在我们可以检测背离之前,必须首先精确定位重要的摆动点。两个辅助函数,IsSignificantSwingLowIsSignificantSwingHigh,被用来识别局部最小值和最大值。它们通过将一根 K 线的低点或高点与给定窗口内的相邻 K 线进行比较,并检查百分比差异是否达到设定的阈值来实现这一点。
  • 这些函数会检查当前 K 线的左侧和右侧。
  • 它们计算百分比差异,以确保只标记出显著的摆动。
  • 这种过滤减少了噪音,确保我们的背离分析专注于有意义的市场变动。
bool IsSignificantSwingLow(int index, int left, int right)
{
   double currentLow = lowBuffer[index];
   // Check left side for local minimum condition
   for(int i = index - left; i < index; i++)
   {
      if(i < 0) continue;
      double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * 100.0;
      if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct)
         return false;
   }
   // Check right side for local minimum condition
   for(int i = index + 1; i <= index + right; i++)
   {
      if(i >= g_totalBars) break;
      double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * 100.0;
      if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct)
         return false;
   }
   return true;
}

bool IsSignificantSwingHigh(int index, int left, int right)
{
   double currentHigh = highBuffer[index];
   // Check left side for local maximum condition
   for(int i = index - left; i < index; i++)
   {
      if(i < 0) continue;
      double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * 100.0;
      if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct)
         return false;
   }
   // Check right side for local maximum condition
   for(int i = index + 1; i <= index + right; i++)
   {
      if(i >= g_totalBars) break;
      double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * 100.0;
      if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct)
         return false;
   }
   return true;
}

5. 背离检测:看涨和看跌背离

一旦识别出摆动点,算法就会比较近期的摆动点以检测背离。对于看跌背离,代码会查看两个摆动高点,并检查价格是否创出更高的高点(HH),而 RSI 却显示出更低的高点(LH)(或者对于隐藏看跌背离则相反)。对于看涨背离,它会类似地比较两个摆动低点。一个可选的 RSI 阈值可以通过确保较早的 RSI 读数处于超卖区域,来进一步验证看涨信号。

  • 使用两个近期的摆动点(高点或低点)进行背离分析。
  • 常规背离和隐藏背离的条件被清晰地分开。
  • 可选参数(如 RSI 超卖条件)为信号强度提供了额外的过滤。

// --- Bearish Divergence (using swing highs)
if(ArraySize(swingHighs) >= 2)
{
   ArraySort(swingHighs); // Ensure ascending order: index 0 is most recent
   int recent   = swingHighs[0];
   int previous = swingHighs[1];
   
   // Regular Bearish Divergence: Price makes a higher high while RSI makes a lower high
   if(highBuffer[recent] > highBuffer[previous] &&
      rsiBuffer[recent] < rsiBuffer[previous] &&
      (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff)
   {
      Print("Regular Bearish Divergence detected at bar ", recent);
      DisplaySignal("RegBearish Divergence", recent);
   }
   // Hidden Bearish Divergence: Price makes a lower high while RSI makes a higher high
   else if(highBuffer[recent] < highBuffer[previous] &&
           rsiBuffer[recent] > rsiBuffer[previous] &&
           (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff)
   {
      Print("Hidden Bearish Divergence detected at bar ", recent);
      DisplaySignal("HiddenBearish Divergence", recent);
   }
}

// --- Bullish Divergence (using swing lows)
if(ArraySize(swingLows) >= 2)
{
   ArraySort(swingLows); // Ensure ascending order: index 0 is most recent
   int recent   = swingLows[0];
   int previous = swingLows[1];
   
   // Regular Bullish Divergence: Price makes a lower low while RSI makes a higher low
   if(lowBuffer[recent] < lowBuffer[previous] &&
      rsiBuffer[recent] > rsiBuffer[previous] &&
      (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff)
   {
      // Optionally require the earlier RSI swing to be oversold
      if(!InpUseRSIThreshold || rsiBuffer[previous] <= InpRSIOversold)
      {
         Print("Regular Bullish Divergence detected at bar ", recent);
         DisplaySignal("RegBullish Divergence", recent);
      }
   }
   // Hidden Bullish Divergence: Price makes a higher low while RSI makes a lower low
   else if(lowBuffer[recent] > lowBuffer[previous] &&
           rsiBuffer[recent] < rsiBuffer[previous] &&
           (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff)
   {
      Print("Hidden Bullish Divergence detected at bar ", recent);
      DisplaySignal("HiddenBullish Divergence", recent);
   }
}

6. 信号显示与记录

当检测到背离时,在视觉上标记信号并记录其详细信息以供后续评估,这一点非常重要。DisplaySignal() 函数不仅在图表上创建一个箭头(对看涨和看跌信号使用不同的箭头代码和颜色),还会更新最新信号的标签,并将信号的元数据存储在一个全局数组中。这种系统性的记录使得后续可以对策略进行回测

  • 通过检查相似 K 线的信号是否已存在,来防止重复信号。
  • 箭头和标签等视觉提示增强了图表的可读性。
  • 每个信号都连同其类型、K 线索引、时间和价格等详细信息一起存储,以便于后续的性能评估。

void DisplaySignal(string signalText, int barIndex)
{
   // Prevent duplicate signals on the same or nearby bars
   for(int i = 0; i < ArraySize(signals); i++)
   {
      if(StringFind(signals[i].type, signalText) != -1)
         if(MathAbs(signals[i].barIndex - barIndex) < InpMinBarsBetweenSignals)
            return;
   }
     
   // Update a label for the latest regular signal
   if(StringFind(signalText, "Reg") != -1)
   {
      string labelName = "LatestSignal";
      if(ObjectFind(0, labelName) == -1)
      {
         if(!ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0))
         {
            Print("Failed to create LatestSignal label");
            return;
         }
         ObjectSetInteger(0, labelName, OBJPROP_CORNER, 0);
         ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, 10);
         ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, 20);
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrWhite);
      }
      ObjectSetString(0, labelName, OBJPROP_TEXT, signalText);
   }
     
   // Create an arrow object to mark the signal on the chart
   string arrowName = "Arrow_" + signalText + "_" + IntegerToString(barIndex);
   if(ObjectFind(0, arrowName) < 0)
   {
      int arrowCode = 0;
      double arrowPrice = 0.0;
      color arrowColor = clrWhite;
      double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
          
      if(StringFind(signalText, "Bullish") != -1)
      {
         arrowCode  = 233; // Wingdings up arrow
         arrowColor = clrLime;
         arrowPrice = lowBuffer[barIndex] - (InpArrowOffset * point);
      }
      else if(StringFind(signalText, "Bearish") != -1)
      {
         arrowCode  = 234; // Wingdings down arrow
         arrowColor = clrRed;
         arrowPrice = highBuffer[barIndex] + (InpArrowOffset * point);
      }
          
      if(!ObjectCreate(0, arrowName, OBJ_ARROW, 0, timeBuffer[barIndex], arrowPrice))
      {
         Print("Failed to create arrow object ", arrowName);
         return;
      }
      ObjectSetInteger(0, arrowName, OBJPROP_COLOR, arrowColor);
      ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode);
   }
     
   // Record the signal details for later backtesting evaluation
   SignalInfo sig;
   sig.type = signalText;
   sig.barIndex = barIndex;
   sig.signalTime = timeBuffer[barIndex];
   if(StringFind(signalText, "Bullish") != -1)
      sig.signalPrice = lowBuffer[barIndex];
   else
      sig.signalPrice = highBuffer[barIndex];
   ArrayResize(signals, ArraySize(signals) + 1);
   signals[ArraySize(signals) - 1] = sig;
     
   UpdateSignalCountLabel();
}

反初始化时的回测评估

最后,当EA反初始化时,会调用 EvaluateSignalsAndPrint() 函数。此函数通过将信号发出后几根 K 线的价格变动与信号记录的价格进行比较,来回顾性地评估所有已记录的信号。它会计算常规信号和隐藏信号的准确率,为我们的背离策略的性能提供宝贵的反馈。

  • 该函数检索完整的历史收盘价数据。
  • 每个信号在固定的 K 线数量(由 InpEvalBars 设置)之后被评估。
  • 会为整体信号以及分别为常规信号和隐藏信号计算准确率指标,以辅助性能验证。

void EvaluateSignalsAndPrint()
{
   double closeAll[];
   int totalBars = CopyClose(_Symbol, _Period, 0, WHOLE_ARRAY, closeAll);
   if(totalBars <= 0)
   {
      Print("Error copying complete close data for evaluation");
      return;
   }
   ArraySetAsSeries(closeAll, true);
   
   int totalEvaluated = 0, regTotal = 0, hidTotal = 0;
   int regEval = 0, hidEval = 0;
   int regCorrect = 0, hidCorrect = 0;
   
   for(int i = 0; i < ArraySize(signals); i++)
   {
      int evalIndex = signals[i].barIndex - InpEvalBars;
      if(evalIndex < 0)
         continue;
      double evalClose = closeAll[evalIndex];
      
      if(StringFind(signals[i].type, "Bullish") != -1)
      {
         if(StringFind(signals[i].type, "Reg") != -1)
         {
            regTotal++;
            regEval++;
            if(evalClose > signals[i].signalPrice)
               regCorrect++;
         }
         else if(StringFind(signals[i].type, "Hidden") != -1)
         {
            hidTotal++;
            hidEval++;
            if(evalClose > signals[i].signalPrice)
               hidCorrect++;
         }
         totalEvaluated++;
      }
      else if(StringFind(signals[i].type, "Bearish") != -1)
      {
         if(StringFind(signals[i].type, "Reg") != -1)
         {
            regTotal++;
            regEval++;
            if(evalClose < signals[i].signalPrice)
               regCorrect++;
         }
         else if(StringFind(signals[i].type, "Hidden") != -1)
         {
            hidTotal++;
            hidEval++;
            if(evalClose < signals[i].signalPrice)
               hidCorrect++;
         }
         totalEvaluated++;
      }
   }
     
   double overallAccuracy = (totalEvaluated > 0) ? (double)(regCorrect + hidCorrect) / totalEvaluated * 100.0 : 0.0;
   double regAccuracy = (regEval > 0) ? (double)regCorrect / regEval * 100.0 : 0.0;
   double hidAccuracy = (hidEval > 0) ? (double)hidCorrect / hidEval * 100.0 : 0.0;
     
   Print("----- Backtest Signal Evaluation -----");
   Print("Total Signals Generated: ", ArraySize(signals));
   Print("Signals Evaluated: ", totalEvaluated);
   Print("Overall Accuracy: ", DoubleToString(overallAccuracy, 2), "%");
   Print("Regular Signals: ", regTotal, " | Evaluated: ", regEval, " | Accuracy: ", DoubleToString(regAccuracy, 2), "%");
   Print("Hidden Signals:  ", hidTotal, " | Evaluated: ", hidEval, " | Accuracy: ", DoubleToString(hidAccuracy, 2), "%");
}


测试结果

使用 MetaEditor 成功编译您的 EA 后,将其拖到图表上进行测试。请确保您使用的是模拟账户,以避免真实资金的风险。您也可以在图表上添加 RSI 指标,以便在测试 EA 时轻松确认信号。为此,请导航到“指标”选项卡,在“面板”文件夹下选择 RSI 指标,并设置您偏好的参数,确保它们与您 EA 中的参数相匹配。查看下面的 GIF,它说明了如何在 MetaTrader 5 图表上添加 RSI 指标窗口。您还可以看到一个已确认的信号,即在一分钟时间框架上的常规看涨背离。

看涨收敛

图 5. 设置指标与测试结果 1

以下是我们在 Boom 500 上进行的另一项测试,该测试得到了价格行为和 RSI 指标的双重确认,显示了一个卖出信号。

测试

图 6. 测试结果 2

另一项测试使用了下面的 GIF 中的回测功能,其中显示了几个积极的变化。如果您仔细观察,会注意到既有隐藏的延续信号,也有常规信号。然而,尽管一些信号产生了积极影响,但由于缺乏确认,仍有少数信号需要被过滤掉。

回测

图 7. 测试结果 3


结论

该工具已被证明与价格行为高度一致,这也是我们系列文章的核心目标,即创建尽可能多的价格行为分析工具。我非常欣赏 RSI 指标如何通过从背离中提取积极信号来有效地与价格行为互动。我们进行的测试显示了有希望的结果和积极的趋势。

然而,我认为现在是时候引入另一项增强了,即使用外部库来进行精确和准确的摆动点识别,从而提高信号的准确性。我的建议是彻底测试该工具,并调整其参数以适应您的交易风格。请记住,每个生成的信号在入场前都应进行交叉核对,因为该工具旨在帮助您监控市场并确认您的整体策略。

日期 工具名称  说明 版本  更新  提示
01/10/24 图表投影仪 用于叠加前一天价格走势(带“幽灵”效果)的脚本。 1.0 首次发布 Lynnchris工具箱中的第一款工具
18/11/24 分析评论 它以表格形式提供前一日的市场信息,并预测市场的未来走向。 1.0 首次发布 Lynnchris工具箱中的第二款工具
27/11/24 分析大师 市场指标每两小时定期更新  1.01 第二版 Lynnchris工具箱中的第三款工具
02/12/24 分析预测器  每两小时定期更新市场指标,并集成Telegram推送功能。 1.1 第三版 工具4
09/12/24 波动性导航工具 该EA使用布林带、RSI和ATR指标分析市场状况。 1.0 首次发布 工具5
19/12/24 均值回归信号收割器  使用均值回归策略分析市场并提供信号  1.0  首次发布  工具6 
9/01/25  信号脉冲  多时间框架分析器 1.0  首次发布  工具7 
17/01/25  指标看板  带按钮的分析面板  1.0  首次发布 工具8 
21/01/25 外部资金流 通过外部库进行分析 1.0  首次发布 工具9 
27/01/25 VWAP 成交量加权平均价格   1.3  首次发布  工具10 
02/02/25  Heikin Ashi  势平滑与反转信号识别  1.0  首次发布  工具11
04/02/25  FibVWAP  通过 Python 分析生成信号  1.0  首次发布  工具12
14/02/25  RSI 背离  价格走势与 RSI 背离的对比  1.0  首次发布  工具13 

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17198

附加的文件 |
RSI_DIVERGENCE.mq5 (33.61 KB)
最近评论 | 前往讨论 (1)
linfo2
linfo2 | 18 2月 2025 在 20:05
再次感谢克里斯精心撰写的有益文章。作为我修改的模板,我非常感激。我也非常欣赏您博学的思想和实用的实现方法。对于我来说,我在 EvaluateSignalsAndPrint 上遇到了问题,它写道函数返回 Error Copying Compete close data for evaluation。最后一个错误是 "4003"。我不够聪明,不知道为什么使用WHOLE_ARRAY 变量时函数会失败。对我来说,如果将 copyclose 中的 "WHOLE_ARRAY "替换为收盘条数,就可以得到返回的 "Backtest Signal Evaluation"(回测信号评估)。顺便说一下,我所在的时区很奇怪,是 GMT +13 时区,有时会出现本地和服务器日期和时间不一致的问题,但这也许能帮到别人。
将 MQL5 与数据处理包集成(第 4 部分):大数据处理 将 MQL5 与数据处理包集成(第 4 部分):大数据处理
本部分探讨了将 MQL5 与强大的数据处理工具集成的高级技术,重点是高效处理大数据,以增强交易分析和决策。
在 MQL5 中创建交易管理面板(第九部分):代码组织(二):模块化 在 MQL5 中创建交易管理面板(第九部分):代码组织(二):模块化
在本次讨论中,我们进一步将 MQL5 程序分解为更小、更易于管理的模块。然后,这些模块化组件将被集成到主程序中,从而增强其组织性和可维护性。这种方法简化了我们主程序的结构,并使各个组件可以在其他EA和指标的开发中复用。通过采用这种模块化设计,我们为未来的增强功能创建了坚实的基础,这将使我们的项目和更广泛的开发者社区都受益。
MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA
在本文中,我们将在 MQL5 中构建一个使用动态仓位缩放的网格交易EA。我们将涵盖策略设计、代码实现和回测过程。最后,我们将分享用于优化该自动化交易系统的关键方案和最佳实践。
在MQL5中构建带自定义画布图形的凯特纳通道(Keltner Channel)指标 在MQL5中构建带自定义画布图形的凯特纳通道(Keltner Channel)指标
本文将介绍如何在MQL5中构建一个带自定义画布图形的凯特纳通道(Keltner Channel)指标。我们将详细阐述移动平均线(MA)与平均真实波幅(ATR)计算的集成方法,以及如何增强型图表的可视化效果。此外,我们还将介绍如何通过回测评估该指标的实际交易表现,为实战交易提供有价值的参考依据。