English
preview
Автоматизация торговых стратегий в MQL5 (Часть 52): Модель tCISD с SSMT и Quarterly Theory

Автоматизация торговых стратегий в MQL5 (Часть 52): Модель tCISD с SSMT и Quarterly Theory

MetaTrader 5 — Трейдинг |
39 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

Трейдеры, знакомые с концепцией Smart Money, часто распознают отдельные сигналы — снятие ликвидности, структуру сессии или признаки разворота, — но затрудняются принимать на их основе решения по четким, воспроизводимым правилам. Проблемы вполне конкретны: отсутствие фиксированной временной привязки циклов; нарушения синхронизации из-за сложностей с учетом летнего времени (DST) и смещения времени сервера брокера; несовпадение баров по времени у коррелирующих символов, создающее ложные дивергенции; отсутствие однозначно определяемой триггерной цены, позволяющей перейти от дивергенции к входу в позицию. В результате получается стратегия, которая по-прежнему опирается скорее на интуицию, чем на набор проверяемых правил.

В предыдущей статье (часть 51) мы автоматизировали модель Bread and Butter Judas Swing, которая торговала на снятиях ликвидности в рамках сессии с целями в зонах премии и дисконта. В этой статье мы представим формализованную программную реализацию модели tCISD для разработчиков MetaQuotes Language 5 (MQL5) и трейдеров, работающих по системным правилам, которым нужна прозрачная для аудита программа. Программа сочетает циклы Quarterly Theory, привязанные к нью-йоркскому времени с автоматическим учетом летнего времени, дивергенцию по методике Sequential Smart Money Technique (SSMT), определяемую путем побарного сравнения с коррелирующим символом, и точный триггерный уровень — цену открытия последней свечи противоположного направления. Эта цена и задает уровень tCISD. Мы реализуем модель в виде программы, управляемой конечным автоматом, который переходит из исходного состояния к ожиданию подтверждения, а затем — ретеста. В программе предусмотрены настраиваемые режимы входа, расчет размера позиции по риску или задание фиксированного лота, обработка стопов и целевых уровней с учетом ограничений брокера, опциональные фильтры вероятности (True Open, премия и дисконт, двухэтапный SSMT), трейлинг-стоп, визуализация и ведение логов. Вы сможете скомпилировать программу, запустить ее в Тестере стратегий и последовательно дорабатывать, обеспечивая воспроизводимость проверок. Мы рассмотрим следующие темы:

  1. Принципы модели tCISD с SSMT и Quarterly Theory
  2. Реализация на MQL5
  3. Тестирование на исторических данных
  4. Заключение


Принципы модели tCISD с SSMT и Quarterly Theory

Модель tCISD основана на идее, что время, а не только цена, определяет вероятный момент разворота рынка. В концепции Quarterly Theory торговый день делится на повторяющиеся циклы, а каждый цикл — на четыре четверти. Временная сетка привязана к фиксированному моменту по нью-йоркскому времени, а не ко времени сервера брокера. В первой четверти, как правило, происходит накопление; во второй формируется начальное движение и задается уровень, который мы принимаем за True Open; в третьей часто происходит разворот или манипуляция; в четвертой — распределение. Используя эту сетку, мы учитываем не только то, что делает цена, но и то, на каком этапе цикла это происходит. Вместо смутного представления о подходящем моменте получаем фиксированные временные окна, по которым можно проверять торговые сигналы.

Эту временную структуру дополняет дивергенция, которая и дает модели преимущество. Для выявления SSMT-дивергенции мы сравниваем торгуемый символ с коррелирующим за одну и ту же четверть. Сигнал возникает, когда оба символа расходятся во мнении о том, какой экстремум был снят. Если торгуемый символ снимает ликвидность за максимумом предыдущей четверти, а коррелирующий символ не делает того же за своим максимумом, один из них дает ложное представление о силе движения. Это расхождение указывает, что взятие максимума было снятием ликвидности, а не истинным пробоем; зеркальная ситуация на минимумах предупреждает о бычьем развороте. Дивергенция указывает на вероятный разворот, но не задает триггерную цену сигнала на вход. Именно эту роль выполняет уровень tCISD — цена открытия последней свечи противоположного направления. Пробой этого уровня обозначает смену характера доставки цены и подтверждает намерение рынка двигаться в новом направлении. Экстремум четверти, за которым была снята ликвидность, становится точкой привязки стоп-лосса. Ниже приведена иллюстрация циклов Quarterly Theory.

TCISD QUARTERLY THEORY MODEL ILLUSTRATION

В реальной торговле дождитесь завершения четверти и проверьте, есть ли между торгуемым и коррелирующим символами расхождение по снятию ликвидности. Только после этого определяйте уровень tCISD для входа. Заранее выберите способ входа: после подтверждающего закрытия бара — раньше, но с меньшей уверенностью в сигнале, или при ретесте — после возврата цены к уровню, по более выгодной цене, но с риском так и не дождаться входа. Чтобы повысить вероятность успешного входа, введите дополнительное условие: уровень должен находиться с нужной стороны от True Open или в соответствующей половине диапазона предыдущего цикла. Разместите стоп-лосс за экстремумом, за которым была снята ликвидность, чтобы к моменту, когда расхождение исчезнет и оба рынка все же одновременно снимут ликвидность за своими экстремумами, ваша позиция уже была закрыта. Ниже показано, что мы будем создавать. Для наглядности мы будем использовать золото (XAUUSD) и серебро (XAGUSD) в качестве основного и коррелирующего символов соответственно.

tCISD MODEL WITH SSMT AND QUARTERLY THEORY ROADMAP


Реализация на MQL5

Начнем реализацию с основы всей программы: подключим торговую библиотеку, объявим перечисления для выбора вариантов настройки, полный набор входных параметров и глобальные переменные, которые сохраняют данные о циклах, дивергенции и сетапе между барами.

//+------------------------------------------------------------------+
//|                                                     tCISD EA.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

//--- Include the standard library for order execution
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum LotSizingMode
  {
   LOTS_FIXED,        // Fixed lot size
   LOTS_RISK_PERCENT  // Risk percent of balance (auto lot)
  };

enum EntryMode
  {
   ENTRY_RETEST,      // Enter on a retest of the tCISD level
   ENTRY_CONFIRM      // Enter on the confirmation close
  };

enum SetupState
  {
   STATE_IDLE,        // No active setup
   STATE_CONFIRM,     // Awaiting the confirmation close through the level
   STATE_RETEST       // Awaiting the retest of the level
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "GENERAL"
input long            MagicNumber = 1107;                        // Magic Number
input LotSizingMode   InpLotSizingMode      = LOTS_RISK_PERCENT; // Lot sizing mode
input double          InpFixedLots          = 0.01;             // Fixed lot
input double          InpRiskPercent        = 0.5;              // Risk per trade (percent of balance)
input string          InpOrderComment       = "tCISD";          // Order comment

input group "CORRELATED SYMBOL"
input string          InpCorrelatedSymbol   = "XAGUSDm";        // Correlated symbol for the SSMT

input group "QUARTERLY THEORY CYCLE"
input int             InpCycleMinutes       = 90;               // Cycle length in minutes (quarter = cycle / 4)
input int             InpBrokerGmtOffset    = 0;                // Broker GMT offset in hours (New York DST automatic)

input group "STOP LOSS"
input int             InpStopBufferPoints   = 300;               // Buffer beyond the SSMT extreme (points)
input int             InpMinStopPoints      = 3000;             // Skip the trade if the stop is closer than this (points)

input group "TAKE PROFIT"
input double          InpRewardRiskRatio    = 2.0;              // Reward to risk ratio

input group "ENTRY"
input ENUM_TIMEFRAMES InpEntryTimeframe     = PERIOD_CURRENT;   // Entry timeframe
input EntryMode       InpEntryMode          = ENTRY_CONFIRM;     // Enter on retest or on confirmation
input int             InpMaxConfirmBars     = 24;               // Bars to wait for the close-through
input int             InpMaxRetestBars      = 24;               // Bars to wait for the retest
input int             InpMaxScanBars        = 100;              // Bars to scan back for the opposing candle

input group "PROBABILITY FILTERS"
input bool            InpUse2StageSSMT      = false;            // Require the SSMT on the cycle above too
input bool            InpUseTrueOpen        = false;             // Bearish above / bullish below the Q2 True Open
input bool            InpUsePremiumDiscount = false;             // tCISD level in the correct half of the previous cycle

input group "TRAILING STOP"
input bool            InpUseTrailingStop    = true;            // Use trailing stop
input int             InpMinProfitPoints    = 1000;             // Minimum profit to activate trailing (points)
input int             InpTrailPoints        = 300;              // Trailing distance (points)

input group "LOGGING"
input bool            InpShowLogs           = true;             // Print messages to the Journal
input string          InpLogPrefix          = "tCISD> ";        // Log prefix

input group "VISUALS"
input bool            InpDrawVisuals        = true;             // Draw structure on the chart
input bool            InpDrawQuarters       = true;             // Draw quarter dividers and labels
input int             InpMarkerSize         = 10;               // Marker size (Wingdings 3)
input color           InpBullColor          = clrDodgerBlue;    // Bullish color
input color           InpBearColor          = clrRed;           // Bearish color
input color           InpQuarterColor       = clrSlateGray;     // Quarter divider and label color
input color           InpSsmtColor          = clrMagenta;       // SSMT marker color
input color           InpTcisdColor         = clrDarkViolet;    // tCISD level color

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   trade;                        // Trade execution object
int      symbolDigits;                 // Cached symbol digits
double   symbolPoint;                  // Cached symbol point size
string   correlatedSymbol;             // Resolved correlated symbol name
bool     correlatedReady = false;      // True once the correlated symbol is selected
int      quarterSeconds  = 1350;       // Quarter length in seconds (cycle / 4)
datetime lastBarTime     = 0;          // Last processed entry-TF bar time

//--- Cached New York DST offset (recomputed once per day)
int      newYorkOffset    = -5;        // Current NY GMT offset in hours
int      newYorkOffsetDay = -1;        // Day-of-year the offset was resolved

//--- Quarter range tracking (current and previous quarter)
datetime quarterStart         = 0;     // Server start time of the current quarter
int      quarterOfCycle       = 0;     // Index of the quarter within its cycle (0-3)
bool     havePreviousQuarter  = false; // True once a previous quarter is stored
double   primaryQuarterHigh;           // Running primary quarter high
double   primaryQuarterLow;            // Running primary quarter low
double   correlatedQuarterHigh;        // Running correlated quarter high
double   correlatedQuarterLow;         // Running correlated quarter low
double   primaryPrevQuarterHigh;       // Primary previous-quarter high
double   primaryPrevQuarterLow;        // Primary previous-quarter low
double   correlatedPrevQuarterHigh;    // Correlated previous-quarter high
double   correlatedPrevQuarterLow;     // Correlated previous-quarter low

//--- Cycle range tracking (current and previous cycle)
datetime cycleStart           = 0;     // Server start time of the current cycle
bool     havePreviousCycle    = false; // True once a previous cycle is stored
double   primaryCycleHigh;             // Running primary cycle high
double   primaryCycleLow;              // Running primary cycle low
double   correlatedCycleHigh;          // Running correlated cycle high
double   correlatedCycleLow;           // Running correlated cycle low
double   primaryPrevCycleHigh;         // Primary previous-cycle high
double   primaryPrevCycleLow;          // Primary previous-cycle low
double   correlatedPrevCycleHigh;      // Correlated previous-cycle high
double   correlatedPrevCycleLow;       // Correlated previous-cycle low
double   trueOpen             = 0.0;   // Q2 open used as the cycle True Open

//--- Setup state machine
SetupState setupState     = STATE_IDLE; // Current setup stage
int        setupDirection = 0;          // Armed direction: +1 buy, -1 sell
datetime   ssmtTime       = 0;          // Bar time of the detected SSMT
double     ssmtExtreme    = 0.0;        // Swept extreme used as the stop anchor
double     tcisdLevel     = 0.0;        // tCISD level whose break confirms entry
int        barsInState    = 0;          // Bars elapsed in the current stage

Сначала подключаем стандартную библиотеку Trade.mqh, чтобы в дальнейшем отправлять ордера и управлять ими через торговый объект. Затем объявляем три перечисления, которые позволяют выбирать настройки из понятных раскрывающихся списков. Перечисление LotSizingMode задает выбор между фиксированным лотом и автоматическим расчетом размера лота по риску; перечисление EntryMode — между входом при ретесте уровня tCISD и входом после подтверждающего закрытия бара за этим уровнем; перечисление SetupState определяет три состояния сетапа: исходное состояние, ожидание подтверждающего закрытия и ожидание ретеста. Последнее перечисление лежит в основе конечного автомата, который управляет всей последовательностью входа.

Далее объявляем входные параметры и объединяем их в группы, чтобы в окне настроек было удобно ориентироваться. В общей группе находятся магическое число, режим расчета размера позиции, риск, значения лота и комментарий к ордеру. В отдельной группе задается коррелирующий символ для выявления дивергенции. В группе Quarterly Theory задается длительность цикла, на основе которой мы определяем четверть и смещение для учета летнего времени в Нью-Йорке. В остальных группах задаются отступ для стоп-лосса и минимальное расстояние до него, соотношение вознаграждения и риска, правила входа, опциональные фильтры вероятности, трейлинг, ведение логов и визуализация.

Наконец, объявляем глобальные переменные, в которых программа сохраняет свое состояние между тиками. Торговый объект отвечает за совершение сделок, а сохраненные в кэше количество знаков после запятой в цене символа и размер пункта позволяют избежать повторных запросов этих свойств. Имя коррелирующего символа указывает источник данных для выявления дивергенции, а флаг готовности показывает, доступен ли этот источник. Мы храним длительность четверти в секундах, время бара для защиты от повторной обработки и кэшированное смещение нью-йоркского времени, определяемое один раз в день. Два больших блока параллельно отслеживают структуру: один — для четверти, другой — для цикла. В этих блоках хранятся текущие максимумы и минимумы торгуемого и коррелирующего символов, сохраненные значения предыдущей четверти и предыдущего цикла для сравнения, а также уровень True Open, зафиксированный во второй четверти. Последний блок — сам конечный автомат, управляющий сетапом. В нем хранятся текущее состояние, направление активированного сетапа, время и экстремум обнаруженной дивергенции, уровень tCISD, пробой которого подтверждает вход, и счетчик баров, прошедших в текущем состоянии. Затем определим вспомогательные функции, которые будут использоваться во всей программе. Начнем со вспомогательных функций для временной сетки, привязанной к нью-йоркскому времени.

Привязка времени к сетке четвертей по нью-йоркскому времени

Концепция Quarterly Theory работает только при правильном отнесении каждого бара к четверти и циклу. Поэтому, прежде чем реализовывать логику обнаружения сигналов, создадим вспомогательные функции, которые определяют нью-йоркское время с учетом летнего времени и положение бара во временной сетке.

//+------------------------------------------------------------------+
//| Detect the open of a new entry-TF bar                            |
//+------------------------------------------------------------------+
bool IsNewBar()
  {
//--- Read the current entry-TF bar time
   datetime time = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Report a new bar and store its time when it changes
   if(time != lastBarTime) { lastBarTime = time; return true; }
//--- Report no new bar
   return false;
  }

//+------------------------------------------------------------------+
//| Compute UTC time of the Nth Sunday of a month at a given hour    |
//+------------------------------------------------------------------+
datetime NthSundayUtc(int year, int month, int nth, int hourUtc)
  {
//--- Build the first day of the month
   MqlDateTime start;
   start.year = year; start.mon = month; start.day = 1; start.hour = 0; start.min = 0; start.sec = 0;
   datetime firstOfMonth = StructToTime(start);
//--- Resolve the weekday of that first day
   TimeToStruct(firstOfMonth, start);
//--- Find the day-of-month of the first Sunday (0 = Sunday)
   int firstSunday = 1 + ((7 - start.day_of_week) % 7);
//--- Step forward to the requested Nth Sunday
   int day = firstSunday + (nth - 1) * 7;
//--- Build the final timestamp at the requested UTC hour
   MqlDateTime result;
   result.year = year; result.mon = month; result.day = day; result.hour = hourUtc; result.min = 0; result.sec = 0;
   return StructToTime(result);
  }

//+------------------------------------------------------------------+
//| Resolve the New York GMT offset for a given UTC moment           |
//+------------------------------------------------------------------+
int NewYorkGmtOffset(datetime utc)
  {
//--- Break the UTC moment into calendar fields
   MqlDateTime parts; TimeToStruct(utc, parts);
//--- Bound US DST: 2nd Sunday of March to 1st Sunday of November
   datetime daylightStart = NthSundayUtc(parts.year, 3, 2, 7);
   datetime daylightEnd   = NthSundayUtc(parts.year, 11, 1, 6);
//--- Return EDT inside the DST window
   if(utc >= daylightStart && utc < daylightEnd) return -4;
//--- Return EST outside the DST window
   return -5;
  }

//+------------------------------------------------------------------+
//| Resolve the New York offset once per day and cache it            |
//+------------------------------------------------------------------+
int NewYorkOffsetCached(datetime utc)
  {
//--- Break UTC into fields to detect a day change
   MqlDateTime parts; TimeToStruct(utc, parts);
//--- Recompute the offset only when the day changes
   if(parts.day_of_year != newYorkOffsetDay)
     {
      //--- Cache the day and its resolved offset
      newYorkOffsetDay = parts.day_of_year;
      newYorkOffset    = NewYorkGmtOffset(utc);
     }
//--- Return the cached offset
   return newYorkOffset;
  }

//+------------------------------------------------------------------+
//| Map a server bar time to its quarter and cycle boundaries        |
//+------------------------------------------------------------------+
void QuarterOf(datetime barServer, datetime &outQuarterStart, int &outQuarterOfCycle, datetime &outCycleStart)
  {
//--- Convert server time to UTC then to New York time
   datetime utc     = barServer - InpBrokerGmtOffset * 3600;
   int      offset  = NewYorkOffsetCached(utc);
   datetime newYork = utc + offset * 3600;
//--- Break New York time into fields
   MqlDateTime parts; TimeToStruct(newYork, parts);
//--- Seconds elapsed since New York midnight
   int secondsOfDay      = parts.hour * 3600 + parts.min * 60 + parts.sec;
//--- Seconds since the 18:00 NY daily anchor
   int secondsSinceAnchor = ((secondsOfDay - 18 * 3600) % 86400 + 86400) % 86400;
//--- Index of the current quarter from the anchor
   int quarterIndex      = secondsSinceAnchor / quarterSeconds;
//--- Position of the quarter within its 4-quarter cycle
   outQuarterOfCycle     = quarterIndex % 4;
//--- Full cycle length in seconds
   int cycleLength       = quarterSeconds * 4;
//--- Seconds into the current quarter and cycle
   int intoQuarter       = secondsSinceAnchor - quarterIndex * quarterSeconds;
   int intoCycle         = secondsSinceAnchor % cycleLength;
//--- Quarter and cycle start in New York time
   datetime quarterStartNewYork = newYork - intoQuarter;
   datetime cycleStartNewYork   = newYork - intoCycle;
//--- Convert the boundaries back to server time
   outQuarterStart = quarterStartNewYork - offset * 3600 + InpBrokerGmtOffset * 3600;
   outCycleStart   = cycleStartNewYork   - offset * 3600 + InpBrokerGmtOffset * 3600;
  }

Начнем с функции IsNewBar, которая с помощью функции iTime считывает время текущего бара на таймфрейме входа и сигнализирует о новом баре только при изменении этого времени. Благодаря этому ресурсоемкая обработка выполняется один раз на бар, а не на каждом тике. Далее нужно корректно определить нью-йоркское время в два шага: сетка четвертей привязана к нему, а из-за перехода на летнее время и обратно часы переводят дважды в год. Функция NthSundayUtc вычисляет точный момент наступления заданного часа в N-е воскресенье месяца. Это необходимо, поскольку переход на летнее время и обратно в США происходит по воскресеньям. Функция NewYorkGmtOffset использует ее для определения границ периода летнего времени — со второго воскресенья марта до первого воскресенья ноября — и возвращает смещение -4 часа в этот период и -5 часов вне его. Чтобы снизить вычислительную нагрузку, функция NewYorkOffsetCached определяет это смещение только при изменении номера дня в году, а не при каждом вызове.

Основная функция в этой группе — QuarterOf: по времени бара на сервере она определяет, к какой четверти и циклу он относится. Сначала она переводит серверное время в UTC с учетом смещения времени сервера брокера, а затем — в нью-йоркское время с помощью кэшированного смещения. Затем она вычисляет количество секунд, прошедших с полуночи по нью-йоркскому времени, и, что особенно важно, пересчитывает их относительно точки привязки 18:00 по Нью-Йорку. В концепции Quarterly Theory с этого момента начинается последовательность циклов торгового дня. Разделив количество секунд, отсчитанных от точки привязки, на длительность четверти, получаем сквозной индекс четверти. Остаток от деления этого индекса на 4 определяет положение четверти в цикле — от первой до четвертой. Вычитая из текущего времени длительность, прошедшую с начала текущей четверти и цикла соответственно, получаем время их начала по Нью-Йорку. Затем функция переводит полученные значения обратно в серверное время, чтобы остальные части программы могли напрямую сравнивать их со временем баров. Такая привязка позволяет на последующих этапах отслеживать не только динамику цены, но и то, в какой именно четверти и цикле происходило движение. Чтобы считывать цены коррелирующего символа, мы определяем следующую функцию.

Чтение цен коррелирующего символа

Дивергенция определяется путем сравнения со вторым рынком, поэтому нам нужен надежный способ сопоставить по времени цены коррелирующего символа с баром торгуемого символа. Определим функцию CorrelatedHighLow, которая получает максимум и минимум этого символа для бара с заданным временем.

//+------------------------------------------------------------------+
//| Read the correlated symbol high and low at a bar time            |
//+------------------------------------------------------------------+
bool CorrelatedHighLow(datetime barTime, double &high, double &low)
  {
//--- Fail when the correlated symbol is unavailable
   if(!correlatedReady) return false;
//--- Map the bar time to a shift on the correlated series
   int shift = iBarShift(correlatedSymbol, InpEntryTimeframe, barTime, false);
   if(shift < 0) return false;
//--- Read the high and low at that shift
   high = iHigh(correlatedSymbol, InpEntryTimeframe, shift);
   low  = iLow(correlatedSymbol, InpEntryTimeframe, shift);
//--- Report success only on valid prices
   return (high > 0 && low > 0);
  }

Здесь мы определяем функцию CorrelatedHighLow, которая считывает максимум и минимум коррелирующего символа для бара, соответствующего по времени одному из баров торгуемого символа. Сначала выходим из функции, если коррелирующий символ не был подготовлен к работе, чтобы не выполнять дальнейшие действия при отсутствии данных. Затем с помощью функции iBarShift по времени бара торгуемого символа определяем индекс соответствующего бара во временном ряду другого символа и считываем его максимум и минимум. Это необходимо, поскольку бары двух символов не всегда совпадают по времени. Оба значения передаем через выходные параметры по ссылке, а успешное выполнение подтверждаем только при допустимых значениях цен. На эту проверку опирается логика выявления дивергенции, прежде чем сравнивать два рынка. Важно правильно сопоставить бары по времени: сравнение несоответствующих баров разных символов привело бы к ложным дивергенциям, которых на самом деле нет. Далее определим вспомогательные функции для расчета торгового объема.

Расчет размера позиции по риску

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

//+------------------------------------------------------------------+
//| Convert risk percent and stop distance into a lot size           |
//+------------------------------------------------------------------+
double LotsByRisk(double entry, double stop)
  {
//--- Derive the money to risk from the account balance
   double riskMoney  = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0;
//--- Measure the stop distance in points
   double stopPoints = MathAbs(entry - stop) / symbolPoint;
//--- Abort on a zero stop distance
   if(stopPoints <= 0) return 0;
//--- Read the tick value and tick size for the symbol
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
//--- Abort on invalid tick metrics
   if(tickValue <= 0 || tickSize <= 0) return 0;
//--- Convert tick value into money per point
   double valuePerPoint = tickValue / tickSize * symbolPoint;
//--- Abort on an invalid per-point value
   if(valuePerPoint <= 0) return 0;
//--- Size the position so the stop loss equals the risk money
   double lots = riskMoney / (stopPoints * valuePerPoint);
//--- Read the broker volume constraints
   double volumeMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volumeMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep;
//--- Clamp within limits and normalize to two decimals
   return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2);
  }

//+------------------------------------------------------------------+
//| Resolve the lot size for a trade by the selected mode            |
//+------------------------------------------------------------------+
double ResolveLots(double entry, double stop)
  {
//--- Pick fixed lots or risk-based lots by the sizing mode
   double lots = (InpLotSizingMode == LOTS_FIXED) ? InpFixedLots : LotsByRisk(entry, stop);
//--- Read the broker volume constraints
   double volumeMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volumeMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep;
//--- Clamp within limits and normalize to two decimals
   return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2);
  }

Сначала определим функцию LotsByRisk, которая рассчитывает размер позиции так, чтобы убыток при срабатывании стоп-лосса равнялся фиксированной доле баланса счета. Получаем баланс счета с помощью функции AccountInfoDouble и рассчитываем на его основе сумму риска. Измеряем расстояние до стоп-лосса в пунктах, а с помощью функции SymbolInfoDouble считываем стоимость и размер тика символа, чтобы определить денежную стоимость движения цены на один пункт. Делим сумму риска на произведение расстояния до стоп-лосса в пунктах и денежной стоимости одного пункта, получая исходное количество лотов. Затем округляем его вниз до значения, кратного шагу объема брокера, ограничиваем минимальным и максимальным объемами и нормализуем до 2 знаков после запятой. Здесь важны проверки на каждом этапе: при нулевом расстоянии до стоп-лосса или недопустимом значении стоимости либо размера тика расчет дал бы некорректный или опасный размер позиции. В таких случаях возвращаем ноль, чтобы вызывающий код мог корректно прекратить дальнейшую обработку.

Затем определим функцию ResolveLots — единую точку вызова расчета размера позиции из остальных частей программы. В зависимости от режима расчета размера позиции выбираем фиксированное количество лотов или рассчитываем его по риску. Затем так же округляем объем по шагу и ограничиваем допустимым диапазоном, чтобы итоговый объем в торговом запросе всегда соответствовал требованиям брокера. Если для каждой сделки рассчитывать размер позиции через эту функцию, все правила расчета будут сосредоточены в одном месте. Их не придется дублировать ни в логике входа после подтверждающего закрытия, ни в логике входа при ретесте. Для визуализации используем следующую логику.

Создание набора инструментов отрисовки

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

//+------------------------------------------------------------------+
//| Draw or update a horizontal trend-line level                     |
//+------------------------------------------------------------------+
void DrawLevel(string name, datetime time1, datetime time2, double price, color clr, ENUM_LINE_STYLE style, int width)
  {
//--- Create the object on first use, otherwise move both anchors
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TREND, 0, time1, price, time2, price);
   else
     {
      //--- Move the left anchor
      ObjectMove(0, name, 0, time1, price);
      //--- Move the right anchor
      ObjectMove(0, name, 1, time2, price);
     }
//--- Apply the line color, style and width
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, style);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
//--- Keep the line as a segment, not a ray
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw or update a chart text label                                |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor)
  {
//--- Create and style the label on first use
   if(ObjectFind(0, name) < 0)
     {
      //--- Create the text object at the anchor point
      ObjectCreate(0, name, OBJ_TEXT, 0, time, price);
      //--- Set the font family and size
      ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
      //--- Make the label non-interactive and hidden from the list
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
     }
//--- Refresh the label text, color and anchor
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
//--- Reposition the label
   ObjectMove(0, name, 0, time, price);
  }

//+------------------------------------------------------------------+
//| Draw a small up or down triangle marker                          |
//+------------------------------------------------------------------+
void DrawMarker(string name, datetime time, double price, bool up, color clr, int anchor)
  {
//--- Create the marker on first use, otherwise reposition it
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TEXT, 0, time, price);
   else
      ObjectMove(0, name, 0, time, price);
//--- Use the Wingdings 3 font for triangle glyphs
   ObjectSetString(0, name, OBJPROP_FONT, "Wingdings 3");
//--- Scale the glyph by the marker size input
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpMarkerSize);
//--- Choose an up or down triangle glyph
   ObjectSetString(0, name, OBJPROP_TEXT, up ? "p" : "q");
//--- Apply the color and anchor
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw or update a dotted vertical line                            |
//+------------------------------------------------------------------+
void DrawVerticalLine(string name, datetime time, color clr)
  {
//--- Create the line on first use, otherwise reposition it
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_VLINE, 0, time, 0);
   else
      ObjectMove(0, name, 0, time, 0);
//--- Apply a dotted style in the given color
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
//--- Send the line to the background
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw the entry, stop and target levels with an entry arrow       |
//+------------------------------------------------------------------+
void DrawTradeLevels(bool isBull, datetime time, double entry, double stop, double takeProfit)
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build a per-entry id from the entry time
   string id = "TC_Ent_" + IntegerToString((int)time);
//--- Span the level lines a fixed number of bars to the right
   datetime endTime = time + (datetime)(PeriodSeconds(InpEntryTimeframe) * 30);
//--- Draw the entry line
   DrawLevel(id + "_e",  time, endTime, entry,      clrDodgerBlue, STYLE_SOLID, 2);
//--- Draw the stop-loss line
   DrawLevel(id + "_sl", time, endTime, stop,       C'220,60,60',  STYLE_DASH,  1);
//--- Draw the take-profit line
   DrawLevel(id + "_tp", time, endTime, takeProfit, C'0,200,80',   STYLE_DASH,  1);
//--- Read the trigger bar high and low for arrow placement
   double barHigh = iHigh(_Symbol, InpEntryTimeframe, 1);
   double barLow  = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Draw the direction arrow at the trigger bar extreme
   DrawMarker(id + "_a", time, isBull ? barLow : barHigh, isBull, isBull ? InpBullColor : InpBearColor,
              isBull ? ANCHOR_UPPER : ANCHOR_LOWER);
  }

Сначала определим функцию DrawLevel для построения или обновления горизонтальной линии между двумя моментами времени на заданном ценовом уровне. При первом вызове создаем ее с помощью функции ObjectCreate как трендовую линию с одинаковой ценой в обеих точках привязки. При последующих вызовах перемещаем обе точки с помощью функции ObjectMove, обновляя существующую линию, а не создавая дубликаты. После размещения задаем цвет, стиль и толщину линии, оставляем ее отрезком, а не лучом, отключаем взаимодействие с ней и скрываем ее из списка объектов. Такой подход — обновлять существующий объект или создавать новый, если его еще нет, — позволяет перерисовывать уровень на каждом баре, не накапливая лишние объекты на графике. Для построения всех остальных линий в программе используется эта же функция.

Затем определим функцию DrawLabel для текстовых меток с названиями уровней. При создании метки задаем ее оформление, а при каждом вызове обновляем текст, цвет и точку привязки. Также определим функцию DrawMarker, которая размещает небольшие треугольные символы из шрифта Wingdings 3. В зависимости от направления выбираем треугольник с вершиной вверх или вниз, а его размер задаем входным параметром размера маркера. Так отмечаем экстремум, за которым была снята ликвидность. Вы можете заменить используемые значки на любые другие по своему выбору. В таблице ниже приведены символы шрифта, которые можно использовать напрямую.

SYMBOL FONTS

Функция DrawVerticalLine завершает набор элементов для отображения структуры: она рисует на заднем плане пунктирную вертикальную линию, отделяющую одну четверть от следующей.

Наконец, определим функцию DrawTradeLevels, которая отмечает на графике сделку после открытия позиции. Проводим линии входа, стоп-лосса и тейк-профита на фиксированное количество баров вправо, рассчитывая их протяженность по времени с учетом таймфрейма с помощью функции PeriodSeconds. Затем размещаем стрелку направления на экстремуме сигнального бара, полученном с помощью функций iHigh и iLow. Вместе эти вспомогательные функции отображают абстрактные элементы модели — четверти, дивергенцию, уровень и сделку — так, чтобы их можно было сразу увидеть на графике. Теперь с помощью этих вспомогательных функций можно реализовать логику поиска уровней tCISD и определения циклов.

Отслеживание диапазонов четвертей и циклов

Теперь перейдем к учету данных, на которых основана вся модель: текущих максимумов и минимумов каждой четверти и цикла для торгуемого и коррелирующего символов. Также реализуем поиск уровня tCISD и сброс сетапа. Здесь определим три функции, основная из которых отслеживает диапазоны.

//+------------------------------------------------------------------+
//| Find the opposing candle open that defines the tCISD level       |
//+------------------------------------------------------------------+
double FindTcisdLevel(bool bearish)
  {
//--- Bound the scan to available history
   int limit = MathMin(InpMaxScanBars, iBars(_Symbol, InpEntryTimeframe) - 1);
//--- Walk back searching for the first opposing candle
   for(int shift = 1; shift <= limit; shift++)
     {
      //--- Read the candle open and close
      double openPrice  = iOpen(_Symbol, InpEntryTimeframe, shift);
      double closePrice = iClose(_Symbol, InpEntryTimeframe, shift);
      //--- Bearish setup: return the open of the last up candle
      if(bearish && closePrice > openPrice) return openPrice;
      //--- Bullish setup: return the open of the last down candle
      if(!bearish && closePrice < openPrice) return openPrice;
     }
//--- Report none found
   return 0;
  }

//+------------------------------------------------------------------+
//| Reset the setup state machine to idle                            |
//+------------------------------------------------------------------+
void ResetSetup(string reason)
  {
//--- Log the reset with its direction and reason when active
   if(setupState != STATE_IDLE)
      Log((setupDirection > 0 ? "Bullish" : "Bearish") + " tCISD setup reset: " + reason + ".");
//--- Clear the state, direction and bar counter
   setupState = STATE_IDLE;
   setupDirection = 0;
   barsInState = 0;
  }

//+------------------------------------------------------------------+
//| Track the quarterly and cyclic high/low ranges on each bar       |
//+------------------------------------------------------------------+
void UpdateCycles()
  {
//--- Read the just-closed primary bar time and prices
   datetime barTime = iTime(_Symbol, InpEntryTimeframe, 1);
   double primaryHigh = iHigh(_Symbol, InpEntryTimeframe, 1);
   double primaryLow  = iLow(_Symbol, InpEntryTimeframe, 1);
   double primaryOpen = iOpen(_Symbol, InpEntryTimeframe, 1);
//--- Read the matching correlated bar high and low
   double correlatedHigh = 0, correlatedLow = 0;
   bool haveCorrelated = CorrelatedHighLow(barTime, correlatedHigh, correlatedLow);
//--- Resolve which quarter and cycle this bar belongs to
   datetime newQuarterStart, newCycleStart; int newQuarterOfCycle;
   QuarterOf(barTime, newQuarterStart, newQuarterOfCycle, newCycleStart);
//--- Handle a change of cycle
   if(newCycleStart != cycleStart)
     {
      //--- Archive the finished cycle range as the previous cycle
      if(cycleStart != 0)
        {
         //--- Save the primary previous-cycle high and low
         primaryPrevCycleHigh = primaryCycleHigh; primaryPrevCycleLow = primaryCycleLow;
         //--- Save the correlated previous-cycle high and low
         correlatedPrevCycleHigh = correlatedCycleHigh; correlatedPrevCycleLow = correlatedCycleLow;
         //--- Mark a previous cycle as available
         havePreviousCycle = true;
        }
      //--- Start a new cycle seeded with this bar
      cycleStart = newCycleStart;
      primaryCycleHigh = primaryHigh; primaryCycleLow = primaryLow;
      correlatedCycleHigh = (haveCorrelated ? correlatedHigh : 0); correlatedCycleLow = (haveCorrelated ? correlatedLow : 0);
      //--- Clear the True Open for the fresh cycle
      trueOpen = 0.0;
     }
   else
     {
      //--- Extend the running cycle range with this bar
      if(primaryHigh > primaryCycleHigh) primaryCycleHigh = primaryHigh;
      if(primaryLow < primaryCycleLow) primaryCycleLow = primaryLow;
      //--- Extend the correlated cycle range when data exists
      if(haveCorrelated)
        {
         //--- Push the correlated cycle high up when exceeded
         if(correlatedCycleHigh == 0 || correlatedHigh > correlatedCycleHigh) correlatedCycleHigh = correlatedHigh;
         //--- Push the correlated cycle low down when exceeded
         if(correlatedCycleLow == 0 || correlatedLow < correlatedCycleLow) correlatedCycleLow = correlatedLow;
        }
     }
//--- Handle a change of quarter
   if(newQuarterStart != quarterStart)
     {
      //--- Archive the finished quarter range as the previous quarter
      if(quarterStart != 0)
        {
         //--- Save the primary previous-quarter high and low
         primaryPrevQuarterHigh = primaryQuarterHigh; primaryPrevQuarterLow = primaryQuarterLow;
         //--- Save the correlated previous-quarter high and low
         correlatedPrevQuarterHigh = correlatedQuarterHigh; correlatedPrevQuarterLow = correlatedQuarterLow;
         //--- Mark a previous quarter as available
         havePreviousQuarter = true;
        }
      //--- Start a new quarter seeded with this bar
      quarterStart = newQuarterStart;
      quarterOfCycle = newQuarterOfCycle;
      primaryQuarterHigh = primaryHigh; primaryQuarterLow = primaryLow;
      correlatedQuarterHigh = (haveCorrelated ? correlatedHigh : 0); correlatedQuarterLow = (haveCorrelated ? correlatedLow : 0);
      //--- Capture the Q2 open as the cycle True Open
      if(newQuarterOfCycle == 1) trueOpen = primaryOpen;
      //--- Draw the quarter divider, label and True Open line
      if(VisualsAllowed() && InpDrawQuarters)
        {
         //--- Draw the quarter divider and its label
         string quarterId = IntegerToString((int)newQuarterStart);
         DrawVerticalLine("TC_QDiv_" + quarterId, newQuarterStart, InpQuarterColor);
         DrawLabel("TC_QLbl_" + quarterId, newQuarterStart, primaryHigh, " Q" + IntegerToString(newQuarterOfCycle + 1), InpQuarterColor, ANCHOR_LEFT_LOWER);
         //--- Draw the True Open line once it is set
         if(trueOpen > 0)
           {
            //--- Draw and label the True Open across the cycle
            string cycleId = IntegerToString((int)cycleStart);
            DrawLevel("TC_TO_" + cycleId, cycleStart, barTime, trueOpen, InpQuarterColor, STYLE_DOT, 1);
            DrawLabel("TC_TOt_" + cycleId, barTime, trueOpen, " True Open", InpQuarterColor, ANCHOR_LEFT);
           }
        }
     }
   else
     {
      //--- Extend the running quarter range with this bar
      if(primaryHigh > primaryQuarterHigh) primaryQuarterHigh = primaryHigh;
      if(primaryLow < primaryQuarterLow) primaryQuarterLow = primaryLow;
      //--- Extend the correlated quarter range when data exists
      if(haveCorrelated)
        {
         //--- Push the correlated quarter high up when exceeded
         if(correlatedQuarterHigh == 0 || correlatedHigh > correlatedQuarterHigh) correlatedQuarterHigh = correlatedHigh;
         //--- Push the correlated quarter low down when exceeded
         if(correlatedQuarterLow == 0 || correlatedLow < correlatedQuarterLow) correlatedQuarterLow = correlatedLow;
        }
     }
  }

Определим функцию FindTcisdLevel для поиска уровня, пробой которого обозначает смену характера доставки цены. Начиная с последнего закрытого бара, просматриваем историю в обратном порядке в пределах заданной глубины поиска. Возвращаем цену открытия первой найденной свечи, направленной против ожидаемого разворота: последней восходящей свечи для медвежьего сетапа или последней нисходящей для бычьего. Эта цена открытия задает уровень, который цена должна пересечь в направлении разворота и за которым должен закрыться бар. Возвращая значение при первом совпадении, мы выбираем ближайшую такую свечу, а не более старую из глубины истории. Также определим функцию ResetSetup, которая возвращает конечный автомат в исходное состояние. При каждой отмене активного сетапа она записывает его направление и причину отмены, чтобы в Журнале всегда было объяснение, почему сетап отменен.

Основная функция — UpdateCycles: она обновляет диапазоны четвертей и циклов на каждом баре. Считываем данные только что закрывшегося бара торгуемого символа и с помощью функции CorrelatedHighLow получаем максимум и минимум соответствующего бара коррелирующего символа. Затем ранее созданная функция QuarterOf определяет, к какой четверти и циклу относится бар. При смене цикла сохраняем максимум и минимум завершенного цикла как значения предыдущего, инициализируем новый цикл данными обрабатываемого бара и сбрасываем True Open. Если цикл не сменился, обновляем его текущий диапазон с учетом нового бара. Параллельно так же обрабатываем четверть: сохраняем данные завершенной четверти перед началом новой. Эта последовательность — сохранение данных, затем сброс — позволяет впоследствии сравнивать завершенную четверть с предыдущей при выявлении дивергенции.

Две детали в обработке четвертей действительно важны. Если начинается вторая четверть цикла, фиксируем цену ее открытия как True Open — опорный уровень, относительно которого впоследствии выполняет проверку опциональный фильтр направленности. Если визуализация включена, по мере формирования каждой четверти рисуем разделитель четвертей, его метку и линию True Open, чтобы временная сетка, на которой основана вся модель, была видна на графике, а не скрыта в коде. Все это время отслеживаем диапазоны коррелирующего символа синхронно с диапазонами торгуемого: проверка дивергенции имеет смысл только при сравнении обоих символов за одну и ту же четверть. Теперь подключим эту логику в обработчиках событий, чтобы определять и отрисовывать циклы и увидеть результат проделанной работы.

Подключение обработчиков событий

Когда основные компоненты готовы, добавим их вызовы в обработчики событий, которые терминал вызывает автоматически. Код инициализации добавим в обработчик событий OnInit, код очистки — в обработчик событий OnDeinit, а начальную часть обработки каждого бара — в обработчик событий OnTick.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Cache the symbol digits and point size
   symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   symbolPoint  = _Point;
//--- Configure the trade object magic number and slippage
   trade.SetExpertMagicNumber(MagicNumber);
   trade.SetDeviationInPoints(20);
//--- Derive the quarter length from the cycle length
   quarterSeconds = MathMax(60, (InpCycleMinutes * 60) / 4);
//--- Select the correlated symbol for the SSMT
   correlatedSymbol = InpCorrelatedSymbol;
   correlatedReady = SymbolSelect(correlatedSymbol, true);
//--- Warn when the correlated symbol is unavailable
   if(!correlatedReady)
      Log("WARNING: correlated symbol '" + correlatedSymbol + "' could not be selected - SSMT disabled until it is available.");
//--- Reset the setup state
   setupState = STATE_IDLE; setupDirection = 0;
//--- Reset the quarter tracking state
   quarterStart = 0; havePreviousQuarter = false;
//--- Reset the cycle tracking state
   cycleStart = 0; havePreviousCycle = false; trueOpen = 0.0;
//--- Force a DST offset recompute on first use
   newYorkOffsetDay = -1;
//--- Seed the bar-time guard
   lastBarTime = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Log a ready banner with the key settings
   Log("tCISD EA ready on " + _Symbol + " vs " + correlatedSymbol +
       " | cycle " + IntegerToString(InpCycleMinutes) + "m | Magic " + IntegerToString(MagicNumber));
//--- Report successful initialization
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete our chart objects on a real removal or chart close
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE)
      ObjectsDeleteAll(0, "TC_");
//--- Clear any chart comment
   Comment("");
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewBar())
     {
      //--- Update the quarter and cycle ranges
      UpdateCycles();
      //--- Flush chart updates when visuals are shown
      if(VisualsAllowed()) ChartRedraw(0);
     }
  }

Начнем с обработчика событий OnInit, в котором подготовим все необходимое для работы программы до первого тика. Сохраняем в кэше количество знаков после запятой в цене символа и размер пункта, чтобы избежать повторных запросов этих свойств. Задаем торговому объекту магическое число и небольшое допустимое отклонение цены, а длительность четверти рассчитываем как четверть длительности цикла. Затем выбираем коррелирующий символ с помощью функции SymbolSelect и сохраняем признак его готовности. Если символ недоступен, выводим предупреждение в Журнал: без его данных выявить дивергенцию невозможно. Возвращаем конечный автомат сетапа и данные отслеживания четвертей и циклов в заданное исходное состояние. Обеспечиваем принудительный пересчет смещения с учетом летнего времени при первом обращении, инициализируем время бара для защиты от повторной обработки, выводим сообщение о готовности и возвращаем INIT_SUCCEEDED, подтверждая готовность программы к работе.

Добавим код очистки в обработчик событий OnDeinit, который терминал вызывает при удалении программы с графика или закрытии графика. Удаляем только графические объекты программы, определяя их по общему префиксу имен, и только при удалении программы с графика или закрытии графика. При обычной перекомпиляции объекты не удаляем, чтобы изменение параметров не приводило к ненужному удалению визуальных элементов. Также очищаем комментарий на графике, чтобы не оставлять на нем текст программы.

Наконец, добавим обработку каждого бара в обработчик событий OnTick, используя функцию IsNewBar для проверки появления нового бара. Благодаря этому логика будет выполняться один раз на бар, а не на каждом тике. На каждом новом баре обновляем диапазоны четвертей и циклов с помощью функции UpdateCycles. Если визуализация включена, затем вызываем функцию ChartRedraw для перерисовки графика, чтобы новые четверти и уровни отображались сразу после их формирования. После компиляции получаем следующий результат.

TCISD LEVELS DRAWING

Теперь, когда работа с уровнями реализована, можно перейти к поиску и отображению новых SSMT-дивергенций и активации сетапов на их основе.

Обнаружение дивергенции и активация сетапа

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

//+------------------------------------------------------------------+
//| Filter by the Q2 True Open bias                                  |
//+------------------------------------------------------------------+
bool PassTrueOpen(int direction, double level)
  {
//--- Pass when the filter is disabled
   if(!InpUseTrueOpen) return true;
//--- Pass when no True Open is set yet
   if(trueOpen <= 0) return true;
//--- Require the level on the correct side of the True Open
   return (direction < 0) ? (level > trueOpen) : (level < trueOpen);
  }

//+------------------------------------------------------------------+
//| Filter by premium/discount half of the previous cycle            |
//+------------------------------------------------------------------+
bool PassPremiumDiscount(int direction, double level)
  {
//--- Pass when the filter is disabled
   if(!InpUsePremiumDiscount) return true;
//--- Pass when no valid previous cycle range exists
   if(!havePreviousCycle || primaryPrevCycleHigh <= primaryPrevCycleLow) return true;
//--- Compute the previous-cycle midpoint
   double midpoint = (primaryPrevCycleHigh + primaryPrevCycleLow) / 2.0;
//--- Require the level in the premium (sell) or discount (buy) half
   return (direction < 0) ? (level > midpoint) : (level < midpoint);
  }

//+------------------------------------------------------------------+
//| Filter by an SSMT on the cycle above the quarter                 |
//+------------------------------------------------------------------+
bool PassTwoStageSSMT(int direction)
  {
//--- Pass when the filter is disabled
   if(!InpUse2StageSSMT) return true;
//--- Fail when the cycle data needed is unavailable
   if(!havePreviousCycle || correlatedCycleHigh == 0 || correlatedPrevCycleHigh == 0) return false;
//--- Bearish: require a divergence on the cycle highs
   if(direction < 0)
      return ((primaryCycleHigh > primaryPrevCycleHigh) != (correlatedCycleHigh > correlatedPrevCycleHigh));
//--- Bullish: require a divergence on the cycle lows
   return ((primaryCycleLow < primaryPrevCycleLow) != (correlatedCycleLow < correlatedPrevCycleLow));
  }

//+------------------------------------------------------------------+
//| Combine all probability filters for a direction and level        |
//+------------------------------------------------------------------+
bool PassFilters(int direction, double level)
  {
//--- Pass only when every enabled filter passes
   return PassTwoStageSSMT(direction) && PassTrueOpen(direction, level) && PassPremiumDiscount(direction, level);
  }

//+------------------------------------------------------------------+
//| Detect an SSMT divergence and arm the setup                      |
//+------------------------------------------------------------------+
void DetectSSMT()
  {
//--- Only look for a new SSMT while idle
   if(setupState != STATE_IDLE) return;
//--- Require a previous quarter and a ready correlated symbol
   if(!havePreviousQuarter || !correlatedReady) return;
//--- Require valid correlated quarter data
   if(correlatedQuarterHigh == 0 || correlatedPrevQuarterHigh == 0) return;
//--- Flag which side each symbol swept versus its previous quarter
   bool primarySweptHigh   = (primaryQuarterHigh > primaryPrevQuarterHigh);
   bool correlatedSweptHigh = (correlatedQuarterHigh > correlatedPrevQuarterHigh);
   bool primarySweptLow    = (primaryQuarterLow < primaryPrevQuarterLow);
   bool correlatedSweptLow  = (correlatedQuarterLow < correlatedPrevQuarterLow);
//--- Anchor markers to the just-closed bar
   datetime barTime = iTime(_Symbol, InpEntryTimeframe, 1);
//--- Bearish SSMT: only one symbol swept its high
   if(primarySweptHigh != correlatedSweptHigh)
     {
      //--- Locate the bearish tCISD level from an up candle
      double level = FindTcisdLevel(true);
      //--- Arm the bearish setup when the level passes the filters
      if(level > 0 && PassFilters(-1, level))
        {
         //--- Latch the armed bearish setup awaiting a close below
         setupDirection = -1; ssmtTime = barTime; ssmtExtreme = primaryQuarterHigh; tcisdLevel = level;
         setupState = STATE_CONFIRM; barsInState = 0;
         //--- Log the bearish SSMT
         Log("Bearish SSMT (highs) | tCISD level " + DoubleToString(level, symbolDigits) + " | waiting for close below");
         //--- Mark and label the swept high
         if(VisualsAllowed())
           {
            //--- Draw the SSMT marker and label at the swept high
            string id = IntegerToString((int)barTime);
            DrawMarker("TC_SSMT_" + id, barTime, ssmtExtreme, false, InpSsmtColor, ANCHOR_LOWER);
            DrawLabel("TC_SSMTt_" + id, barTime, ssmtExtreme, " SSMT", InpSsmtColor, ANCHOR_LOWER);
           }
        }
      //--- Stop after handling the high-side case
      return;
     }
//--- Bullish SSMT: only one symbol swept its low
   if(primarySweptLow != correlatedSweptLow)
     {
      //--- Locate the bullish tCISD level from a down candle
      double level = FindTcisdLevel(false);
      //--- Arm the bullish setup when the level passes the filters
      if(level > 0 && PassFilters(1, level))
        {
         //--- Latch the armed bullish setup awaiting a close above
         setupDirection = 1; ssmtTime = barTime; ssmtExtreme = primaryQuarterLow; tcisdLevel = level;
         setupState = STATE_CONFIRM; barsInState = 0;
         //--- Log the bullish SSMT
         Log("Bullish SSMT (lows) | tCISD level " + DoubleToString(level, symbolDigits) + " | waiting for close above");
         //--- Mark and label the swept low
         if(VisualsAllowed())
           {
            //--- Draw the SSMT marker and label at the swept low
            string id = IntegerToString((int)barTime);
            DrawMarker("TC_SSMT_" + id, barTime, ssmtExtreme, true, InpSsmtColor, ANCHOR_UPPER);
            DrawLabel("TC_SSMTt_" + id, barTime, ssmtExtreme, " SSMT", InpSsmtColor, ANCHOR_UPPER);
           }
        }
     }
  }

Определим три опциональных фильтра, которые повышают вероятность успеха за счет проверки дополнительных условий перед активацией сетапа. Функция PassTrueOpen проверяет, находится ли уровень с нужной стороны от True Open второй четверти: выше него для продажи и ниже для покупки. Так вход против текущего движения допускается только в соответствии с направленностью цикла. В функции PassPremiumDiscount вычисляем середину диапазона предыдущего цикла и проверяем, находится ли уровень в зоне премии для продажи или в зоне дисконта для покупки. Это позволяет продавать дорого и покупать дешево. Функция PassTwoStageSSMT проверяет наличие дивергенции того же направления на один уровень выше — для цикла, а не четверти. Таким образом, расхождение на более высоком таймфрейме подтверждает уже найденную дивергенцию. Затем объединим все три фильтра в функции PassFilters: она пропускает сигнал только при выполнении условий всех включенных фильтров. Каждый отключенный фильтр возвращает true, поэтому по умолчанию модель работает без фильтрации.

Определим функцию DetectSSMT для обнаружения дивергенции и активации сетапа. Она выполняется только тогда, когда конечный автомат находится в исходном состоянии, чтобы не изменять уже активный сетап. Сначала с помощью флагов отмечаем, за каким экстремумом предыдущей четверти каждый символ снял ликвидность. Для этого сравниваем максимум и минимум четверти торгуемого символа со значениями его предыдущей четверти и выполняем такое же сравнение для коррелирующего символа. Дивергенция определяется расхождением значений этих флагов: если только один символ снял ликвидность за своим максимумом, возникает медвежий сигнал, а если только один снял ликвидность за своим минимумом — бычий. Именно проверка по правилу исключающего ИЛИ составляет суть методики: условие выполняется, когда два рынка дают разные сигналы о снятии ликвидности за соответствующим экстремумом.

При медвежьей дивергенции с помощью функции FindTcisdLevel определяем уровень tCISD по последней восходящей свече. Если уровень проходит фильтры, активируем медвежий сетап: сохраняем максимум четверти, за которым была снята ликвидность, как точку привязки стоп-лосса, а уровень tCISD — как триггерный уровень сигнала на вход. Затем переводим конечный автомат в состояние ожидания подтверждающего закрытия бара ниже уровня tCISD. Для бычьего сетапа действия полностью зеркальны: точкой привязки стоп-лосса служит минимум четверти, за которым была снята ликвидность, а подтверждением — закрытие бара выше уровня tCISD. В обоих случаях записываем в лог дивергенцию, на основе которой активирован сетап. Если визуализация включена, отмечаем и подписываем экстремум, за которым была снята ликвидность, чтобы на графике было видно, где возник сетап. На этом этапе сделки еще не совершались: мы лишь выявили расхождение и задали уровень, по которому будет проверяться условие входа — подтверждающее закрытие бара за уровнем либо последующий ретест уровня. При вызове функции в обработчике событий тика получаем следующий результат.

SSMT ARMED SETUP

Мы видим, что сетап успешно активирован. Теперь остается реализовать переходы активированного сетапа к следующим состояниям и открытие позиции.

Открытие позиции и переходы между состояниями сетапа

Теперь реализуем открытие позиции по сетапу, активированному с привязкой к уровню tCISD. Здесь определим две функции: одна рассчитывает размер позиции, задает защитные уровни и отправляет ордер, а другая управляет переходами конечного автомата от обнаружения дивергенции до входа в позицию.

//+------------------------------------------------------------------+
//| Size, build SL and TP, and open the trade                        |
//+------------------------------------------------------------------+
void OpenTrade(bool isBull)
  {
//--- Compute the stop buffer in price
   double buffer = InpStopBufferPoints * symbolPoint;
//--- Enter at the market on the correct side
   double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), symbolDigits)
                         : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), symbolDigits);
//--- Place the stop beyond the swept SSMT extreme
   double stop = isBull ? ssmtExtreme - buffer : ssmtExtreme + buffer;
//--- Reject the trade when the stop is closer than the minimum
   if(MathAbs(entry - stop) / symbolPoint < InpMinStopPoints) { ResetSetup("stop below minimum"); return; }
//--- Read the broker minimum stop distance (stops level or spread)
   long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   spread     = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   double minPoints  = (double)MathMax(stopsLevel, spread);
   if(minPoints < 1) minPoints = 1;
//--- Convert the minimum distance to price
   double minDistance = minPoints * symbolPoint;
//--- Push the stop out to the broker minimum when too tight
   if(isBull) { if(entry - stop < minDistance) stop = entry - minDistance; }
   else       { if(stop - entry < minDistance) stop = entry + minDistance; }
//--- Normalize the stop and measure the risk distance
   stop = NormalizeDouble(stop, symbolDigits);
   double riskDistance = MathAbs(entry - stop);
//--- Abort on an invalid risk distance
   if(riskDistance <= 0) { ResetSetup("invalid risk distance"); return; }
//--- Set the take profit at the reward-to-risk multiple of the stop
   double takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance
                              : entry - InpRewardRiskRatio * riskDistance;
//--- Push the target out to the broker minimum when too tight
   if(isBull) { if(takeProfit - entry < minDistance) takeProfit = entry + minDistance; }
   else       { if(entry - takeProfit < minDistance) takeProfit = entry - minDistance; }
//--- Normalize the target
   takeProfit = NormalizeDouble(takeProfit, symbolDigits);
//--- Resolve the lot size for this trade
   double lots = ResolveLots(entry, stop);
//--- Abort on a lot sizing error
   if(lots <= 0) { ResetSetup("lot calc error"); return; }
//--- Send the market order on the correct side
   bool ok = isBull ? trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment)
                    : trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment);
//--- Annotate and log a successful fill
   if(ok)
     {
      //--- Draw the entry, stop and target levels
      datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
      DrawTradeLevels(isBull, now, entry, stop, takeProfit);
      //--- Log the fill details
      Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, symbolDigits) +
          "  SL=" + DoubleToString(stop, symbolDigits) + "  TP=" + DoubleToString(takeProfit, symbolDigits) +
          "  lots=" + DoubleToString(lots, 2));
     }
   else
      //--- Log the failure reason
      Log("Open failed: " + trade.ResultRetcodeDescription());
//--- Reset the setup after the attempt
   ResetSetup("filled");
  }

//+------------------------------------------------------------------+
//| Advance the setup through confirmation and retest to entry       |
//+------------------------------------------------------------------+
void ProgressSetup()
  {
//--- Do nothing while idle
   if(setupState == STATE_IDLE) return;
//--- Count another bar in the current state
   barsInState++;
//--- Read the just-closed bar close, high and low
   double closePrice = iClose(_Symbol, InpEntryTimeframe, 1);
   double highPrice  = iHigh(_Symbol, InpEntryTimeframe, 1);
   double lowPrice   = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Confirmation stage: wait for the close through the level
   if(setupState == STATE_CONFIRM)
     {
      //--- Reset if both symbols resolved the bearish divergence
      if(setupDirection < 0 && primaryQuarterHigh > primaryPrevQuarterHigh && correlatedQuarterHigh > correlatedPrevQuarterHigh) { ResetSetup("SSMT resolved (both swept highs)"); return; }
      //--- Reset if both symbols resolved the bullish divergence
      if(setupDirection > 0 && primaryQuarterLow < primaryPrevQuarterLow && correlatedQuarterLow < correlatedPrevQuarterLow) { ResetSetup("SSMT resolved (both swept lows)"); return; }
      //--- Reset if the close-through never arrived in time
      if(barsInState > InpMaxConfirmBars) { ResetSetup("no close-through in time"); return; }
      //--- Test for a confirming close through the tCISD level
      bool confirmed = (setupDirection < 0) ? (closePrice < tcisdLevel) : (closePrice > tcisdLevel);
      //--- Handle a confirmed close
      if(confirmed)
        {
         //--- Anchor the confirmed level line to the current bar
         datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
         //--- Draw the confirmed tCISD level and label
         if(VisualsAllowed())
           {
            //--- Draw the tCISD level line and its label
            string id = IntegerToString((int)ssmtTime);
            DrawLevel("TC_Lvl_" + id, ssmtTime, now, tcisdLevel, InpTcisdColor, STYLE_DASHDOT, 1);
            DrawLabel("TC_Lvlt_" + id, now, tcisdLevel, (setupDirection < 0 ? " -tCISD" : " +tCISD"), InpTcisdColor, ANCHOR_LEFT);
           }
         //--- Log the confirmation
         Log((setupDirection < 0 ? "Bearish" : "Bullish") + " tCISD confirmed at " + DoubleToString(tcisdLevel, symbolDigits));
         //--- Enter immediately in confirm mode, else await the retest
         if(InpEntryMode == ENTRY_CONFIRM) { OpenTrade(setupDirection > 0); return; }
         setupState = STATE_RETEST; barsInState = 0;
        }
      //--- Stop after the confirmation stage
      return;
     }
//--- Retest stage: wait for price to return to the level
   if(setupState == STATE_RETEST)
     {
      //--- Reset if the SSMT high is violated before the retest
      if(setupDirection < 0 && highPrice > ssmtExtreme) { ResetSetup("SSMT high violated before retest"); return; }
      //--- Reset if the SSMT low is violated before the retest
      if(setupDirection > 0 && lowPrice < ssmtExtreme) { ResetSetup("SSMT low violated before retest"); return; }
      //--- Reset if the retest never arrived in time
      if(barsInState > InpMaxRetestBars) { ResetSetup("no retest in time"); return; }
      //--- Enter short on a retest up into the level
      if(setupDirection < 0 && highPrice >= tcisdLevel) OpenTrade(false);
      //--- Enter long on a retest down into the level
      else if(setupDirection > 0 && lowPrice <= tcisdLevel) OpenTrade(true);
     }
  }

Здесь определим функцию OpenTrade для исполнения решения, уже принятого конечным автоматом. Входим в рынок в нужном направлении и размещаем стоп-лосс за экстремумом, за которым была снята ликвидность, с заданным отступом. Затем выполняем две проверки этого стоп-лосса. Сначала отказываемся от сделки, если расстояние от цены входа до стоп-лосса меньше заданного минимума: при слишком близком стоп-лоссе вход по сетапу нецелесообразен. Затем с помощью функции SymbolInfoInteger получаем минимальное расстояние, требуемое брокером, исходя из уровня стопов или спреда. Если стоп-лосс или тейк-профит расположен слишком близко к цене, отодвигаем соответствующий уровень до этого расстояния, чтобы ордер всегда соответствовал требованиям брокера. Принимая расстояние от цены входа до стоп-лосса за величину риска, задаем расстояние до тейк-профита как произведение этой величины и заданного соотношения вознаграждения и риска. Затем определяем количество лотов с помощью функции ResolveLots и отправляем ордер методом Buy или Sell торгового объекта. Если ордер исполнен, отображаем на графике уровни сделки и записываем подробности в лог. Независимо от результата попытки затем сбрасываем сетап: для каждого сетапа предусмотрена только одна попытка входа.

Определим функцию ProgressSetup, которая обрабатывает активированный сетап на каждом новом баре. Именно здесь проявляется назначение конечного автомата с двумя рабочими этапами. На этапе подтверждения ожидаем закрытия бара за уровнем tCISD: ниже него для медвежьего сетапа или выше для бычьего. Однако сначала проверяем условия отмены сетапа: исчезновение дивергенции, когда оба символа уже сняли ликвидность за соответствующим экстремумом и исходного расхождения больше нет, либо превышение допустимого числа баров без закрытия за уровнем. После подтверждающего закрытия бара отображаем и подписываем уровень, затем либо сразу входим в рынок в режиме подтверждения, либо переводим сетап в состояние ожидания ретеста. На этапе ретеста ожидаем возврата цены к уровню: открываем короткую позицию, когда цена поднимается к уровню для ретеста, и длинную — когда цена опускается к уровню для ретеста. Сбрасываем сетап, если до ретеста цена выходит за экстремум, за которым была снята ликвидность, либо если ретест не происходит за отведенное число баров. Такое разделение позволяет использовать один и тот же сигнал при двух подходах к торговле: входить раньше, но с меньшей уверенностью в сигнале в режиме подтверждения, или дожидаться возврата цены к уровню в режиме ретеста. При вызове функции мы получаем следующий результат.

PROGRESSIVE SETUP

Мы видим, что сетапы подтверждаются и по ним открываются позиции. Остается добавить трейлинг-стопы для сопровождения позиций при движении цены в нашу пользу. Для этого используем следующую логику.

//+------------------------------------------------------------------+
//| Trail the stop on this EA's open positions                       |
//+------------------------------------------------------------------+
void ManageTrailing()
  {
//--- Do nothing when trailing is disabled
   if(!InpUseTrailingStop) return;
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Read the position side, entry, stop and target
      bool   isBull  = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      double entry   = PositionGetDouble(POSITION_PRICE_OPEN);
      double curStop = PositionGetDouble(POSITION_SL);
      double curTP   = PositionGetDouble(POSITION_TP);
      //--- Read the current bid and ask
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      //--- Measure open profit in points
      double profitPoints = isBull ? (bid - entry) / symbolPoint : (entry - ask) / symbolPoint;
      //--- Trail only past the activation threshold
      if(profitPoints >= InpMinProfitPoints + InpTrailPoints)
        {
         //--- Compute the trailed stop behind price
         double newStop = isBull ? bid - InpTrailPoints * symbolPoint : ask + InpTrailPoints * symbolPoint;
         newStop = NormalizeDouble(newStop, symbolDigits);
         //--- Move the stop only when it improves protection
         bool improves = isBull ? (newStop > curStop) : (curStop == 0 || newStop < curStop);
         if(improves) trade.PositionModify(ticket, newStop, curTP);
        }
     }
  }

Определим функцию ManageTrailing для подтягивания стоп-лосса по позициям программы. Если трейлинг отключен, дальнейшая обработка в функции не выполняется. Перебираем все открытые позиции и обрабатываем только те, у которых совпадают символ и магическое число программы, чтобы не затрагивать позиции других программ. Для каждой позиции программы считываем направление, цену входа и текущий стоп-лосс, затем рассчитываем нереализованную прибыль в пунктах по разнице между ценой входа и текущей ценой Bid или Ask. Только когда прибыль превысит порог активации — сумму минимальной прибыли и дистанции трейлинга, — рассчитываем новый уровень стоп-лосса на заданном расстоянии от текущей цены. Переносим стоп-лосс с помощью метода PositionModify торгового объекта только тогда, когда новый уровень улучшает защиту позиции. Стоп-лосс сдвигается лишь в выгодную для нас сторону, никогда не возвращаясь в обратном направлении. Так он последовательно подтягивается вслед за благоприятным движением цены.

Теперь мы завершим обработчик событий OnTick: добавим вызовы функции трейлинга к уже существующей последовательности обработки каждого бара.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewBar())
     {
      //--- Update the quarter and cycle ranges
      UpdateCycles();
      //--- Look for a new SSMT divergence
      DetectSSMT();
      //--- Advance any armed setup toward entry
      ProgressSetup();
      //--- Flush chart updates when visuals are shown
      if(VisualsAllowed()) ChartRedraw(0);
     }
//--- Trail open positions every tick
   ManageTrailing();
  }

После компиляции получаем следующий результат.

ACTIVE TRAILING STOP

На визуализации видно, что при движении цены в нашу пользу позиции сопровождаются трейлинг-стопом. Остается протестировать программу на исторических данных — этому посвящен следующий раздел.


Тестирование на исторических данных

Компилируем программу и запускаем ее в Тестере стратегий MetaTrader 5 в визуальном режиме. Это позволяет бар за баром наблюдать формирование четвертей и активацию сетапов по дивергенциям. Результат представлен ниже в формате Graphics Interchange Format (GIF).

TCISD BACKTSET

В ходе тестирования программа делила каждую сессию на четверти и циклы, привязанные к нью-йоркскому времени, и отображала разделители и уровень True Open второй четверти по мере их определения. Программа отслеживала максимумы и минимумы четверти для торгуемого и коррелирующего символов. Сетап активировался только при расхождении между ними по снятию ликвидности за соответствующим экстремумом; этот экстремум отмечался меткой дивергенции. Затем для каждого активированного сетапа программа определяла уровень tCISD по последней свече противоположного направления и ожидала условия входа: закрытия последующего бара за этим уровнем в режиме подтверждения или возврата цены к уровню в режиме ретеста. В сделках, которые развивались в нашу пользу, трейлинг-стоп продвигался вслед за ценой, как только прибыль превышала порог его активации.

Ниже приведен график результатов тестирования на исторических данных.

GRAPH

Ниже приведен отчет о тестировании на исторических данных.

REPORT


Заключение

В итоге мы реализовали идею tCISD в виде конкретной программы на языке MQL5, которую можно тестировать и которая решает практические проблемы, обозначенные в начале статьи. В частности, программа привязывает циклы и четверти к нью-йоркскому времени с автоматическим учетом летнего времени и определяет положение серверных баров в этой сетке, предотвращая смещение временной привязки. Она сопоставляет бары торгуемого и коррелирующего символов по времени, чтобы избежать ложных межрыночных дивергенций. Кроме того, программа отслеживает максимумы и минимумы четвертей и циклов для обоих символов, выявляет SSMT-дивергенцию, когда только один символ снимает ликвидность за соответствующим экстремумом, и отмечает этот экстремум.

Затем программа определяет триггерный уровень tCISD по цене открытия последней свечи противоположного направления. Четко заданный конечный автомат управляет переходами из исходного состояния к ожиданию подтверждения, а затем — ретеста, обеспечивая вход в позицию на основе дивергенции. В программе предусмотрены вход после подтверждающего закрытия бара или при ретесте, фиксированное количество лотов или расчет размера позиции по проценту риска, стоп-лосс за экстремумом, за которым была снята ликвидность, тейк-профит по заданному соотношению вознаграждения и риска, соблюдение минимального расстояния до стоп-уровней, установленного брокером, опциональные фильтры вероятности (True Open, премия и дисконт, двухэтапный SSMT), трейлинг-стоп, визуализация на графике и ведение логов.

В результате вы получаете рабочую программу, которую можно скомпилировать, запустить в Тестере стратегий и настроить с помощью входных параметров. Она превращает прежде интуитивный процесс в воспроизводимый пайплайн: обнаружение сигнала, проверка условий, срабатывание сигнала на вход, совершение сделки и сопровождение позиции.

Отказ от ответственности: данный материал представлен в образовательных целях. Торговля сопряжена со значительными финансовыми рисками, а результаты, полученные при тестировании на исторических данных, не гарантируют будущих результатов. Перед запуском этой программы на реальном рынке критически важны тщательное тестирование на исторических данных и строгое управление рисками.


      Перевод с английского произведен MetaQuotes Ltd.
      Оригинальная статья: https://www.mql5.com/en/articles/23549

      Прикрепленные файлы |
      tCISD_EA.mq5 (46.96 KB)
      Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5 Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5
      Ценовые выбросы искажают показания индикаторов, основанных на среднем арифметическом и стандартном отклонении. В статье представлена библиотека робастной статистики на языке MQL5 (RobustStats.mqh), реализующая вычисление медианы, медианное абсолютное отклонение (MAD) с коэффициентом масштабирования 1,4826 и наклона Тейла — Сена, а также представлены три индикатора для непосредственной замены полос Боллинджера, канала линейной регрессии и осциллятора z-оценки. Совместное отображение индикаторов для сравнения и измерение точки разрушения на EURUSD показывают, как робастные инструменты сохраняют форму, когда одиночный скачок цены сдвигает классические.
      Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
      В статье реализован автономный советник Adaptive SuperTrend, который выполняет внутренние расчеты на выбранном таймфрейме без использования внешних буферов и файлов индикаторов. В нем предусмотрены расчет размера позиции в лотах на основе риска, стоп-лоссы на основе ATR, пошаговый трейлинг по соотношению прибыли и риска (RR), дополнительное подтверждение для защиты от перерисовки, которое можно включить, и управление торговлей по сессиям. На практике эту структуру можно использовать повторно для единообразной обработки сигналов при появлении нового бара и проверки ордеров на соответствие требованиям брокера.
      Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
      В статье представлен автономный модуль фильтрации новостей для MetaTrader 5, созданный на основе API экономического календаря платформы. Модуль реализует сопоставление торговых символов с валютами, торговые паузы перед событиями и после них, а также необязательное сокращение объёма позиции в дни с событиями высокой значимости, с резервным вариантом на основе CSV для Тестера стратегий. Демонстрационный советник и панель мониторинга на графике в реальном времени показывают интеграцию и проверку как в реальной торговой среде, так и в условиях бэктеста.
      Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта
      Мы создаем на языке MQL5 программу для торговли на разворотах в рамках торговых сессий по модели Bread and Butter Judas Swing. Программа определяет дневную направленность по данным старшего таймфрейма, задает килл-зоны по нью-йоркскому времени и определяет зоны премии и дисконта каждой сессии по ее текущему диапазону. Обязательное условие — снятие ликвидности, после которого смена рыночной структуры подтверждает вход. Читатели получат готовый подход с четкими, проверяемыми правилами: активировать сетапы только во время активных сессий и открывать позиции в соответствии с дневной направленностью.