Автоматизация торговых стратегий в MQL5 (Часть 52): Модель tCISD с SSMT и Quarterly Theory
Введение
Трейдеры, знакомые с концепцией 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), трейлинг-стоп, визуализация и ведение логов. Вы сможете скомпилировать программу, запустить ее в Тестере стратегий и последовательно дорабатывать, обеспечивая воспроизводимость проверок. Мы рассмотрим следующие темы:
- Принципы модели tCISD с SSMT и Quarterly Theory
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
Принципы модели tCISD с SSMT и Quarterly Theory
Модель tCISD основана на идее, что время, а не только цена, определяет вероятный момент разворота рынка. В концепции Quarterly Theory торговый день делится на повторяющиеся циклы, а каждый цикл — на четыре четверти. Временная сетка привязана к фиксированному моменту по нью-йоркскому времени, а не ко времени сервера брокера. В первой четверти, как правило, происходит накопление; во второй формируется начальное движение и задается уровень, который мы принимаем за True Open; в третьей часто происходит разворот или манипуляция; в четвертой — распределение. Используя эту сетку, мы учитываем не только то, что делает цена, но и то, на каком этапе цикла это происходит. Вместо смутного представления о подходящем моменте получаем фиксированные временные окна, по которым можно проверять торговые сигналы.
Эту временную структуру дополняет дивергенция, которая и дает модели преимущество. Для выявления SSMT-дивергенции мы сравниваем торгуемый символ с коррелирующим за одну и ту же четверть. Сигнал возникает, когда оба символа расходятся во мнении о том, какой экстремум был снят. Если торгуемый символ снимает ликвидность за максимумом предыдущей четверти, а коррелирующий символ не делает того же за своим максимумом, один из них дает ложное представление о силе движения. Это расхождение указывает, что взятие максимума было снятием ликвидности, а не истинным пробоем; зеркальная ситуация на минимумах предупреждает о бычьем развороте. Дивергенция указывает на вероятный разворот, но не задает триггерную цену сигнала на вход. Именно эту роль выполняет уровень tCISD — цена открытия последней свечи противоположного направления. Пробой этого уровня обозначает смену характера доставки цены и подтверждает намерение рынка двигаться в новом направлении. Экстремум четверти, за которым была снята ликвидность, становится точкой привязки стоп-лосса. Ниже приведена иллюстрация циклов Quarterly Theory.

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

Реализация на 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. В зависимости от направления выбираем треугольник с вершиной вверх или вниз, а его размер задаем входным параметром размера маркера. Так отмечаем экстремум, за которым была снята ликвидность. Вы можете заменить используемые значки на любые другие по своему выбору. В таблице ниже приведены символы шрифта, которые можно использовать напрямую.

Функция 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 для перерисовки графика, чтобы новые четверти и уровни отображались сразу после их формирования. После компиляции получаем следующий результат.

Теперь, когда работа с уровнями реализована, можно перейти к поиску и отображению новых 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. В обоих случаях записываем в лог дивергенцию, на основе которой активирован сетап. Если визуализация включена, отмечаем и подписываем экстремум, за которым была снята ликвидность, чтобы на графике было видно, где возник сетап. На этом этапе сделки еще не совершались: мы лишь выявили расхождение и задали уровень, по которому будет проверяться условие входа — подтверждающее закрытие бара за уровнем либо последующий ретест уровня. При вызове функции в обработчике событий тика получаем следующий результат.

Мы видим, что сетап успешно активирован. Теперь остается реализовать переходы активированного сетапа к следующим состояниям и открытие позиции.
Открытие позиции и переходы между состояниями сетапа
Теперь реализуем открытие позиции по сетапу, активированному с привязкой к уровню 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: ниже него для медвежьего сетапа или выше для бычьего. Однако сначала проверяем условия отмены сетапа: исчезновение дивергенции, когда оба символа уже сняли ликвидность за соответствующим экстремумом и исходного расхождения больше нет, либо превышение допустимого числа баров без закрытия за уровнем. После подтверждающего закрытия бара отображаем и подписываем уровень, затем либо сразу входим в рынок в режиме подтверждения, либо переводим сетап в состояние ожидания ретеста. На этапе ретеста ожидаем возврата цены к уровню: открываем короткую позицию, когда цена поднимается к уровню для ретеста, и длинную — когда цена опускается к уровню для ретеста. Сбрасываем сетап, если до ретеста цена выходит за экстремум, за которым была снята ликвидность, либо если ретест не происходит за отведенное число баров. Такое разделение позволяет использовать один и тот же сигнал при двух подходах к торговле: входить раньше, но с меньшей уверенностью в сигнале в режиме подтверждения, или дожидаться возврата цены к уровню в режиме ретеста. При вызове функции мы получаем следующий результат.

Мы видим, что сетапы подтверждаются и по ним открываются позиции. Остается добавить трейлинг-стопы для сопровождения позиций при движении цены в нашу пользу. Для этого используем следующую логику.
//+------------------------------------------------------------------+ //| 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(); }
После компиляции получаем следующий результат.

На визуализации видно, что при движении цены в нашу пользу позиции сопровождаются трейлинг-стопом. Остается протестировать программу на исторических данных — этому посвящен следующий раздел.
Тестирование на исторических данных
Компилируем программу и запускаем ее в Тестере стратегий MetaTrader 5 в визуальном режиме. Это позволяет бар за баром наблюдать формирование четвертей и активацию сетапов по дивергенциям. Результат представлен ниже в формате Graphics Interchange Format (GIF).

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

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

Заключение
В итоге мы реализовали идею tCISD в виде конкретной программы на языке MQL5, которую можно тестировать и которая решает практические проблемы, обозначенные в начале статьи. В частности, программа привязывает циклы и четверти к нью-йоркскому времени с автоматическим учетом летнего времени и определяет положение серверных баров в этой сетке, предотвращая смещение временной привязки. Она сопоставляет бары торгуемого и коррелирующего символов по времени, чтобы избежать ложных межрыночных дивергенций. Кроме того, программа отслеживает максимумы и минимумы четвертей и циклов для обоих символов, выявляет SSMT-дивергенцию, когда только один символ снимает ликвидность за соответствующим экстремумом, и отмечает этот экстремум.
Затем программа определяет триггерный уровень tCISD по цене открытия последней свечи противоположного направления. Четко заданный конечный автомат управляет переходами из исходного состояния к ожиданию подтверждения, а затем — ретеста, обеспечивая вход в позицию на основе дивергенции. В программе предусмотрены вход после подтверждающего закрытия бара или при ретесте, фиксированное количество лотов или расчет размера позиции по проценту риска, стоп-лосс за экстремумом, за которым была снята ликвидность, тейк-профит по заданному соотношению вознаграждения и риска, соблюдение минимального расстояния до стоп-уровней, установленного брокером, опциональные фильтры вероятности (True Open, премия и дисконт, двухэтапный SSMT), трейлинг-стоп, визуализация на графике и ведение логов.
В результате вы получаете рабочую программу, которую можно скомпилировать, запустить в Тестере стратегий и настроить с помощью входных параметров. Она превращает прежде интуитивный процесс в воспроизводимый пайплайн: обнаружение сигнала, проверка условий, срабатывание сигнала на вход, совершение сделки и сопровождение позиции.
Отказ от ответственности: данный материал представлен в образовательных целях. Торговля сопряжена со значительными финансовыми рисками, а результаты, полученные при тестировании на исторических данных, не гарантируют будущих результатов. Перед запуском этой программы на реальном рынке критически важны тщательное тестирование на исторических данных и строгое управление рисками.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23549
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5
Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
Автоматизация торговых стратегий в MQL5 (Часть 51): Модель Bread and Butter Judas Swing с зонами премии и дисконта
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования