Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
Содержание
- Введение
- Архитектурный переход от индикатора к советнику
- Что рассматривается в этой статье
- Основа советника — входные параметры и настройка
- Модуль расчета Adaptive SuperTrend
- Функция обработки торговых операций
- Исполнение торговых операций и управление позициями
- Трейлинг-стопы — защита прибыли
- Временные и сессионные фильтры
- Тестер стратегий и анализ результатов
- Заключение
Введение
В предыдущей статье (Часть II) мы разработали индикатор Adaptive SuperTrend — систему следования за трендом, которая сочетает SuperTrend с анализом дивергенций на основе либо MPO4, либо RSI. Динамически уменьшая множитель ATR при появлении дивергенции против текущего тренда, индикатор пытается раньше выявлять ослабление тренда, сохраняя при этом структуру, благодаря которой SuperTrend популярен среди трейдеров.
В этой статье мы создадим автоматический советник на основе концепции Adaptive SuperTrend. Вместо использования отдельного индикатора, подключенного к графику, все расчеты будут выполняться внутри советника. Это сделает его автономным и пригодным как для реальной торговли, так и для тестирования стратегии. По ходу работы мы добавим динамический расчет размера позиции, установку стоп-лоссов на основе ATR, трейлинг-стопы по соотношению прибыли и риска, дополнительное подтверждение для защиты от перерисовки, которое можно включить, а также фильтрацию сделок по торговым сессиям.
Архитектурный переход от индикатора к советнику
Нам необходимо понять, что меняется при переходе от индикатора к автоматизированной торговой системе. При таком переходе принципиально меняется то, как программа взаимодействует с рынком и управляет собственным состоянием. Индикатор — это пассивный инструмент. Он получает данные о ценах, выполняет вычисления и отображает результаты на графике. Его работа заканчивается после отображения результатов. Советник же — это активная программа. Он выполняет действия в соответствии с торговой логикой, взаимодействует с брокером и управляет позициями. Он должен обрабатывать сбои, проверять данные, отслеживать собственные сделки и реагировать на изменения рыночных условий в реальном времени. Это различие в назначении существенно влияет на структуру кода, обработку ошибок и управление состоянием.
Два подхода — внутренний расчет и внешний индикатор
При создании советника на основе пользовательского индикатора нам нужно решить: будет ли советник самостоятельно вычислять значения индикатора или использовать скомпилированный файл индикатора, вызывая функцию iCustom()?
Подход 1 — внешний индикатор через iCustom()
Простой способ — подключить скомпилированный индикатор к графику и считывать значения его буферов с помощью iCustom(). Советник становится легковесной программной оберткой, которая использует сигналы, сформированные внешним индикатором.
Преимущества:
- Код советника остается короче и проще.
- Логика индикатора поддерживается в одном месте, поэтому любое его обновление автоматически отражается на работе советника.
- Визуальная отладка становится проще, поскольку индикатор уже отображается на графике.
Недостатки:
- Для работы советника файл индикатора должен находиться в папке индикаторов.
- Для тестирования на исторических данных файл индикатора должен находиться в соответствующей папке.
- Производительность может снизиться, поскольку советнику приходится многократно обращаться к данным внешних буферов.
- Советник начинает зависеть от конкретной структуры буферов индикатора и принятых правил именования.
Подход 2 — внутренний расчет
Другой вариант — встроить весь модуль расчета индикатора непосредственно в советник. Советник самостоятельно вычисляет все необходимые значения, не используя внешний файл.
Преимущества:
- советник автономен — подключите его к любому графику, и он сразу начнет работать;
- тестирование на исторических данных проще, поскольку внешние зависимости отсутствуют;
- производительность повышается, поскольку все вычисления выполняются в памяти без межпроцессного взаимодействия;
- советник можно модифицировать и оптимизировать как единое целое.
Недостатки:
- код советника становится более объемным и сложным;
- если требуется использовать и визуальный индикатор, и советник, логику индикатора необходимо дублировать;
- любые изменения в логике индикатора необходимо вручную переносить в код советника.
Выбранный нами подход
В этой статье мы будем использовать подход с внутренними расчетами. Представленный здесь советник полностью рассчитывает значения Adaptive SuperTrend в собственном коде, поэтому подключать индикатор к графику не требуется. Мы выбрали внутренние расчеты, чтобы контролировать поток данных и устранить внешние зависимости.
Что рассматривается в этой статье
В этом вводном разделе мы рассмотрели переход от индикатора к советнику и объяснили решение использовать внутренние расчеты. В следующих разделах мы:
- создадим основу советника, определив входные параметры, глобальные переменные и порядок инициализации;
- реализуем полный модуль расчета Adaptive SuperTrend (при необходимости с отсылками к статье об индикаторе);
- добавим обнаружение сигналов с обработкой появления нового бара и возможностью дождаться одного или нескольких дополнительных баров для подтверждения сигнала;
- реализуем управление риском с динамическим расчетом размера позиции;
- добавим трейлинг-стоп и отслеживание позиций;
- добавим торговые фильтры по сессиям.
Ниже показана архитектура советника, реализованная в этой статье:

Основа советника — входные параметры и настройка
После принятия решения об архитектуре мы переходим к созданию основы советника. В этом разделе рассматриваются входные параметры, глобальные переменные, а также процедуры инициализации и освобождения ресурсов. Важно правильно реализовать эти элементы, поскольку от них зависят поведение советника, возможности его настройки и надежность работы.
Первый шаг — подготовить точки входа программы на языке MQL5: указать имя файла советника и необходимую директиву #property, а также создать заготовки функций OnInit(), OnTick() и OnDeinit(). Это выглядит следующим образом:
//+------------------------------------------------------------------+ //| The Adaptive SuperTrend EA.mq5 | //| Copyright 2026, soloharbinger | //| https://www.mql5.com/ru/users/soloharbinger | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, soloharbinger" #property link "https://www.mql5.com/ru/users/soloharbinger" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- }
Затем сразу после директивы #property советника мы подключаем торговую библиотеку для выполнения торговых операций.
#include <Trade/Trade.mqh> Входные параметры
Советник настраивается с помощью входных параметров. Мы группируем их по логическим разделам, чтобы пользователь мог быстро найти и настроить связанные параметры. Полный набор входных параметров определяется сразу после подключения торгового класса.
//--- INPUTS input group "=== Adaptive Oscillator Settings ===" enum ENUM_DIV_SOURCE { DIV_SRC_MPO = 0, DIV_SRC_RSI = 1 }; input ENUM_DIV_SOURCE InpSource = DIV_SRC_MPO; // Oscillator Source input int MPO_len = 6; // MPO4 Length input int MPO_smooth = 7; // MPO4 Smoothing input int DivRsiPeriod = 14; // RSI Period input int DivPivotLen = 2; // Pivot Length of Divergence input group "=== SuperTrend Configuration ===" input int SuperTrendPeriod = 10; // SuperTrend Period input double SuperTrendMultiplier = 3.0; // SuperTrend [ATR] Multiplier input ENUM_TIMEFRAMES SuperTrendTimeframe = PERIOD_CURRENT; // SuperTrend Timeframe input group "=== Divergence Shrinking ===" input bool EnableAdaptiveShrink = false; // Enable Adaptive Shrinking input double DivSensitivity = 0.3; // Shrink Factor [0.0 = off, 1.0 Extreme] input group "=== Repainting Protection ===" input bool UseRepaintingProtection = false; // Anti-Repainting Protection input int ConfirmationBars = 1; // Bars for Signal Confirmation [1-3] input group "=== Risk Management ===" input double riskPercent = 0.5; // Risk Per Trade [%] input int ATRperiod = 14; // Stop-loss ATR Period input double ATRmultiplier = 2.0; // Stop-loss Multiplier input double TP_Ratio = 2.0; // Take-profit Risk-Reward-Ratio [RRR] input bool UseTrailingStops = false; // Enable Trailing Stops input double TrailingStartRR = 1.0; // Start Trailing at this RRR input double TrailingStepRR = 1.0; // Trailing Step RRR input group "=== Time & Session Filters ===" input bool EnableSessionFilter = false; // Enable Session Filter input int BrokerGMTOffset = 3; // Broker GMT Offset [e.g., 3 for GMT+3] input bool EnableAsianSession = true; // Enable Asian Session input bool EnableLondonSession = true; // Enable London Session input bool EnableNewYorkSession = true; // Enable New York Session input bool EnableSydneySession = true; // Enable Sydney Session
Пояснение
Большинство этих параметров понятны сами по себе, но на некоторые стоит обратить внимание.
Параметр осциллятора-источника InpSource позволяет пользователю выбрать между осциллятором MPO4 (из первой части этой серии) и стандартным RSI. Это полезно для трейдеров, которые предпочитают привычный индикатор моментума. Индикатор MPO4 рассчитывает давление на основе взвешенных тел свечей, тогда как RSI — стандартный осциллятор перекупленности и перепроданности.
Параметр таймфрейма SuperTrendTimeframe позволяет рассчитывать значения SuperTrend на таймфрейме, отличном от таймфрейма графика. Например, вы можете подключить советник к минутному графику, но рассчитывать значения SuperTrend на пятнадцатиминутном таймфрейме. Это распространенный метод анализа с использованием нескольких таймфреймов.
Параметр адаптивного сжатия EnableAdaptiveShrink включает механизм сжатия. Если этот параметр отключен, советник работает как стандартный индикатор SuperTrend без корректировки на основе дивергенций.
Параметр защиты от перерисовки UseRepaintingProtection добавляет еще один слой подтверждения сигнала. Если защита включена, после появления потенциального сигнала советник ожидает указанное количество баров (ConfirmationBars), прежде чем выполнять торговые действия. Это помогает отфильтровать ложные сигналы на рынке с частыми сменами направления, хотя и вносит небольшую задержку.
Входные параметры управления риском задают динамический расчет размера позиции, установку стоп-лоссов и трейлинг. Размер позиции в лотах рассчитывается на основе riskPercent (процент от баланса счета, которым рискуют в каждой сделке), расстояния до стоп-лосса, определяемого по ATR, и стоимости тика инструмента. В алгоритме трейлинга используются параметры TrailingStartRR и TrailingStepRR для перемещения стоп-лосса по мере того, как позиция становится прибыльной.
Сессионные фильтры позволяют ограничить торговлю определенными торговыми сессиями. Это позволяет избегать торговли в периоды низкой ликвидности или сосредоточиться на часах наибольшей активности по выбранному инструменту.
Глобальные переменные и структуры
После входных параметров мы объявляем глобальные переменные для хранения хэндлов, значений буферов и данных состояния. Советник использует класс CTrade из стандартной библиотеки для исполнения ордеров, поэтому мы создаем его экземпляр в глобальной области.
//--- EA Globals CTrade trade; input int solomagic = 225; // Set Magic Number //--- State & Buffer Variables double ATRBuffer[]; int ATRHandle; int SuperTrendATRHandle; int lastProcessedBar = -1; int previousTrend = 0; //--- Adaptive SuperTrend Internal Buffers double Trend[], TrendDirection[]; double st_up[], st_dn[], st_trend_calc[]; double buf_MPO_Raw[], buf_MPO_Smooth[], buf_RSI[], buf_ATR_ST[]; double LastPivLowPriceBuffer[], LastPivLowOscBuffer[]; double LastPivHighPriceBuffer[], LastPivHighOscBuffer[]; double LastTypeBuffer[], BarsSinceBuffer[]; double st_open[], st_high[], st_low[], st_close[]; int rsiHandleDiv; double SmoothAlpha; //--- Position Tracking struct PositionData { ulong ticket; double openPrice, initialRisk; double originalSL; double tp; double trailingSL; bool tpHit; }; PositionData positions[];
Структура PositionData используется для отслеживания основных характеристик каждой открытой позиции. Это необходимо для управления трейлинг-стопом и удаления данных о закрытых позициях из массива отслеживания. Внутренние буферы (Trend, TrendDirection, st_up и т.д.) соответствуют буферам, используемым в индикаторе. В них хранятся значения линии SuperTrend, направление тренда, значения осциллятора, а также данные состояния для обнаружения дивергенций и работы механизма сжатия.
OnInit() — инициализация
Функция OnInit() вызывается при подключении советника к графику или при запуске терминала. В ней задается магическое число, создаются хэндлы индикаторов и инициализируются постоянные значения.
//+------------------------------------------------------------------+ //| Expert Initialization Function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(solomagic); //--- Risk based ATR ATRHandle = iATR(_Symbol, _Period, ATRperiod); if(ATRHandle == INVALID_HANDLE) return INIT_FAILED; ArraySetAsSeries(ATRBuffer, true); //--- SuperTrend ATR (separate because they can use different periods) SuperTrendATRHandle = iATR(_Symbol, SuperTrendTimeframe, SuperTrendPeriod); if(SuperTrendATRHandle == INVALID_HANDLE) return INIT_FAILED; if(InpSource == DIV_SRC_RSI) { rsiHandleDiv = iRSI(_Symbol, SuperTrendTimeframe, DivRsiPeriod, PRICE_CLOSE); if(rsiHandleDiv == INVALID_HANDLE) return INIT_FAILED; } SmoothAlpha = 2.0 / (MPO_smooth + 1.0); //--- Validate the ConfirmationBars input if(ConfirmationBars > 3) { Print("Too many confirmation bars"); return INIT_PARAMETERS_INCORRECT; } Print("Adaptive SuperTrend EA initialized."); return INIT_SUCCEEDED; }
Мы создаем два отдельных хэндла индикатора ATR: один — для расчета стоп-лосса на основе риска (с использованием таймфрейма графика и входного параметра ATRperiod), а другой — для расчета значений SuperTrend (с использованием SuperTrendTimeframe и SuperTrendPeriod). Это позволяет пользователю, например, устанавливать стоп-лоссы на основе ATR с периодом 14 на текущем таймфрейме и одновременно использовать ATR с периодом 10 на старшем таймфрейме для расчета полос SuperTrend. Если в качестве осциллятора-источника выбран RSI, мы создаем хэндл индикатора RSI с указанным периодом на таймфрейме SuperTrend. Если выбран MPO4, внешний хэндл не требуется, поскольку расчет выполняется внутри советника. Также проверяется входной параметр ConfirmationBars. В идеале следует проверять все параметры. Здесь мы проверяем только параметр ConfirmationBars, поскольку в стратегии предусмотрен ограниченный диапазон количества баров подтверждения.
Значение SmoothAlpha вычисляется для экспоненциального сглаживания осциллятора. Оно определяется по стандартной формуле сглаживания EMA: alpha = 2 / (period + 1). Каждый хэндл проверяется на корректность. Если какой-либо хэндл имеет значение INVALID_HANDLE, советник возвращает INIT_FAILED и не запускается без необходимых источников данных.
OnDeinit() — освобождение ресурсов
Когда советник удаляется с графика или терминал завершает работу, функция OnDeinit() освобождает хэндлы индикаторов. Это рекомендуемая практика, позволяющая избежать утечек памяти и обеспечить корректное освобождение ресурсов.
//+------------------------------------------------------------------+ //| Expert Deinitialization Function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(ATRHandle != INVALID_HANDLE) IndicatorRelease(ATRHandle); if(SuperTrendATRHandle != INVALID_HANDLE) IndicatorRelease(SuperTrendATRHandle); if(rsiHandleDiv != INVALID_HANDLE) IndicatorRelease(rsiHandleDiv); }
Мы проверяем каждый хэндл перед его освобождением. Параметр reason указывает причину деинициализации советника, но для освобождения ресурсов он нам не нужен.
Теперь в советнике есть набор настраиваемых входных параметров, глобальные переменные для хранения данных, а также корректные процедуры инициализации и освобождения ресурсов. На этой основе в следующих разделах мы добавим модуль расчета, обнаружение сигналов и совершение сделок.
Модуль расчета Adaptive SuperTrend
Модуль расчета составляет основу советника. В нем рассчитываются значения Adaptive SuperTrend, обнаруживаются дивергенции и применяется механизм сжатия. Логика такая же, как у пользовательского индикатора из предыдущей статьи, с небольшими изменениями для работы внутри советника. Подробное описание логики обнаружения дивергенций, управления состоянием и механизма сжатия см. в разделе 4 предыдущей статьи, посвященной пользовательскому индикатору Adaptive SuperTrend.. Математическая основа остается неизменной. Здесь мы сосредоточимся на реализации в советнике и кратко поясним каждый процесс.
Функция расчета
Все расчеты в советнике выполняются в одной функции — CalculateAdaptiveSuperTrend(). Она вызывается из функции OnTick() при обнаружении нового бара. Расчеты в этой функции выполняются в несколько этапов:
Этап 1 — подготовка к расчетам
//+------------------------------------------------------------------+ //| Adaptive Divergence SuperTrend Internal Math | //+------------------------------------------------------------------+ void CalculateAdaptiveSuperTrend() { int lookback = 300; int bars_total = Bars(_Symbol, SuperTrendTimeframe); if(bars_total < lookback) lookback = bars_total - 2; if(lookback < SuperTrendPeriod + MPO_len + DivPivotLen + 5) return; //--- Array Resizing ArrayResize(Trend, lookback); ArrayResize(TrendDirection, lookback); ArrayResize(st_up, lookback); ArrayResize(st_dn, lookback); ArrayResize(st_trend_calc, lookback); ArrayResize(buf_MPO_Raw, lookback); ArrayResize(buf_MPO_Smooth, lookback); ArrayResize(buf_ATR_ST, lookback); ArrayResize(buf_RSI, lookback); ArrayResize(LastTypeBuffer, lookback); ArrayResize(BarsSinceBuffer, lookback); ArrayResize(LastPivLowPriceBuffer, lookback); ArrayResize(LastPivLowOscBuffer, lookback); ArrayResize(LastPivHighPriceBuffer, lookback); ArrayResize(LastPivHighOscBuffer, lookback); ArrayResize(st_open, lookback); ArrayResize(st_high, lookback); ArrayResize(st_low, lookback); ArrayResize(st_close, lookback); //--- Set as Series ArraySetAsSeries(Trend, true); ArraySetAsSeries(TrendDirection, true); ArraySetAsSeries(st_up, true); ArraySetAsSeries(st_dn, true); ArraySetAsSeries(st_trend_calc, true); ArraySetAsSeries(buf_MPO_Raw, true); ArraySetAsSeries(buf_MPO_Smooth, true); ArraySetAsSeries(buf_ATR_ST, true); ArraySetAsSeries(buf_RSI, true); ArraySetAsSeries(LastTypeBuffer, true); ArraySetAsSeries(BarsSinceBuffer, true); ArraySetAsSeries(LastPivLowPriceBuffer, true); ArraySetAsSeries(LastPivLowOscBuffer, true); ArraySetAsSeries(LastPivHighPriceBuffer, true); ArraySetAsSeries(LastPivHighOscBuffer, true); ArraySetAsSeries(st_open, true); ArraySetAsSeries(st_high, true); ArraySetAsSeries(st_low, true); ArraySetAsSeries(st_close, true); //--- Copy Market Data if(CopyOpen(_Symbol, SuperTrendTimeframe, 0, lookback, st_open) < lookback || CopyHigh(_Symbol, SuperTrendTimeframe, 0, lookback, st_high) < lookback || CopyLow(_Symbol, SuperTrendTimeframe, 0, lookback, st_low) < lookback || CopyClose(_Symbol, SuperTrendTimeframe, 0, lookback, st_close) < lookback) return; double tempATR[]; ArraySetAsSeries(tempATR, true); if(CopyBuffer(SuperTrendATRHandle, 0, 0, lookback, tempATR) < lookback) return; for(int k=0; k<lookback; k++) buf_ATR_ST[k] = tempATR[k]; if(InpSource == DIV_SRC_RSI) { double tempRSI[]; ArraySetAsSeries(tempRSI, true); if(CopyBuffer(rsiHandleDiv, 0, 0, lookback, tempRSI) < lookback) return; for(int k=0; k<lookback; k++) buf_RSI[k] = tempRSI[k]; } int limit = lookback - MPO_len - DivPivotLen - 3;
Пояснение: перед началом вычислений советник подготавливает рабочую среду. Сначала функция определяет, сколько исторических баров следует обработать, чтобы данных было достаточно для расчета ATR и значений осциллятора, а также обнаружения дивергенций. Затем функция выделяет память для всех необходимых внутренних массивов, задает для них индексацию как у временных рядов (где индекс 0 всегда соответствует последнему бару) и копирует в память рыночные данные, значения ATR и, при необходимости, значения RSI. Получение данных отделено от математических расчетов, поэтому на остальных этапах алгоритма используются только локальные массивы. Это сокращает количество повторных вызовов индикаторов, ускоряет вычисления и упрощает сопровождение кода.
Этап 2 — инициализация состояния для исторических баров
//--- Initial Boundaries st_trend_calc[limit+1] = (st_close[limit+1] > st_open[limit+1]) ? 1 : -1; double initMedian = (st_high[limit+1] + st_low[limit+1]) / 2.0; st_up[limit+1] = initMedian + SuperTrendMultiplier * buf_ATR_ST[limit+1]; st_dn[limit+1] = initMedian - SuperTrendMultiplier * buf_ATR_ST[limit+1]; buf_MPO_Smooth[limit+1] = 0; LastTypeBuffer[limit+1] = 0; BarsSinceBuffer[limit+1] = 9999; LastPivLowPriceBuffer[limit+1] = DBL_MAX; LastPivLowOscBuffer[limit+1] = -99999; LastPivHighPriceBuffer[limit+1] = 0; LastPivHighOscBuffer[limit+1] = 99999;
Пояснение: поскольку расчет выполняется в обратном направлении по историческим барам, алгоритм должен сначала определить начальную точку отсчета. При этой инициализации для самого старого из обрабатываемых баров задаются начальное направление тренда, начальные значения полос SuperTrend, нейтральное состояние дивергенции и значения опорных точек по умолчанию. Для каждого последующего бара эти данные переносятся и обновляются. Это позволяет советнику сохранять согласованность состояния при обработке исторических баров, не рассчитывая заново результаты предыдущих решений.
Этап 3 — перенос состояния и расчет значений осциллятора
for(int i = limit; i >= 0; i--) { //--- Inherit State LastTypeBuffer[i] = LastTypeBuffer[i+1]; BarsSinceBuffer[i] = BarsSinceBuffer[i+1] + 1; LastPivLowPriceBuffer[i] = LastPivLowPriceBuffer[i+1]; LastPivLowOscBuffer[i] = LastPivLowOscBuffer[i+1]; LastPivHighPriceBuffer[i] = LastPivHighPriceBuffer[i+1]; LastPivHighOscBuffer[i] = LastPivHighOscBuffer[i+1]; double finalMultiplier = SuperTrendMultiplier; //--- MPO / RSI Logic if(InpSource == DIV_SRC_MPO) { double rollingSum = 0; double sumBodies = 0; for(int k = 0; k < MPO_len; k++) sumBodies += MathAbs(st_close[i+k] - st_open[i+k]); double avgBody = (sumBodies > 0) ? sumBodies / MPO_len : Point(); for(int k = 0; k < MPO_len; k++) { int idx = i + k; double body = MathAbs(st_close[idx] - st_open[idx]); double dir = (st_close[idx] > st_open[idx]) ? 1.0 : (st_close[idx] < st_open[idx]) ? -1.0 : 0.0; double weight = (avgBody > 0) ? body / avgBody : 1.0; rollingSum += (dir * weight); } buf_MPO_Raw[i] = (rollingSum / (MPO_len * 2.0)) * 100.0; } else { buf_MPO_Raw[i] = buf_RSI[i]; } buf_MPO_Smooth[i] = (buf_MPO_Raw[i] * SmoothAlpha) + (buf_MPO_Smooth[i+1] * (1.0 - SmoothAlpha));
Пояснение: в начале каждой итерации переносится ранее сохраненное состояние дивергенции, после чего рассчитывается значение выбранного осциллятора моментума. Если выбран MPO4, советник вычисляет взвешенные значения давления непосредственно по телам свечей, а затем применяет экспоненциальное сглаживание. Если выбран RSI, вместо этого используются ранее скопированные значения RSI. Благодаря такой абстракции для обоих осцилляторов используется один и тот же модуль обнаружения дивергенций, поэтому все последующие этапы алгоритма не зависят от выбранного осциллятора-источника.
Этап 4 — обнаружение дивергенций
//--- Divergence Pivot Detection int pIdx = i + DivPivotLen; if(pIdx <= limit) { bool isPivLow = true, isPivHigh = true; for(int k = 1; k <= DivPivotLen; k++) { if(buf_MPO_Smooth[pIdx] > buf_MPO_Smooth[pIdx+k] || buf_MPO_Smooth[pIdx] > buf_MPO_Smooth[pIdx-k]) isPivLow = false; if(buf_MPO_Smooth[pIdx] < buf_MPO_Smooth[pIdx+k] || buf_MPO_Smooth[pIdx] < buf_MPO_Smooth[pIdx-k]) isPivHigh = false; } if(isPivLow) { if(st_low[pIdx] < LastPivLowPriceBuffer[pIdx] && buf_MPO_Smooth[pIdx] > LastPivLowOscBuffer[pIdx]) { LastTypeBuffer[i] = 1; // Bull Div BarsSinceBuffer[i] = 0; } LastPivLowPriceBuffer[i] = st_low[pIdx]; LastPivLowOscBuffer[i] = buf_MPO_Smooth[pIdx]; } if(isPivHigh) { if(st_high[pIdx] > LastPivHighPriceBuffer[pIdx] && buf_MPO_Smooth[pIdx] < LastPivHighOscBuffer[pIdx]) { LastTypeBuffer[i] = -1; // Bear Div BarsSinceBuffer[i] = 0; } LastPivHighPriceBuffer[i] = st_high[pIdx]; LastPivHighOscBuffer[i] = buf_MPO_Smooth[pIdx]; } }
Пояснение: после расчета значений осциллятора советник ищет подтвержденные опорные максимумы и минимумы, используя заданную длину окна определения опорных точек. Каждая подтвержденная опорная точка сравнивается с ранее сохраненной опорной точкой, чтобы определить, есть ли бычья или медвежья дивергенция. Алгоритм не просто формирует сигнал, а фиксирует тип дивергенции и количество баров, прошедших с момента ее появления. Значения этих переменных состояния используются для управления механизмом адаптивного сжатия, описанным в предыдущей статье.
Этап 5 — применение механизма адаптивного сжатия
//--- Apply Adaptive Shrink if(EnableAdaptiveShrink) { int prevTrendState = (int)st_trend_calc[i+1]; if((prevTrendState == -1 && LastTypeBuffer[i] == 1 && BarsSinceBuffer[i] < 200) || (prevTrendState == 1 && LastTypeBuffer[i] == -1 && BarsSinceBuffer[i] < 200)) { finalMultiplier = SuperTrendMultiplier * (1.0 - DivSensitivity); } }
Пояснение: здесь реализован адаптивный компонент алгоритма. Если обнаружена действующая дивергенция против текущего тренда и срок ее действия еще не истек, советник временно уменьшает множитель ATR с учетом заданного пользователем коэффициента чувствительности. В противном случае исходный множитель сохраняется. Благодаря такой динамической корректировке полосы SuperTrend приближаются к цене только тогда, когда рыночный моментум начинает ослабевать, и остаются неизменными при устойчивом тренде.
Этап 6 — обновление значений SuperTrend
//--- Basic SuperTrend Math double median = (st_high[i] + st_low[i]) / 2.0; double basicUp = median + finalMultiplier * buf_ATR_ST[i]; double basicDn = median - finalMultiplier * buf_ATR_ST[i]; double prevUp = st_up[i+1]; double prevDn = st_dn[i+1]; double prevClose = st_close[i+1]; if(prevClose < prevUp && basicUp > prevUp) st_up[i] = prevUp; else st_up[i] = basicUp; if(prevClose > prevDn && basicDn < prevDn) st_dn[i] = prevDn; else st_dn[i] = basicDn; int currTrend = (int)st_trend_calc[i+1]; if(currTrend == -1 && st_close[i] > st_up[i]) currTrend = 1; else if(currTrend == 1 && st_close[i] < st_dn[i]) currTrend = -1; st_trend_calc[i] = currTrend; if(EnableAdaptiveShrink && currTrend != (int)st_trend_calc[i+1]) { LastTypeBuffer[i] = 0; BarsSinceBuffer[i] = 9999; }
Пояснение: после определения окончательного множителя ATR советник рассчитывает новые значения полос SuperTrend по стандартным формулам SuperTrend. Текущее состояние тренда обновляется каждый раз, когда цена пересекает соответствующую полосу. При каждом завершенном развороте тренда состояние дивергенции сбрасывается, чтобы устаревшие сигналы не могли повлиять на следующий тренд.
Этап 7 — подготовка итогового результата
//--- Map to Trading logic Array Format (0 = Bull, 1 = Bear) if(st_trend_calc[i] == 1) { Trend[i] = st_dn[i]; TrendDirection[i] = 0.0; } else { Trend[i] = st_up[i]; TrendDirection[i] = 1.0; } } }
Пояснение: на заключительном этапе внутреннее представление тренда преобразуется в формат, используемый остальной частью советника. Для отображения выбирается соответствующая линия SuperTrend, а направление тренда записывается в упрощенный сигнальный буфер в виде кода, который затем используется в торговом модуле.
Различия между реализацией индикатора и советника
Реализация расчетов в советнике отличается от реализации в индикаторе несколькими особенностями:
- Отсутствие буферов отображения: советнику не требуется рисовать стрелки или цветные линии. Для принятия торговых решений ему нужны только массивы Trend и TrendDirection. При этом отпадает необходимость во всех буферах индикатора, которые требовались ранее.
- Источники данных: советник использует собственные глобальные хэндлы (SuperTrendATRHandle, rsiHandleDiv) и копирует данные в локальные массивы. Индикатор использует массивы open[], high[], low[] и close[], которые передаются непосредственно в функцию OnCalculate(). Советник должен копировать эти данные из терминала.
- Расчет периода анализа: советник динамически рассчитывает глубину ретроспективного анализа на основе Bars(_Symbol, SuperTrendTimeframe). Индикатор использует значение параметра rates_total своей функции. Оба способа приводят к одному и тому же результату.
- Возвращаемые значения: функция советника не возвращает значение. Она заполняет глобальные буферы и завершает выполнение. Индикатор возвращает значение rates_total, чтобы сообщить о завершении обработки.
- Отсутствие повторной инициализации буфера при каждом вызове: в начале каждой итерации цикла индикатор присваивает элементам BullArrowBuffer[i] и BearArrowBuffer[i] значение EMPTY_VALUE. Советнику это не нужно, так как в нем нет визуальных элементов.
Эти различия относятся к деталям реализации, а не к логике. Основные математические расчеты, перенос состояния и логика сжатия остаются такими же, как в индикаторе.
Функция расчета CalculateAdaptiveSuperTrend() передает результаты торговой логике через два глобальных массива:
- Trend[] содержит значение линии SuperTrend (нижней полосы при восходящем тренде или верхней полосы при нисходящем тренде);
- TrendDirection[] содержит 0.0 для бычьего тренда и 1.0 для медвежьего тренда.
При обнаружении сигналов в советнике считываются значения TrendDirection, чтобы определить момент смены тренда. Это связующее звено между модулем расчета и торговой логикой.
Функция обработки торговых операций
Обнаружение нового бара в функции OnTick()
Теперь, когда модуль расчета формирует данные о тренде, нам необходимо определять моменты смены тренда и реагировать на эти сигналы. В этом разделе рассматриваются обнаружение нового бара, считывание значений массива TrendDirection, определение пересечений, а также включаемый по желанию механизм подтверждения сигнала по барам для защиты от перерисовки.
Советник обрабатывает сигналы один раз на каждом баре. Это предотвращает повторные входы по одному и тому же сигналу — проблему, часто встречающуюся в советниках, которые проверяют условия на каждом тике. Для отслеживания последнего обработанного бара в этом шаблоне используется статическая переменная.
//+------------------------------------------------------------------+ //| Main Tick Function | //+------------------------------------------------------------------+ void OnTick() { //--- New Bar Detection [everything below this block processes only once per bar] datetime currentBarTime = iTime(_Symbol, SuperTrendTimeframe, 0); static datetime lastSuccessfulBarTime = 0; if(currentBarTime == lastSuccessfulBarTime) return; //--- Execute Adaptive SuperTrend Logic CalculateAdaptiveSuperTrend(); //--- Anti-repainting protection int checkIdx = 1 + (UseRepaintingProtection ? ConfirmationBars : 0); if(ArraySize(TrendDirection) <= checkIdx) return; if(CopyBuffer(ATRHandle, 0, 0, 1, ATRBuffer) < 1) return; double dirValue = TrendDirection[checkIdx]; if(dirValue == EMPTY_VALUE) return; lastSuccessfulBarTime = currentBarTime; }
Пояснение
Значение currentBarTime определяется с помощью iTime() для таймфрейма, заданного параметром SuperTrendTimeframe. Это связано с тем, что значения SuperTrend могут рассчитываться на таймфрейме, отличном от таймфрейма графика. Советник обрабатывает сигналы по этому таймфрейму, а не по таймфрейму графика.
После вызова CalculateAdaptiveSuperTrend() массив TrendDirection содержит состояние тренда для каждого бара. Индекс checkIdx определяет, для какого бара следует считывать данные. При вычислении checkIdx учитывается, включено ли подтверждение по дополнительным барам: если UseRepaintingProtection имеет значение false, то checkIdx = 1 (последний завершенный бар), а если UseRepaintingProtection имеет значение true, то checkIdx = 1 + ConfirmationBars. Например, при ConfirmationBars = 1 советник считывает данные бара, предшествующего последнему завершенному бару. Это означает, что сигнал считается действительным, только если он сохранялся в течение хотя бы одного дополнительного бара.
Проверка dirValue гарантирует, что будет использовано допустимое значение. EMPTY_VALUE означает, что модуль расчета не получил результат для этого бара. Это возможно, если период анализа слишком короткий.
Функция обработки сигналов
Получив допустимое значение направления тренда, советник вызывает ProcessTradeLogic() для формирования сигналов.
//+------------------------------------------------------------------+ //| Core Signal Detection | //+------------------------------------------------------------------+ void ProcessTradeLogic() { int signalBarIndex = 1 + (UseRepaintingProtection ? ConfirmationBars : 0); int previousBarIndex = 2 + (UseRepaintingProtection ? ConfirmationBars : 0); double confirmedDirection = TrendDirection[signalBarIndex]; double previousDirection = TrendDirection[previousBarIndex]; bool superTrendBuy = (previousDirection == 1.0 && confirmedDirection == 0.0); bool superTrendSell = (previousDirection == 0.0 && confirmedDirection == 1.0); if(confirmedDirection == 0.0) previousTrend = 1; else if(confirmedDirection == 1.0) previousTrend = -1; int currentTrend = superTrendBuy ? 1 : (superTrendSell ? -1 : 0); if(superTrendBuy) { CloseExistingTrade(); ExecuteTrade(1); } else if(superTrendSell) { CloseExistingTrade(); ExecuteTrade(2); } }
Пояснение
В функции задаются два индекса баров: signalBarIndex (бар, по которому определяется текущее состояние тренда) и previousBarIndex (бар, по которому определяется предыдущее состояние тренда). Пересечение фиксируется при смене направления тренда: сигнал на покупку возникает, когда previousDirection соответствует медвежьему тренду (1,0), а confirmedDirection — бычьему (0,0); сигнал на продажу возникает, когда previousDirection соответствует бычьему тренду (0,0), а confirmedDirection — медвежьему (1,0). Значение переменной previousTrend обновляется в соответствии с текущим трендом. Этот подход можно использовать даже в комментариях на графике для наглядного обозначения текущего тренда.
При обнаружении сигнала советник сначала закрывает имеющуюся позицию с помощью CloseExistingTrade(), а затем открывает новую позицию в новом направлении с помощью ExecuteTrade(). Обе функции мы создадим далее в статье. Это обеспечивает работу только с одной позицией одновременно — так этот советник работает по умолчанию.
Механизм подтверждения по барам задерживает формирование сигнала. Это помогает избежать ложных сигналов на боковом рынке, где SuperTrend может ненадолго сменить направление, а затем вернуться к прежнему. Если этот механизм включен, советник действует, только когда новое направление тренда сохраняется в течение числа баров, заданного параметром ConfirmationBars. Индексы баров, по которым определяется состояние тренда, увеличиваются на число баров подтверждения, поэтому последние бары фактически не учитываются. Здесь приходится идти на компромисс: советник дольше ждет перед входом в рынок и может упустить начало движения, но при этом отфильтровывает ложные сигналы, которые привели бы к убыткам.
Совершение сделок и управление позициями
Теперь, когда обнаружение сигналов реализовано, нам необходимо добавить логику совершения сделок. В этом разделе рассматриваются динамический расчет размера позиции, установка стоп-лосса, расчет тейк-профита, проверка ордера, а также функции управления позициями для их отслеживания и закрытия.
Функция ExecuteTrade()
Функция ExecuteTrade() рассчитывает размер позиции в лотах, определяет уровни стоп-лосса и тейк-профита и отправляет ордер брокеру. Кроме того, она сохраняет данные о позиции для управления трейлинг-стопом.
//+------------------------------------------------------------------+ //| Order Execution | //+------------------------------------------------------------------+ void ExecuteTrade(int trend) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskAmount = balance * (riskPercent / 100.0); double ATRvalue = ATRBuffer[0]; if(ATRvalue <= 0) return; double price = (trend == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); double stoplossDistance = ATRvalue * ATRmultiplier; double sl = NormalizeDouble(price + ((trend == 1) ? -stoplossDistance : stoplossDistance), digits);
В начале функции рассчитывается сумма риска на основе баланса счета и входного параметра riskPercent. Значение ATRvalue считывается из буфера ATRBuffer, который был заполнен ранее в функции OnTick(). Цена входа определяется направлением торговли: SYMBOL_ASK — для покупок и SYMBOL_BID — для продаж. Расстояние до стоп-лосса равно значению ATRvalue, умноженному на ATRmultiplier. При сделке на покупку стоп-лосс устанавливается ниже цены входа. При сделке на продажу он устанавливается выше цены входа.
Брокеры устанавливают минимально допустимое расстояние до стоп-уровней. Если рассчитанный нами стоп-лосс находится слишком близко к цене входа, ордер будет отклонен. Мы выполняем простую проверку и корректировку:
//--- Minimum stop validation double minStop = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point; if(MathAbs(price - sl) < minStop) { sl = (trend == 1) ? price - minStop * 1.5 : price + minStop * 1.5; sl = NormalizeDouble(sl, digits); stoplossDistance = MathAbs(price - sl); } double tp = NormalizeDouble(price + (trend == 1 ? stoplossDistance * TP_Ratio : -stoplossDistance * TP_Ratio), digits);
Свойство SYMBOL_TRADE_STOPS_LEVEL возвращает минимально допустимое расстояние до стоп-уровней в пунктах. Умножаем его на размер пункта, чтобы получить расстояние в ценовых единицах. Если рассчитанное нами расстояние до стоп-лосса меньше этого минимального значения, мы увеличиваем его до minStop * 1.5. Затем расстояние до тейк-профита рассчитывается умножением скорректированного расстояния до стоп-лосса на TP_Ratio.
Динамический расчет размера позиции
Размер позиции в лотах рассчитывается на основе суммы риска и денежного риска на один лот. Для этого требуются размер тика и стоимость тика инструмента.
//--- Lot size calculation double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); if(tickValue <= 0 || tickSize <= 0) { PrintFormat("Invalid Tick Value or Tick Size. Cannot calculate lot size."); return; } double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); double monetaryRiskPerLot = stoplossDistance * contractSize / point * tickSize; double lotSize = 0; if(monetaryRiskPerLot > 0) { lotSize = NormalizeDouble(riskAmount / monetaryRiskPerLot, 2); } else { Print("Monetary risk per lot is zero. Cannot calculate lot size."); return; }
Размер тика и стоимость тика получаются из свойств символа. Денежный риск на один лот рассчитывается следующим образом:
stoplossDistance * contractSize / point * tickSize;
Так мы получаем сумму в долларах, которой рискуем при торговле одним стандартным лотом. Если разделить сумму риска на это значение, получится размер позиции в лотах. При стандартном шаге размера позиции результат округляется до двух знаков после запятой.
После расчета размера позиции мы проверяем его на соответствие ограничениям брокера по минимальному и максимальному объему, а также по шагу его изменения.
//--- Validate Lot Size against broker limits double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); lotSize = MathMax(minLot, MathMin(lotSize, maxLot)); lotSize = NormalizeDouble(lotSize / lotStep, 0) * lotStep;
Размер позиции ограничивается минимальным и максимальным допустимыми значениями, а затем с помощью целочисленного деления округляется вниз до ближайшего значения, кратного шагу изменения размера позиции. Это гарантирует, что объем ордера будет соответствовать требованиям брокера.
Отправка ордера
После проверки и нормализации всех значений ордер отправляется брокеру с помощью класса CTrade. Мы проверяем возвращаемое значение и записываем результат в лог.
bool success = false; ResetLastError(); if(trend == 1) { success = trade.Buy(lotSize, _Symbol, 0, sl, tp, "SuperTrend Buy 1"); } else { success = trade.Sell(lotSize, _Symbol, 0, sl, tp, "SuperTrend Sell 1"); } if(success) { int index = ArraySize(positions); ArrayResize(positions, index+1); positions[index].openPrice = price; positions[index].initialRisk = MathAbs(price - sl); positions[index].originalSL = sl; positions[index].tp = tp; //--- Fetch Tickets and emergency fallback positions[index].ticket = trade.ResultOrder(); if(positions[index].ticket == 0) { for(int i = PositionsTotal()-1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket) && PositionGetInteger(POSITION_MAGIC) == solomagic && PositionGetString(POSITION_SYMBOL) == _Symbol) { positions[index].ticket = ticket; } } } } }
Если ордер успешно исполнен, мы сохраняем данные о позиции в массиве positions[]. Тикет ордера получается с помощью trade.ResultOrder(). Если возвращается ноль (что может произойти у некоторых брокеров), мы используем запасной способ: перебираем все открытые позиции с нашим магическим числом и символом.
Функция закрытия текущей позиции и очистки данных
Когда появляется новый сигнал в противоположном направлении, советник закрывает текущую позицию, прежде чем открыть новую.
//+------------------------------------------------------------------+ //| Close any current trade before executing another | //+------------------------------------------------------------------+ void CloseExistingTrade() { for(int i = PositionsTotal()-1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket) && PositionGetInteger(POSITION_MAGIC) == solomagic && PositionGetString(POSITION_SYMBOL) == _Symbol) trade.PositionClose(ticket); } ArrayResize(positions, 0); previousTrend = 0; }
Функция перебирает все открытые позиции, определяет те, которые принадлежат нашему советнику (по магическому числу и символу), и закрывает их с помощью trade.PositionClose(). Затем массив positions[] очищается, чтобы не использовать устаревшие данные о позициях. Переменной previousTrend присваивается значение 0.
Отслеживание позиций и очистка данных
В массиве positions[] хранятся данные обо всех открытых позициях для управления трейлинг-стопом. Функция CleanupClosedPositions() удаляет из массива отслеживания данные о закрытых позициях.
//+------------------------------------------------------------------+ //| Prevent tracking closed positions | //+------------------------------------------------------------------+ void CleanupClosedPositions() { for(int i = ArraySize(positions)-1; i >= 0; i--) { if(!PositionSelectByTicket(positions[i].ticket)) { ArrayRemove(positions, i, 1); } } }
Эта функция вызывается в начале выполнения OnTick(). Она перебирает элементы массива positions[] от конца к началу и удаляет записи с тикетами, которые больше не действуют. Обратный проход важен, поскольку функция ArrayRemove() сдвигает оставшиеся элементы, что при прямом проходе привело бы к проблемам с индексами.
В следующем разделе мы реализуем логику трейлинг-стопа. Для защиты прибыли при движении цены в нашу пользу в ней используются данные о позициях из массива positions[].
Трейлинг-стопы — защита прибыли
Теперь, когда совершение сделок реализовано, мы добавим механизм трейлинг-стопа. Трейлинг-стоп — ключевой компонент многих систем следования за трендом. Он позволяет советнику защищать прибыль при движении цены в нашу пользу, оставляя запас для ее колебаний. Логика трейлинга в этом советнике основана на соотношении прибыли и риска (RRR). Стоп-лосс переносится, когда соотношение прибыли и риска достигает заданного значения RRR, а затем пошагово подтягивается по мере дальнейшего движения цены в нашу пользу.
Структура PositionData
Прежде чем реализовать логику трейлинга, нам необходимо разобраться в структуре данных для отслеживания позиций:
//--- Position Tracking struct PositionData { ulong ticket; double openPrice, initialRisk; double originalSL; double tp; double trailingSL; bool tpHit; }; PositionData positions[];Поля структуры данных позиции имеют следующее назначение:
- ticket — тикет позиции;
- openPrice — цена входа;
- initialRisk — расстояние от цены входа до начального стоп-лосса;
- originalSL — начальный уровень стоп-лосса;
- tp — уровень тейк-профита;
- tpHit — флаг, указывающий, сработал ли тейк-профит.
Функция трейлинг-стопа
Логика трейлинг-стопа реализована в функции HandleTrailingStops(), которая вызывается в начале функции OnTick().
//+------------------------------------------------------------------+ //| Trailing Start and Trailing Step Function | //+------------------------------------------------------------------+ void HandleTrailingStops() { if(!UseTrailingStops) return; int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); for(int i = 0; i < ArraySize(positions); i++) { ulong ticket = positions[i].ticket; if(ticket != 0 && PositionSelectByTicket(ticket)) { ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double currentPrice = (posType == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); double openPrice = positions[i].openPrice; double initialRisk = positions[i].initialRisk; double currentSL = PositionGetDouble(POSITION_SL); double origSL = positions[i].originalSL; double tp = positions[i].tp; if(initialRisk <= 0) continue; double currentRRR = MathAbs(currentPrice - openPrice) / initialRisk;
В начале функции определяются тип позиции и текущая цена. Для позиции на покупку мы используем SYMBOL_BID, поскольку стоп-лосс срабатывает по этой котировке. Для позиции на продажу мы используем SYMBOL_ASK. Текущее значение RRR рассчитывается делением абсолютной величины изменения цены на начальное расстояние до стоп-лосса.
Проверка условия трейлинга
Механизм трейлинг-стопа включается, только когда соотношение прибыли и риска достигает минимального значения RRR.
//--- Check if the price has reached the initial trailing point if(currentRRR >= TrailingStartRR) { //--- Calculate NewSL based on your live market logic double newSL; if(posType == POSITION_TYPE_BUY) newSL = NormalizeDouble(currentPrice - (initialRisk * TrailingStepRR), digits); else newSL = NormalizeDouble(currentPrice + (initialRisk * TrailingStepRR), digits); //--- Validate that new SL doesn't cross current price and only moves favorably if((posType == POSITION_TYPE_BUY && newSL > currentSL && newSL > origSL) || (posType == POSITION_TYPE_SELL && newSL < currentSL && newSL < origSL)) { trade.PositionModify(ticket, newSL, tp); } } } } }
Входной параметр TrailingStartRR определяет, когда начинается трейлинг. Например, при TrailingStartRR = 1.0 трейлинг-стоп включается, как только цена пройдет расстояние, равное начальному расстоянию до стоп-лосса. Когда условие выполняется, рассчитывается новый уровень стоп-лосса:
- Для позиции на покупку:
currentPrice - (initialRisk * TrailingStepRR)
- Для позиции на продажу:
currentPrice + (initialRisk * TrailingStepRR)
Входной параметр TrailingStepRR определяет, на каком расстоянии от текущей цены размещается стоп-лосс. При TrailingStepRR = 1.0 расстояние от текущей цены до стоп-лосса в точности равно начальному расстоянию до стоп-лосса. Перед переносом стоп-лосса функция проверяет, что новый уровень выгоднее: для позиции на покупку он должен быть выше и текущего, и начального уровня, а для позиции на продажу — ниже и текущего, и начального уровня. Эта проверка предотвращает перенос стоп-лосса назад, который противоречил бы цели трейлинга. Кроме того, эта проверка гарантирует, что трейлинг-стоп будет только подтягиваться, а не отодвигаться.
Когда вызывать функцию трейлинг-стопа
Функция HandleTrailingStops() вызывается в начале OnTick(), до проверки появления нового бара. Это сделано намеренно.
//+------------------------------------------------------------------+ //| Main Tick Function | //+------------------------------------------------------------------+ void OnTick() { //--- Trade Management [queried on every tick] CleanupClosedPositions(); HandleTrailingStops(); //--- New Bar Detection [everything below this block processes only once per bar] datetime currentBarTime = iTime(_Symbol, SuperTrendTimeframe, 0); static datetime lastSuccessfulBarTime = 0; if(currentBarTime == lastSuccessfulBarTime) return;
Трейлинг-стопы необходимо проверять на каждом тике, поскольку цена меняется и между закрытиями баров. Если бы мы выполняли проверку только при появлении новых баров, стоп-лосс мог бы не переноситься при значительных движениях цены внутри бара, что снизило бы эффективность трейлинга.
Трейлинг-стоп работает независимо от логики обнаружения сигналов. Советник может сопровождать позицию трейлинг-стопом и одновременно отслеживать появление нового сигнала. При обнаружении пересечения функция CloseExistingTrade() закрывает позицию (после чего трейлинг-стопы для нее больше не нужны), а затем функция ExecuteTrade() открывает новую позицию. Такое разделение ответственности упрощает сопровождение кода. Логика трейлинг-стопа использует только массив positions[] и не зависит от слоя обнаружения сигналов. Аналогичным образом логика обработки сигналов не зависит от того, как работают трейлинг-стопы.
Временные и сессионные фильтры
Мы добавим в советник управление торговлей по сессиям, поскольку не все часы работы рынка одинаково подходят для торговли. Во время одних торговых сессий ликвидность и волатильность выше, а другие проходят спокойнее, и в это время могут возникать ложные сигналы. Сессионные фильтры позволяют пользователю торговать только во время выбранных торговых сессий.
Мы определяем четыре основные сессии с помощью структуры. Для каждой сессии задаются время начала, время окончания и название. Время указано по UTC.
//--- Session Times (UTC) struct SessionTime { int startH, startM; int endH, endM; string name; }; const SessionTime Asian = {0, 0, 9, 0, "Asian"}; const SessionTime London = {8, 0, 17, 0, "London"}; const SessionTime NY = {13, 0, 22, 0, "New York"}; const SessionTime Sydney = {22, 0, 7, 0, "Sydney"};
Далее мы создадим функцию, которая определяет, приходится ли заданное время на период работы сессии.
//+------------------------------------------------------------------+ //| Check Session Time | //+------------------------------------------------------------------+ bool CheckSession(int currentMinutes, const SessionTime &session) { int start = session.startH * 60 + session.startM; int end = session.endH * 60 + session.endM; return (start < end) ? (currentMinutes >= start && currentMinutes < end) : (currentMinutes >= start || currentMinutes < end); }
Функция CheckSession() преобразует часы и минуты в количество минут, прошедших с полуночи, чтобы упростить сравнение. Если сессия начинается и заканчивается в пределах одних суток (start < end), проверка проста: текущее время должно находиться между start и end. Если сессия продолжается после полуночи (start > end), используется логическое условие "или": текущее время должно быть либо позже start, либо раньше end.
Наконец, мы добавим функцию сессионного фильтра, которая объединяет проверки сессий и возвращает логическое значение, указывающее, разрешена ли торговля.
//+------------------------------------------------------------------+ //| Trade & Session Switch Function | //+------------------------------------------------------------------+ bool IsTradingAllowed() { if(!EnableSessionFilter) return true; //--- Convert broker server time to UTC using the offset datetime serverTime = TimeCurrent(); datetime utcTime = serverTime - (BrokerGMTOffset * 3600); MqlDateTime ts; TimeToStruct(utcTime, ts); int now = ts.hour * 60 + ts.min; bool inSession = false; if(EnableAsianSession) inSession |= CheckSession(now, Asian); if(EnableLondonSession) inSession |= CheckSession(now, London); if(EnableNewYorkSession) inSession |= CheckSession(now, NY); if(EnableSydneySession) inSession |= CheckSession(now, Sydney); return inSession; }
Функция сначала проверяет значение параметра EnableSessionFilter. Если фильтр отключен, торговля разрешена в любое время. Это позволяет пользователю полностью отказаться от ограничений по сессиям. Если фильтр включен, функция получает серверное время брокера с помощью TimeCurrent(). Это время, передаваемое сервером брокера; оно может отличаться от местного времени пользователя. Входной параметр BrokerGMTOffset задает смещение для перевода серверного времени в UTC. Например, если сервер брокера использует время GMT+3, при значении параметра 3 из серверного времени вычитаются три часа, чтобы получить время по UTC. После вычисления времени по UTC функция извлекает часы и минуты и преобразует их в одно целое число, обозначающее количество минут, прошедших с полуночи. Затем функция проверяет каждую выбранную сессию и возвращает true, если хотя бы одна из них сейчас идет.
Когда вызывать функцию сессионного фильтра
Сессионный фильтр подключается в OnTick() вызовом IsTradingAllowed(). Если торговля запрещена, советник не выполняет торговую логику.
//+------------------------------------------------------------------+ //| Main Tick Function | //+------------------------------------------------------------------+ void OnTick() { //--- Trade Management [queried on every tick] CleanupClosedPositions(); HandleTrailingStops(); //--- New Bar Detection [everything below this block processes only once per bar] datetime currentBarTime = iTime(_Symbol, SuperTrendTimeframe, 0); static datetime lastSuccessfulBarTime = 0; if(currentBarTime == lastSuccessfulBarTime) return; //--- Execute Adaptive SuperTrend Logic CalculateAdaptiveSuperTrend(); //--- Anti-repainting protection int checkIdx = 1 + (UseRepaintingProtection ? ConfirmationBars : 0); if(ArraySize(TrendDirection) <= checkIdx) return; if(CopyBuffer(ATRHandle, 0, 0, 1, ATRBuffer) < 1) return; double dirValue = TrendDirection[checkIdx]; if(dirValue == EMPTY_VALUE) return; lastSuccessfulBarTime = currentBarTime; if(IsTradingAllowed()) ProcessTradeLogic(); }
Проверка торговой сессии выполняется после обнаружения нового бара и проверки данных, но до вызова ProcessTradeLogic(). Это гарантирует, что сделки будут совершаться только во время торговых сессий. Сессионный фильтр работает независимо от логики обработки сигналов. Он не влияет ни на расчет значений индикатора, ни на отображение графика. Он лишь определяет, можно ли совершать сделки в текущий момент.
Примечание: преобразование времени в UTC с помощью параметра BrokerGMTOffset важно, поскольку брокеры используют разные часовые пояса. Пользователь должен знать смещение времени сервера своего брокера относительно UTC. Например, если серверное время брокера опережает UTC на три часа, параметру BrokerGMTOffset следует присвоить значение 3. Некоторые брокеры меняют смещение при переходе на летнее время, поэтому пользователю, возможно, придется корректировать это значение в зависимости от сезона.
Тестер стратегий и анализ результатов
Примечание автора: эту адаптивную стратегию автор пока не проверял всесторонне на исторических данных. На момент написания статьи была проверена только работоспособность торговой логики, чтобы убедиться, что каждый компонент работает так, как задумано. Сама стратегия была разработана на основе глубокого понимания принципов работы индикаторов SuperTrend, поэтому результаты тестирования в Тестере стратегий, представленные в этой статье, оказались для меня почти такими же новыми, как и для вас.
Важно не только оценить советник Adaptive SuperTrend как целостную систему, но и проанализировать вклад каждого вспомогательного модуля. Для этого мы будем поочередно включать основные компоненты советника и сравнивать результаты тестирования в Тестере стратегий с результатами базовой реализации SuperTrend. Это позволяет отдельно оценить влияние каждого дополнения к архитектуре и показать вклад каждого модуля.
Будут оцениваться следующие компоненты: базовая реализация SuperTrend, механизм адаптивного сжатия, бары подтверждения, управление трейлинг-стопом и сессионные фильтры. В каждом тесте используются одинаковые рыночные данные, торговые условия и настройки риска, чтобы любые изменения в результатах можно было отнести именно к оцениваемому модулю.
Тест 1 — базовая реализация SuperTrend
Мы будем использовать значения входных параметров по умолчанию: все вспомогательные модули отключены (false), настройки осциллятора также оставлены по умолчанию. Стратегия тестировалась на инструменте Gold на пятнадцатиминутном таймфрейме с 1 января по 1 июня (шесть месяцев). Вот результаты нашего первого теста:

В итоге результат оказался отрицательным: профит-фактор ниже 1,0 (0,9), 223 сделки, просадка эквити около 11%, почти равное число покупок и продаж и доля прибыльных сделок около 32%. Это стратегия на основе базовой реализации SuperTrend. Вот как выглядит кривая эквити за этот период:

Мы надеемся улучшить вид этой кривой по мере добавления в стратегию других компонентов.
Тест 2 — механизм адаптивного сжатия
Теперь мы включаем механизм адаптивного сжатия и присваиваем коэффициенту сжатия значение 0,5. При обнаружении подходящей дивергенции против текущего тренда расстояние от цены до линии SuperTrend сокращается на 50%, что позволяет стратегии раньше реагировать на ослабление моментума. 
Результаты и график приведены ниже:


За тот же шестимесячный период профит-фактор и фактор восстановления выросли (до 1,14 и 1,26 соответственно), чистый результат стал положительным, количество сделок увеличилось, просадка эквити снизилась примерно до 9%, доля прибыльных сделок немного повысилась, а кривая эквити стала лучше.
Тест 3 — бары подтверждения
Далее мы включим входной параметр защиты от перерисовки, чтобы перед совершением сделки дождаться дополнительных баров подтверждения. Затем можно просто выбрать один дополнительный бар для подтверждения.

Затем мы проверяем, как это сказывается на результате и кривой эквити:


Профит-фактор и фактор восстановления стали еще выше (1,23 и 2,75 соответственно), прибыль выросла более чем в 1,6 раза, количество сделок осталось примерно тем же, просадка эквити значительно снизилась — примерно до 6%, доля прибыльных сделок значительно выросла, а кривая эквити стала более плавной. Эти результаты позволяют предположить, что ожидание еще одного бара улучшает качество сигналов за счет отсеивания преждевременных разворотов при сохранении почти той же частоты торговли.
Тест 4 — управление трейлинг-стопом
Стратегии следования за трендом часто выигрывают от динамической защиты плавающей прибыли. Теперь мы проверим, обеспечивает ли модуль трейлинг-стопа аналогичное преимущество для стратегии Adaptive SuperTrend. Мы включим трейлинг, начнем его, когда прибыль достигнет величины начального риска, и затем будем подтягивать стоп-лосс при каждом дальнейшем приросте прибыли на половину этой величины. 
Результаты и график приведены ниже:


Профит-фактор и фактор восстановления снова выросли (1,3 и 3,76 соответственно). Чистая прибыль немного увеличилась при той же частоте сделок, просадка эквити снизилась до 5%, доля прибыльных сделок стала значительно выше, чем в предыдущем тесте, но средняя прибыль на сделку уменьшилась, а кривая эквити улучшилась. Результаты показывают, что модуль трейлинг-стопа действительно улучшает сопровождение позиций, сохраняя при этом сопоставимую общую прибыльность за период тестирования.
Тест 5 — сессионный фильтр
Сессионный фильтр — это скорее дополнительный функционал для трейдеров, которым некомфортно торговать во время сессий с низкой активностью или на боковом рынке. В качестве примера мы исключим лондонскую сессию, что позволит оценить, как ограничение времени торговли влияет на общие результаты работы системы. Для разных активов или стилей торговли могут лучше подходить разные наборы торговых сессий. Входные параметры, результаты и график приведены ниже:



Профит-фактор и фактор восстановления выросли (1,4 и 4,57 соответственно), общая прибыль осталась примерно на том же уровне, частота торговли снизилась до 80% от предыдущего значения — с 317 до 251 сделки, просадка эквити уменьшилась до 4,2%, доля прибыльных сделок немного выросла, а кривая эквити оставалась стабильной. Исключение лондонской сессии положительно влияет на стратегию и снижает частоту торговли.
В приведенных здесь примерах оценивается только одна конфигурация за шестимесячный период. Трейдеры могут повторить эти тесты на разных инструментах и таймфреймах, используя более длительные периоды исторических данных. На практике можно сначала провести несколько предварительных тестов на исторических данных, чтобы определить перспективные диапазоны параметров, а затем выполнить медленную оптимизацию и получить более устойчивый набор параметров для будущих условий торговли.
Анализ результатов
Последовательные тесты показывают, что каждый вспомогательный модуль улучшает отдельный аспект работы советника Adaptive SuperTrend. Система не ограничивается одной функцией, а объединяет несколько взаимодополняющих компонентов, которые в совокупности повышают ее устойчивость по сравнению с базовой реализацией SuperTrend. В таблице ниже приведены наши наблюдения по результатам тестирования на исторических данных за шесть месяцев:
| Модуль | Основной вклад | Наблюдаемый эффект |
|---|---|---|
| Базовая реализация SuperTrend. | Реализация для сравнения. | Отрицательное матожидание, наибольшая просадка, самая низкая доля прибыльных сделок. |
| Адаптивное сжатие. | Более ранняя реакция на ослабление трендов. | Более высокий профит-фактор, меньшая просадка и больше торговых возможностей. |
| Бары подтверждения. | Подтверждение сигнала. | Более высокая доля прибыльных сделок и более высокий фактор восстановления при сопоставимой частоте сделок. |
| Управление трейлинг-стопом. | Динамическое сопровождение позиций. | Снижение просадки и улучшение восстановления при защите плавающей прибыли. |
| Сессионный фильтр. | Выбор рынка. | Снижение частоты сделок при сохранении прибыльности и повышении стабильности эквити. |
Каждое дополнение устраняет отдельный недостаток базовой стратегии. В результате кривая эквити становится более стабильной, а показатели эффективности с учетом риска улучшаются за тот же период тестирования.
Заключение
В этой статье мы развили концепцию Adaptive SuperTrend, перейдя от индикатора к советнику, способному автоматически обнаруживать сигналы и совершать сделки. Советник включает ряд функций: динамический расчет размера позиции, стоп-лоссы на основе ATR, управление трейлинг-стопом по соотношению прибыли и риска (RRR), включаемое по желанию подтверждение сигнала дополнительными барами и сессионную фильтрацию. В совокупности эти дополнения призваны улучшить качество сигналов, сопровождение позиций и общую устойчивость системы. Статья построена по принципу разделения ответственности: модуль расчета формирует данные о тренде, модуль обнаружения сигналов выявляет пересечения, слой исполнения проверяет и обрабатывает ордера, а модуль управления позициями отслеживает и защищает открытые позиции.
Хотя представленная здесь конфигурация служит практической отправной точкой, трейдерам рекомендуется продолжать тестировать стратегию на различных инструментах, таймфреймах и в разных рыночных условиях. Модульная структура советника также позволяет легко экспериментировать с новыми фильтрами, методами управления риском или параметрами оптимизации, чтобы дополнительно адаптировать стратегию к индивидуальному стилю торговли.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23541
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта
Трейдинг с экономическим календарем MQL5 (часть 12): Хранение данных в SQLite и предотвращение повторных сделок
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования