Разработка инструментария для анализа движения цен (Часть 13): RSI Sentinel
Содержание
Введение
Дивергенция — это понятие в техническом анализе, которое означает отклонение индикатора, например импульса или осциллятора, от движения цены. По сути, когда цена формирует новые максимумы или минимумы, не отражаемые индикатором, это может сигнализировать об ослаблении тренда и потенциально предвещать разворот или изменение импульса. Дивергенция RSI — простой способ определить потенциальные развороты рынка. Когда цена движется в одном направлении, а RSI — в другом, это может сигнализировать об изменении тренда. Однако ручное сканирование графиков на предмет этих сигналов может быть медленным и подверженным ошибкам. Тут на помощь приходит автоматизация.
В этой статье мы создадим советник на MQL5, который автоматически обнаруживает сигналы дивергенции RSI. Советник отметит эти сигналы четкими стрелками на вашем графике и предоставит краткую сводку, чтобы вы могли быстро увидеть, что происходит. Независимо от того, новичок вы или опытный трейдер, этот инструмент поможет вам обнаружить торговые возможности, которые вы сможете проверить перед совершением сделок, и все это без траты часов на ручной анализ. Давайте углубимся и посмотрим, как советник RSI Divergence может упростить ваш торговый процесс.
Обзор стратегии
Дивергенция RSI
Дивергенция RSI происходит, когда индекс относительной силы (RSI) движется в направлении, отличном от направления цены актива, что сигнализирует о возможном изменении ценового импульса. Этот контраст между RSI и ценовым движением является ключевым индикатором, который трейдеры используют для прогнозирования разворотов рынка или продолжения тренда. Обычно RSI следует за динамикой цены, тем самым подтверждая преобладающие тренды. Однако когда появляется дивергенция, она выявляет несоответствие, которое часто предшествует значительному движению цены. Раннее распознавание этих сигналов может иметь решающее значение для определения времени входа на рынок и выхода из него.
В контексте дивергенции RSI существуют два основных типа
1. Обычная дивергенция RSI
Регулярная дивергенция RSI обычно рассматривается как сигнал разворота. Это свидетельствует о том, что текущий тренд теряет силу и может вот-вот развернуться.
- Обычная бычья дивергенция RSI
Происходит, когда цена формирует более низкий минимум, а RSI формирует более высокий минимум. Это говорит о том, что, хотя цена снижается, импульс начинает смещаться вверх, намекая на потенциальный разворот в сторону восходящего тренда.

Рис 1. Бычье расхождение
- Обычная медвежья дивергенция RSI
Происходит, когда цена формирует более высокий максимум, а RSI — более низкий максимум. Несмотря на рост цен, ослабление импульса (на что указывает RSI) сигнализирует о возможном надвигающемся спаде.

Рис 2. Медвежья дивергенция
2. Скрытая дивергенция RSI
Скрытая дивергенция RSI интерпретируется как сигнал продолжения тренда, а не предстоящего разворота. Это подтверждает, что текущий тренд все еще имеет силу, даже несмотря на временное расхождение RSI и цены.
- Скрытая бычья дивергенция RSI: если при восходящем тренде цена формирует более высокий минимум, а RSI — более низкий минимум, это указывает на то, что коррекция носит временный характер и восходящий тренд, скорее всего, продолжится.

Рис. 3. Скрытая бычья дивергенция
- Скрытая медвежья дивергенция RSI: при нисходящем тренде, когда цена формирует более низкий максимум, а RSI формирует более высокий максимум, это подтверждает силу нисходящего тренда и предполагает, что нисходящее движение, вероятно, сохранится.

Рис 4. Скрытая медвежья дивергенция
Ниже представлена сводная таблица, в которой отражены основные различия между типами дивергенции RSI:
| Тип дивергенции RSI | Ценовое действие | Действие RSI | Тип сигнала | Ожидание |
|---|---|---|---|---|
| Обычный бычий | Low Low(LL) | Higher Low(HL) | Разворот вверх | Нисходящий тренд к восходящему тренду |
| Обычный медвежий | Higher High(HH) | Lower Higher(LH) | Разворот вниз | Восходящий тренд к нисходящему тренду |
| Скрытый бычий | High Low(HL) | Low Low(LL) | Продолжение вверх | Восходящий тренд продолжается |
| Скрытый медвежий | Lower High (LH) | High High(HH) | Продолжение вниз | Нисходящий тренд продолжается |
Подводя итог, можно сказать, что советник непрерывно сканирует данные как цен, так и RSI за определенный период времени, чтобы обнаружить расхождения в их движениях - то, что мы называем дивергенцией RSI.
Вот что он делает:
1. Сбор и подготовка данных
Советник собирает значения RSI вместе с соответствующими ценовыми данными (минимумы, максимумы, закрытия и время) из последних баров. Это гарантирует, что анализ всегда будет основан на самой последней и полной информации.2. Определение точек колебания
Затем он определяет локальные максимумы и минимумы колебаний как по данным цены, так и по данным RSI. Эти точки колебания служат контрольными маркерами для нашего анализа дивергенции.3. Обнаружение обычной дивергенции
- Обычная бычья дивергенция: советник ищет случаи, когда цена формирует более низкий минимум, а RSI — более высокий минимум, сигнализируя о том, что нисходящий тренд, возможно, теряет импульс и может развернуться вверх.
- Обычная медвежья дивергенция: также проверяет ситуации, когда цена достигает более высокого максимума, а RSI формирует более низкий максимум, что указывает на возможное приближение конца восходящего тренда по мере ослабления импульса.
- Скрытая бычья дивергенция: При восходящем тренде, если цена формирует более высокий минимум, а RSI фиксирует более низкий минимум, советник идентифицирует это как признак того, что общий восходящий тренд все еще силен, несмотря на временный откат.
- Скрытая медвежья дивергенция: И наоборот, если во время нисходящего тренда цена достигает более низкого максимума, а RSI показывает более высокий максимум, это подтверждает, что нисходящий тренд, скорее всего, продолжится.
5. Генерация визуальных и логарифмических сигналов
При обнаружении расхождения, будь оно обычным или скрытым, советник визуально отмечает событие на графике (с помощью стрелок и меток) и регистрирует сведения о сигнале для дальнейшего анализа или тестирования на исторических данных. Узнайте больше о том, как выполняются указанные выше процессы, в разделе "Разбор кода" ниже.
Код 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 и колебаний (swing)
- InpRSIPeriod - период RSI.
- InpSwingLeft и InpSwingRight - сколько полос с каждой стороны учитывается при обнаружении точек колебания. Изменение этих значений делает обнаружение колебаний более мягким или более строгим.
Настройки дивергенции и оценки сигнала
- InpLookback - сколько баров в прошлом скрипт будет сканировать на предмет расхождений.
- InpEvalBars - количество баров, которое необходимо выждать перед оценкой успешности сигнала.
- InpMinBarsBetweenSignals - помогает избежать дублирования сигналов, обеспечивая минимальное расстояние между похожими сигналами.
Настройки отображения
- InpArrowOffset - расстояние (в пунктах), на которое смещаются стрелки от точки колебания, что повышает визуальную ясность на графике.
Дополнительный пороговый фильтр RSI
- InpUseRSIThreshold, InpRSIOversold и InpRSIOverbought обеспечивает дополнительный уровень сортировки. Это гарантирует, что в случае бычьей дивергенции более раннее колебание 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. Сбор данных о каждой новой свече
В функции OnTick() мы проверяем, закрылась ли новая свеча перед обработкой. Это гарантирует, что наш анализ всегда работает с полными данными. Затем мы копируем массивы значений RSI, минимумов, максимумов, закрытий и временных данных за настраиваемый период просмотра. Настройка массивов как серий гарантирует, что данные будут упорядочены, начиная с самого последнего столбца с индексом 0.
- Код ждет закрытия следующей свечи, чтобы избежать обработки неполных данных.
- Данные RSI и цен извлекаются с помощью таких функций, как CopyBuffer и CopyLow/High/Close/Time.
- Использование 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. Обнаружение колебаний (определение минимумов и максимумов колебаний)
Прежде чем обнаружить расхождения, мы должны сначала определить существенные точки колебаний. Две вспомогательные функции, IsSignificantSwingLow и IsSignificantSwingHigh, используются для определения локальных минимумов и максимумов. Они делают это, сравнивая минимум или максимум бара с соседними барами в заданном окне и проверяя, что процентная разница соответствует установленному пороговому значению.- Функции проверяют как левую, так и правую часть текущего бара.
- Они рассчитывают процентную разницу, чтобы гарантировать, что отмечаются только существенные колебания.
- Такая сортировка снижает уровень шума, гарантируя, что наш анализ дивергенций будет сосредоточен на значимых движениях рынка.
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. Обнаружение дивергенций: медвежья и бычья дивергенция
После определения точек колебаний алгоритм сравнивает недавние колебания, чтобы обнаружить расхождения. Для медвежьей дивергенции код определяет два максимума колебания и проверяет, достигает ли цена более высокого максимума, в то время как RSI показывает более низкий максимум (или наоборот для скрытой медвежьей дивергенции). В случае бычьей дивергенции аналогичным образом сравниваются два минимума колебания. Дополнительный порог 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() не только создает стрелку на графике (используя различные коды и цвета стрелок для бычьих и медвежьих сигналов), но и обновляет метку для последнего сигнала и сохраняет метаданные сигнала в глобальном массиве. Эта систематическая запись позволит в дальнейшем тестировать стратегию на истории.
- Дублирование сигналов предотвращается путем проверки наличия сигнала для аналогичного бара.
- Визуальные подсказки, такие как стрелки и метки, повышают читаемость графика.
- Каждый сигнал сохраняется с такими данными, как тип, индекс бара, время и цена, что облегчает последующую оценку эффективности.
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(); }
Оценка тестирования на истории при деинициализации
Наконец, функция EvaluateSignalsAndPrint() вызывается при деинициализации советника. Эта функция ретроспективно оценивает все записанные сигналы, сравнивая движение цены через несколько баров после сигнала с записанной ценой сигнала. Она рассчитывает точность как обычных, так и скрытых сигналов, предоставляя ценную обратную связь относительно эффективности нашей стратегии дивергенции.
- Функция извлекает полные исторические данные о закрытии.
- Каждый сигнал оценивается после фиксированного количества баров (установленного 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 перетащите его на график для тестирования. Обязательно используйте демо-счет, чтобы не рисковать реальными деньгами. Вы также можете добавить индикатор RSI на свой график для легкого подтверждения сигналов при тестировании вашего советника. Для этого перейдите на вкладку "Индикаторы", выберите индикатор RSI в папке Panels и задайте желаемые параметры, убедившись, что они соответствуют параметрам вашего советника. Ниже показано, как добавить окно индикатора RSI на график MetaTrader 5. Вы также можете увидеть подтвержденный сигнал — обычную бычью дивергенцию на минутном таймфрейме.

Рис. 5. Настройка индикатора и результата теста 1
Ниже представлен еще один тест, который мы провели на Boom 500, подтвержденный как ценовым действием, так и индикатором RSI, показывающим сигнал на продажу.

Рис. 6. Результат теста 2
Еще один тест был проведен с использованием тестирования на истории на GIF-изображении ниже, где показано несколько позитивных изменений. Если вы посмотрите внимательно, вы заметите как скрытые сигналы продолжения, так и обычные сигналы. Однако некоторые сигналы необходимо отфильтровать из-за отсутствия подтверждения, несмотря на их положительное влияние.

Рис. 7. Результат теста 3
Заключение
Этот инструмент доказал свою исключительную согласованность с ценовым действием, что и является основной целью нашей серии — создать как можно больше инструментов анализа ценового действия. Я по-настоящему оценил, насколько эффективно индикатор RSI взаимодействует с ценовым движением, извлекая положительные сигналы из дивергенций. Проведенные нами испытания показали многообещающие результаты и положительную динамику.
Однако я считаю, что пришло время представить еще одно усовершенствование, которое использует внешние библиотеки для точной идентификации колебаний, тем самым повышая точность сигнала. Мой совет — тщательно протестируйте инструмент и настройте его параметры в соответствии со своим стилем торговли. Помните, что каждый сгенерированный сигнал следует перепроверять перед входом, поскольку инструмент предназначен для того, чтобы помочь вам следить за рынком и подтверждать вашу общую стратегию.
| Дата | Название инструмента | Описание | Версия | Обновления | Примечания |
|---|---|---|---|---|---|
| 01/10/24 | Chart Projector | Скрипт для наложения эффекта призрака на движение цены за предыдущий день. | 1.0 | Первоначальная версия | Первый инструмент в Lynnchris Tools Chest |
| 18/11/24 | Analytical Comment | Предоставляет информацию за предыдущий день в табличном формате, а также прогнозирует будущее направление рынка. | 1.0 | Первоначальная версия | Второй инструмент в Lynnchris Tools Chest |
| 27/11/24 | Analytics Master | Регулярное обновление рыночных показателей каждые два часа | 1.01 | Вторая версия | Третий инструмент в Lynnchris Tools Chest |
| 02/12/24 | Analytics Forecaster | Регулярное обновление рыночных показателей каждые два часа с интеграцией с Telegram | 1.1 | Третья версия | Инструмент номер 4 |
| 09/12/24 | Volatility Navigator | Советник анализирует рыночные условия с помощью полос Боллинджера, RSI и ATR. | 1.0 | Первоначальная версия | Инструмент номер 5 |
| 19/12/24 | Mean Reversion Signal Reaper | Анализирует рынок и генерирует сигналы, используя стратегию возврата к среднему | 1.0 | Первоначальная версия | Инструмент номер 6 |
| 9/01/25 | Signal Pulse | Анализирует несколько таймфреймов | 1.0 | Первоначальная версия | Инструмент номер 7 |
| 17/01/25 | Metrics Board | Панель с кнопками для анализа | 1.0 | Первоначальная версия | Инструмент номер 8 |
| 21/01/25 | External Flow | Аналитика с помощью внешних библиотек | 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 DIVERGENCE | Дивергенция цены и RSI | 1.0 | Первоначальная версия | Инструмент номер 13 |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17198
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа движения цен (Часть 14): Parabolic Stop and Reverse
От новичка до эксперта: Индикатор Market Periods Synchronizer
Разработка динамического советника на нескольких парах (Часть 3): Стратегии возврата к среднему и моментума
Нейросети в трейдинге: Агрегация движения по времени (Окончание)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования