Разработка собственного торгового советника (Часть 1): От ненадежного скрипта к работающему советнику
Содержание
- Введение
- Первая попытка: создаем скрипт с недостатками
- Исправление 1: определение появления нового бара
- Исправление 2: учет открытых позиций и магический номер
- Исправление 3: динамические уровни стоп-лосса и тейк-профита на основе ATR
- Исправление 4: обработка ошибок и проверка корректности
- Дополнительные защитные проверки для реальной эксплуатации
- Заключение
Введение
Многие трейдеры пытаются пройти путь от торговой идеи до полностью автоматизированного торгового советника, но лишь немногие успешно доходят до конца. Проблема редко заключается в самой торговой идее. Чаще проблема заключается в архитектуре программы, реализующей эту идею. Скрипт может успешно компилироваться, совершать торговые операции и показывать хорошие результаты при тестировании на исторических данных. Однако в реальной торговле скрипт все равно может давать сбои, если в нем отсутствуют защитные механизмы, связанные с исполнением торговых операций, управлением позициями, контролем рисков и проверкой корректности. В этой статье мы преобразуем советник на пересечении скользящих средних, в котором намеренно допущены ошибки, в более надёжную основу для автоматической торговли. В процессе мы устраним несколько типичных недостатков советников, созданных начинающими разработчиками:
- Повторные входы, вызванные обработкой сигналов на каждом тике.
- Отсутствие учета открытых позиций и разграничения торговых операций по магическому номеру.
- Фиксированные уровни стоп-лосса и тейк-профита, не учитывающие волатильность рынка.
- Отсутствуют проверки и обработка ошибок.
В данной статье предполагается, что читатель в общих чертах знаком с MQL5 и MetaTrader 5. Вместо того чтобы уделять основное внимание разработке торговой стратегии, мы сосредоточимся на архитектурных улучшениях, которые помогут превратить рабочий скрипт в более надежный торговый советник.
К концу этой части мы:
- Создадим базовый каркас советника.
- Реализуем определение появления нового бара.
- Добавим разграничение торговых операций по магическому номеру и учет открытых позиций.
- Заменим фиксированные уровни стоп-лосса и тейк-профита уровнями на основе ATR.
- Добавим проверки корректности данных и исполнения торговых операций.
- Предложим дополнительные защитные проверки для реальной эксплуатации.
Примеры основаны на простой стратегии пересечения скользящих средних, однако представленные методы можно применять во многих других системах автоматической торговли.
Создаем первый вариант: скрипт с недостатками
Начнём с простой стратегии, основанной на пересечении скользящих средних. Когда быстрая скользящая средняя пересекает медленную снизу вверх, советник открывает позицию на покупку; когда она пересекает ее сверху вниз, советник открывает позицию на продажу. Трейдер, обладающий базовыми знаниями по MQL5, сможет быстро воплотить эту идею в жизнь. Созданный советник компилируется, открывает позиции и, на первый взгляд, работает нормально, однако в его архитектуре присутствует ряд недостатков, которые проявятся при торговле в реальных условиях.
Вот каркас советника, с которого трейдер начинает писать код:
//+------------------------------------------------------------------+ //| FlawedMACrossover.mq5 | //| Copyright 2026, soloharbinger | //| https://www.mql5.com/en/users/soloharbinger | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, soloharbinger" #property link "https://www.mql5.com/en/users/soloharbinger" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- }
Затем трейдер подключает торговую библиотеку и добавляет входной параметр, который пользователь может настраивать:
//+------------------------------------------------------------------+ //| FlawedMACrossover.mq5 | //| Copyright 2026, soloharbinger | //| https://www.mql5.com/en/users/soloharbinger | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, soloharbinger" #property link "https://www.mql5.com/en/users/soloharbinger" #property version "1.00" #include <Trade/Trade.mqh> //--- Input Parameters input int FastMA = 10; // Fast MA period input int SlowMA = 20; // Slow MA period input double LotSize = 0.1; // Fixed lot size input int StopLoss = 200; // Fixed stop loss in points input int TakeProfit = 400; // Fixed take profit in points
Пока что всё выглядит вполне разумно. Входные параметры понятны, и пользователь может настраивать их в окне свойств советника.
Глобальные хэндлы и инициализация
Далее трейдер объявляет глобальные переменные сразу под входными параметрами для хранения хэндлов индикаторов, использует класс CTrade для торговых операций и инициализирует хэндлы индикаторов в функции OnInit():
//--- Global Variables CTrade trade; int fastHandle, slowHandle; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize Handles fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE); slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE); if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE) { return INIT_FAILED; } //--- EA initialization successful return(INIT_SUCCEEDED); }
Трейдер создает два хэндла скользящих средних, проверяет их корректность и возвращает INIT_SUCCEEDED. Кроме того, трейдер освобождает хэндлы в функции OnDeinit():
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Deinitialize indicator IndicatorRelease(fastHandle); IndicatorRelease(slowHandle); }
Торговая логика в функции OnTick()
Трейдер знает, что функция OnTick() вызывается при каждом тике, и хочет проверять пересечения скользящих средних на последнем закрытом баре (индекс 1). Трейдер копирует значения MA, сравнивает их и выполняет торговые операции с помощью функций класса CTrade:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Populate arrays for both fast and slow indicators double fast[], slow[]; ArraySetAsSeries(fast, true); ArraySetAsSeries(slow, true); CopyBuffer(fastHandle, 0, 0, 3, fast); CopyBuffer(slowHandle, 0, 0, 3, slow); //--- Check crossover on the latest closed bar (index 1) and execute trade if(fast[1] > slow[1] && fast[2] <= slow[2]) { // Buy signal double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - StopLoss * _Point; double tp = ask + TakeProfit * _Point; trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy"); } else if(fast[1] < slow[1] && fast[2] >= slow[2]) { // Sell signal double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + StopLoss * _Point; double tp = bid - TakeProfit * _Point; trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell"); } }
Полный код торгового советника с ошибками
Вот полный текст скрипта в том виде, в каком его написал наш трейдер. Программа компилируется, подключается к графику и, похоже, даже работает — по крайней мере, в течение нескольких минут:
//+------------------------------------------------------------------+ //| FlawedMACrossover.mq5 | //| Copyright 2026, soloharbinger | //| https://www.mql5.com/en/users/soloharbinger | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, soloharbinger" #property link "https://www.mql5.com/en/users/soloharbinger" #property version "1.00" #include <Trade/Trade.mqh> //--- Input Parameters input int FastMA = 10; // Fast MA period input int SlowMA = 20; // Slow MA period input double LotSize = 0.1; // Fixed lot size input int StopLoss = 200; // Fixed stop loss in points input int TakeProfit = 400; // Fixed take profit in points //--- Global Variables CTrade trade; int fastHandle, slowHandle; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize Handles fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE); slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE); if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE) { return INIT_FAILED; } //--- EA initialization successful return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Deinitialize indicator IndicatorRelease(fastHandle); IndicatorRelease(slowHandle); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Populate arrays for both fast and slow indicators double fast[], slow[]; ArraySetAsSeries(fast, true); ArraySetAsSeries(slow, true); CopyBuffer(fastHandle, 0, 0, 3, fast); CopyBuffer(slowHandle, 0, 0, 3, slow); //--- Check crossover on the latest closed bar (index 1) and execute trade if(fast[1] > slow[1] && fast[2] <= slow[2]) { // Buy signal double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - StopLoss * _Point; double tp = ask + TakeProfit * _Point; trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy"); } else if(fast[1] < slow[1] && fast[2] >= slow[2]) { // Sell signal double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + StopLoss * _Point; double tp = bid - TakeProfit * _Point; trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell"); } }
Почему этот советник даст сбой при реальной эксплуатации
Хотя советник компилируется, совершает сделки и рассчитывает стоп-лосс и тейк-профит, в нем есть четыре серьезные проблемы:
Проблема 1: Повторные входы
Условие пересечения проверяется на каждом тике, что позволяет открывать несколько одинаковых позиций по одному и тому же сигналу. Трейдеры могут использовать тестер стратегий, чтобы наглядно увидеть эту проблему:
Анимация 1.
Проблема 2: Отсутствие учета открытых позиций
Советник никогда не проверяет, есть ли уже открытая позиция, что может привести к чрезмерному количеству сделок. Это может привести к чрезмерному использованию кредитного плеча, маржин-коллу и потере всех средств на счете.
Проблема 3: Фиксированные уровни стоп-лосса и тейк-профита
Статические уровни стоп-лосса и тейк-профита не адаптируются к изменениям волатильности рынка.
Проблема 4: Отсутствие проверок и обработки ошибок
В коде используется функция отправки торгового запроса, но результат ее вызова не проверяется. Если брокер отклоняет ордер (например, из-за того, что стоп-уровень находится слишком близко к рыночной цене или объем в лотах недопустим), советник может записать в журнал общее сообщение об ошибке и продолжить работу. Советник не узнает, что операция завершилась неудачно. Данные индикаторов не проверяются, успешность выполнения торговых запросов не подтверждается, а содержательная диагностическая информация, которая помогла бы выявить проблемы с исполнением торговых операций, отсутствует.
Эти проблемы часто встречаются в советниках, разработанных начинающими программистами, и могут привести к нестабильному поведению при реальной торговле. В следующих разделах мы рассмотрим каждую проблему и постепенно преобразуем скрипт в более надёжную основу.
Решение 1: Определение появления нового бара
Первый серьезный недостаток нашего торгового советника — повторное исполнение торговых операций. Поскольку функция OnTick() запускается при поступлении каждого нового ценового тика, условие пересечения может оставаться истинным в течение всей свечи. Без дополнительного контроля советник может открыть несколько позиций по одному сигналу. Решение заключается в том, чтобы принимать торговые решения только один раз на бар.
Решение: обрабатывать сигналы только один раз на бар
Достичь этого можно, отслеживая время открытия текущего бара. Если советник уже обработал этот бар, он немедленно выходит из обработчика. В противном случае он обновляет сохраненную метку времени и продолжает выполнение торговой логики.
Реализация выглядит следующим образом:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Get the opening time of the current bar datetime currentBarTime = iTime(_Symbol, _Period, 0); if(currentBarTime == 0) return; //--- Static variable to remember the last bar we processed static datetime lastProcessedBarTime = 0; //--- If this bar has already been processed, exit immediately if(currentBarTime == lastProcessedBarTime) return; //--- Mark this bar as processed lastProcessedBarTime = currentBarTime; //--- Now we can safely execute our trading logic double fast[], slow[]; ArraySetAsSeries(fast, true); ArraySetAsSeries(slow, true); CopyBuffer(fastHandle, 0, 0, 3, fast); CopyBuffer(slowHandle, 0, 0, 3, slow); //--- Check crossover on the latest closed bar (index 1) and execute trade if(fast[1] > slow[1] && fast[2] <= slow[2]) { // Buy signal double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - StopLoss * _Point; double tp = ask + TakeProfit * _Point; trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy"); } else if(fast[1] < slow[1] && fast[2] >= slow[2]) { // Sell signal double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + StopLoss * _Point; double tp = bid - TakeProfit * _Point; trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell"); } }
Ключевое слово static позволяет переменной сохранять своё значение между вызовами функции. Без этого значение lastProcessedBarTime сбрасывалось бы на каждом тике, и механизм определения появления нового бара не работал бы. В качестве дополнительной защитной меры перед обработкой нового бара мы проверяем, что iTime() возвращает корректную метку времени, чтобы код не выполнялся, пока данные синхронизируются.
Почему это работает
Рассмотрим, что происходит при открытии нового бара:
- При появлении первого тика нового бара переменная currentBarTime принимает значение времени открытия нового бара.
- В переменной `lastProcessedBarTime` по-прежнему хранится время открытия предыдущего бара.
- Поскольку они различаются, условие не выполняется, и мы приступаем к выполнению нашей торговой логики.
- Сразу после этого мы устанавливаем значение `lastProcessedBarTime` равным времени текущего бара.
При каждом последующем тике того же бара:
- currentBarTime остаётся неизменным.
- Значение lastProcessedBarTime теперь равно currentBarTime.
- Условие if(currentBarTime == lastProcessedBarTime) становится истинным, и мы досрочно выходим из функции.
Результат можно легко проверить в тестере стратегий:
Анимация 2.
Благодаря этому дополнению торговый советник оценивает сигналы только один раз за свечу, что позволяет исключить повторные входы, вызванные многократной обработкой тиков. Следующее усовершенствование — учет открытых позиций, позволяющий торговому советнику правильно определять собственные позиции и управлять ими.
Исправление 2: Учет открытых позиций и магический номер
Благодаря определению появления нового бара советник больше не открывает несколько позиций на одной и той же свече. Однако учет открытых позиций в нем по-прежнему не реализован. Торговый советник не знает, есть ли у него уже открытая позиция, и может продолжать открывать новые позиции, не учитывая существующую рисковую экспозицию. Чтобы решить эту проблему, мы добавим разграничение торговых операций по магическому номеру и функции подсчета позиций.
Решение: подсчет позиций и разграничение торговых операций по магическому номеру
Чтобы исправить это, нам понадобятся 2 компонента:
- Магический номер: уникальный идентификатор, которым помечаются все торговые операции нашего советника. Это позволяет нам отличать наши торговые операции от ручных операций и операций других советников.
- Функции подсчета позиций: функции, которые подсчитывают, сколько позиций открыто у нашего советника и в каком направлении (покупка или продажа).
Добавление магического номера
Магический номер — это обычный целочисленный входной параметр. Мы добавляем его к нашим входным параметрам:
//--- Input Parameters input int FastMA = 10; // Fast MA period input int SlowMA = 20; // Slow MA period input double LotSize = 0.1; // Fixed lot size input int StopLoss = 200; // Fixed stop loss in points input int TakeProfit = 400; // Fixed take profit in points input int MagicNumber = 12345; // EA Magic Number
Затем мы задаем этот номер для нашего торгового объекта в функции OnInit():
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the Magic Number for all trades trade.SetExpertMagicNumber(MagicNumber); //--- Initialize Handles fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE); slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE); if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE) { return INIT_FAILED; } //--- EA initialization successful return(INIT_SUCCEEDED); }
Каждая позиция, открытая советником, теперь помечена уникальным идентификатором советника, что позволяет отличать ее от позиций, открытых вручную или другими советниками.
Создание функций подсчета позиций
Теперь нам нужны функции, которые покажут, сколько позиций открыто у нашего советника. Мы напишем 2 вспомогательные функции:
- Функция CountOpenPositions() возвращает общее количество открытых позиций нашего советника.
- Функция CountOpenPositionsByDirection() возвращает количество открытых позиций нашего советника по каждому направлению — на покупку и на продажу.
Вот реализация:
//+------------------------------------------------------------------+ //| Count total open positions for this EA | //+------------------------------------------------------------------+ int CountOpenPositions() { int count = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket)) { if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { count++; } } } return count; } //+------------------------------------------------------------------+ //| Count open positions by direction (1 = Buy, -1 = Sell) | //+------------------------------------------------------------------+ int CountOpenPositionsByDirection(int direction) { int count = 0; ENUM_POSITION_TYPE posType = (direction == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket)) { if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == posType) { count++; } } } return count; }
Эти вспомогательные функции подсчитывают все открытые позиции по текущему торговому символу с заданным магическим номером. Функция подсчета позиций по направлению позволяет проверить, сколько у нас в данный момент открытых позиций на покупку или продажу.
Добавление учета открытых позиций в OnTick()
Теперь мы можем использовать эти функции, чтобы избежать чрезмерной торговли. Мы добавим простое правило: открывать новую позицию только в том случае, если в этом направлении еще нет открытой позиции. В функции OnTick() после определения появления нового бара мы проверим, есть ли у нас уже позиция в том направлении, в котором мы собираемся торговать:
//--- Populate MA array double fast[], slow[]; ArraySetAsSeries(fast, true); ArraySetAsSeries(slow, true); CopyBuffer(fastHandle, 0, 0, 3, fast); CopyBuffer(slowHandle, 0, 0, 3, slow); //--- Check crossover on the latest closed bar (index 1) and execute trade if(fast[1] > slow[1] && fast[2] <= slow[2]) { //--- Buy signal: Only open a trade if we don't already have a buy position if(CountOpenPositionsByDirection(1) == 0) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - StopLoss * _Point; double tp = ask + TakeProfit * _Point; trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed Buy"); } } else if(fast[1] < slow[1] && fast[2] >= slow[2]) { //--- Sell signal: Only open a trade if we don't already have a sell position if(CountOpenPositionsByDirection(-1) == 0) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + StopLoss * _Point; double tp = bid - TakeProfit * _Point; trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed Sell"); } } }
С учетом этих изменений торговый советник обрабатывает сигналы один раз за бар. Кроме того, советник проверяет, есть ли уже позиция в направлении сигнала, и открывает новую позицию только в том случае, если такой позиции нет. Это сокращает количество повторных входов и предотвращает открытие нескольких позиций по одному и тому же сигналу на одном баре. Хотя это значительно повышает торговую дисциплину, такой подход не следует рассматривать как полноценное управление позициями. Такие факторы, как определение размера позиции, лимиты риска по счету и правила исполнения торговых операций, по-прежнему влияют на общий торговый риск.
Примечание о максимальном количестве позиций
Некоторые трейдеры могут захотеть разрешить открытие нескольких позиций в одном направлении, например для постепенного наращивания позиции по тренду. Мы рассмотрим этот вопрос в одной из следующих частей данной серии, когда будем обсуждать системы наращивания позиций и пирамидинг. На данный момент правило «одна позиция на каждое направление» является безопасным и консервативным вариантом по умолчанию.
Вариант без хеджирования
Некоторые трейдеры допускают открытие разнонаправленных позиций по одному и тому же торговому символу. Другие же предпочитают строго направленный подход. Чтобы поддержать оба подхода, мы можем ввести необязательный параметр, разрешающий хеджирование.Добавление входного параметра
Нам нужно добавить простой входной параметр логического типа ниже остальных:
//--- Input Parameters input int FastMA = 10; // Fast MA period input int SlowMA = 20; // Slow MA period input double LotSize = 0.1; // Fixed lot size input int StopLoss = 200; // Fixed stop loss in points input int TakeProfit = 400; // Fixed take profit in points input int MagicNumber = 12345; // EA Magic Number input bool AllowHedging = false; // Allow opposing positions (hedging)
Еще раз изменим проверку позиций
Если входной параметр AllowHedging имеет значение false, необходимо блокировать любые новые входы в рынок, если уже есть открытая позиция в любом направлении. Мы соответствующим образом изменим нашу функцию OnTick():
//--- Check crossover on the latest closed bar (index 1) and execute trade if(fast[1] > slow[1] && fast[2] <= slow[2]) { //--- Buy signal: Check if we can open a Buy position bool canOpenTrade = false; if(AllowHedging) { //--- Hedging is allowed: Only block Buys if there is already a Buy canOpenTrade = (CountOpenPositionsByDirection(1) == 0); } else { //--- Hedging is not allowed: Block trades if there is any open position canOpenTrade = (CountOpenPositions() == 0); } if(canOpenTrade) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - StopLoss * _Point; double tp = ask + TakeProfit * _Point; trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy"); } } else if(fast[1] < slow[1] && fast[2] >= slow[2]) { //--- Sell signal: Check if we can open a Sell position bool canOpenTrade = false; if(AllowHedging) { //--- Hedging is allowed: Only block Sells if there is already a Sell canOpenTrade = (CountOpenPositionsByDirection(-1) == 0); } else { //--- Hedging is not allowed: Block trades if there is any open position canOpenTrade = (CountOpenPositions() == 0); } if(canOpenTrade) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + StopLoss * _Point; double tp = bid - TakeProfit * _Point; trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Sell"); } } }
На данном этапе советник может распознавать собственные торговые операции, предотвращать повторные входы в одном направлении и при необходимости запрещать одновременное открытие разнонаправленных позиций. Это составляет основу управления позициями, но пока не является полноценной системой управления сделками. Более продвинутые советники могут закрывать или разворачивать существующие позиции при появлении противоположного сигнала, учитывать особенности работы неттинговых счетов, а также обрабатывать частичное исполнение ордеров и другие сценарии исполнения торговых операций. Эти усовершенствования опираются на изложенные здесь базовые принципы.
Исправление 3: Динамические стоп-уровни на основе ATR
Мы разобрались с повторными входами и управлением позициями, но наш советник по-прежнему использует фиксированные уровни стоп-лосса и тейк-профита. Проблема с фиксированными расстояниями заключается в том, что волатильность рынка постоянно меняется. Стоп-уровень, подходящий для спокойных рыночных условий, может оказаться слишком близким к цене в периоды высокой волатильности. Распространенное решение — использовать индикатор «средний истинный диапазон (ATR)» для динамического расчета расстояний до стоп-уровней с учетом текущей волатильности рынка. Этот инструмент широко используется для оценки волатильности рынка, поэтому его можно применять для динамического расчета уровней стоп-лосса и тейк-профита.
Почему ATR?
ATR — это индикатор волатильности. Более высокие значения ATR свидетельствуют о более значительных ценовых колебаниях, тогда как более низкие значения указывают на более спокойную рыночную конъюнктуру. Опираясь при расчете стоп-лоссов и тейк-профитов на показатель ATR, советник автоматически адаптирует свои параметры риска к текущим рыночным условиям.
Добавление ATR в наш советник
Сначала мы добавляем входные параметры: период ATR, множитель и коэффициент для расчета тейк-профита на основе расстояния до стоп-лосса. Кроме того, мы удалим входные параметры, задающие фиксированные значения стоп-лосса и тейк-профита:
//--- Input Parameters input int FastMA = 10; // Fast MA period input int SlowMA = 20; // Slow MA period input double LotSize = 0.1; // Fixed lot size input int MagicNumber = 12345; // EA Magic Number input bool AllowHedging = false; // Allow Opposing Position (Hedging) input int ATRPeriod = 14; // ATR Period for Volatility Calculation input double ATRMultiplier = 1.5; // ATR Multiplier for Stop-Loss Distance input double TPRatio = 2.0; // Take Profit Ratio
Далее мы создаем хэндл индикатора ATR в функции OnInit(), как и для индикатора скользящей средней:
//--- Global Variables CTrade trade; int fastHandle, slowHandle; int atrHandle; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the Magic Number for all trades trade.SetExpertMagicNumber(MagicNumber); //--- Initialize Handles fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE); slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE); atrHandle = iATR(_Symbol, _Period, ATRPeriod); if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE) { return INIT_FAILED; } //--- EA initialization successful return(INIT_SUCCEEDED); }
А в функции OnDeinit() мы освобождаем хэндл индикатора ATR:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Deinitialize indicator IndicatorRelease(fastHandle); IndicatorRelease(slowHandle); IndicatorRelease(atrHandle); }
Расчет динамических уровней стоп-лосса и тейк-профита
Теперь, когда индикатор ATR подготовлен, мы изменим торговую логику, чтобы использовать уровни стоп-лосса и тейк-профита на основе ATR. В функции OnTick() мы сначала скопируем текущее значение ATR. Рассчитаем расстояние до стоп-лосса:
//--- Populate the ATR double atrArray[]; ArraySetAsSeries(atrArray, true); CopyBuffer(atrHandle, 0, 0, 1, atrArray); double atrValue = atrArray[0];
Примечание: мы используем atrArray[0], то есть текущее значение ATR, а не значение на последнем закрытом баре. В отличие от сигнала пересечения скользящих средних, ATR используется только для определения расстояния до стоп-лосса в момент открытия позиции. Использование самого свежего значения ATR позволяет уровням стоп-лосса и тейк-профита отражать текущую волатильность рынка, сохраняя динамический характер управления рисками на основе ATR.
Прежде чем рассчитывать динамическое расстояние до стоп-лосса, необходимо проверить корректность значения ATR. Это предотвращает попытки советника торговать, когда данные ATR еще недоступны (например, сразу после добавления советника на график). Без этой проверки советник рассчитывал бы нулевое расстояние до стоп-лосса, что приводило бы к некорректным уровням стоп-лосса и тейк-профита и отклонению ордеров. Мы просто добавляем следующую проверку:
//--- Validate that we have valid data if(atrValue <= 0) return; // No valid ATR yet
Получение рыночных цен
Прежде чем рассчитывать динамическое расстояние до стоп-лосса с помощью ATR, необходимо получить актуальные рыночные цены. Вместо того чтобы запрашивать цены Ask и Bid по отдельности, мы используем функцию SymbolInfoTick(), чтобы получить согласованный срез последних рыночных данных. Это снижает вероятность считывания цен из разных тиков. Кроме того, этот способ соответствует рекомендациям по получению текущих рыночных цен в MQL5.
//--- Retrieve market prices MqlTick tick; if(!SymbolInfoTick(_Symbol, tick)) { Print("Failed to obtain market prices."); return; } double ask = tick.ask; double bid = tick.bid;
Теперь рассчитаем расстояние до стоп-уровня, число знаков после запятой и величину пункта для торгуемого символа:
//--- Calculate the dynamic stop distance double stopDistance = atrValue * ATRMultiplier; int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
Затем мы используем это динамическое расстояние как для стоп-лосса, так и для тейк-профита. Для сделки на покупку:
double sl = NormalizeDouble(ask - stopDistance, digits); double tp = NormalizeDouble(ask + (stopDistance * TPRatio), digits);
А для сделки на продажу:
double sl = NormalizeDouble(bid + stopDistance, digits); double tp = NormalizeDouble(bid - (stopDistance * TPRatio), digits);
Примечание о нормализации цены
Функция NormalizeDouble() обеспечивает соответствие значений стоп-лосса и тейк-профита требуемой точности цены для данного торгового символа. Эта функция преобразования необходима, поскольку MetaTrader 5 требует округлять цену до числа знаков после запятой, установленного для данного торгового символа. В противном случае брокер может отклонить ордер. Число знаков после запятой определяется с помощью:SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
Для большинства валютных пар это 5 знаков после запятой (например, 1.12345), но для некоторых торговых символов их может быть 2, 3 или 4.
Благодаря интеграции ATR уровни стоп-лосса и тейк-профита теперь автоматически корректируются с учетом волатильности рынка. Это обеспечивает более гибкое управление рисками по сравнению с использованием стоп-уровней на фиксированном расстоянии и позволяет советнику адаптироваться к различным рыночным условиям и таймфреймам.
Исправление 4: Обработка ошибок и проверки
Несмотря на уже внесенные улучшения, наш советник по-прежнему исходит из того, что каждая операция завершается успешно. На практике данные индикаторов могут быть недоступны, вызовы функции CopyBuffer() могут завершаться с ошибкой, а брокеры могут отклонять торговые запросы. Надежный советник должен проверять корректность данных перед выполнением торговых операций и контролировать результаты важных операций.
Проблема: скрытый сбой
В текущей версии наш советник выполняет торговые операции, не проверяя результат их выполнения. Обратите внимание на эту строку из нашей функции OnTick():
trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy");
Если этот вызов Buy() завершится неудачей (например, из-за того, что стоп-лосс находится слишком близко к цене входа в рынок), советник просто продолжит работу. Без проверки результатов советник не может узнать, был ли торговый запрос принят или отклонен. С точки зрения трейдера сигналы могут казаться корректными, хотя на самом деле торговая операция не исполняется. Это затрудняет поиск и устранение неисправностей и может приводить к тому, что проблемы остаются незамеченными в течение длительного времени.
Решение: систематическая проверка данных и результатов
Для повышения надёжности мы будем:
- Проверять корректность данных индикатора перед их использованием.
- Проверять результаты исполнения торговых операций.
- Записывать в журнал полезную диагностическую информацию при возникновении проблем.
Проверка данных: обеспечение готовности индикатора
Перед копированием данных из индикаторного буфера рекомендуется убедиться, что рассчитано достаточное количество баров. В MQL5 функция BarsCalculated() используется для того, чтобы советник не пытался считывать ещё не рассчитанные данные, что позволяет избежать лишних ошибок времени выполнения при запуске.
//--- Ensure indicators are ready if(BarsCalculated(fastHandle) < 3 || BarsCalculated(slowHandle) < 3 || BarsCalculated(atrHandle) < 1) { Print("Indicators are not ready yet."); return; }
Проверка корректности данных индикатора
Мы уже добавили проверку данных ATR:
//--- Validate that we have a valid ATR value if(atrValue <= 0) return; // No valid ATR yet
Однако не следует ограничиваться проверкой корректности данных ATR. Кроме того, нам следует проверять и записывать в журнал значение ATR и данные ATR, чтобы знать, когда советник не выполняет расчёты.
//--- Populate the ATR double atrArray[]; ArraySetAsSeries(atrArray, true); if(CopyBuffer(atrHandle, 0, 0, 1, atrArray) < 1) { Print("Failed to get ATR data"); return; } double atrValue = atrArray[0]; //--- Validate that we have a valid ATR value if(atrValue <= 0) { Print("Invalid ATR value: ", atrValue); return; }
Тот же принцип применим и к скользящим средним. Прежде чем использовать значения индикаторов, необходимо убедиться, что было успешно скопировано достаточное количество данных.
//--- Populate the MA buffer double fast[], slow[]; ArraySetAsSeries(fast, true); ArraySetAsSeries(slow, true); int fastCopied = CopyBuffer(fastHandle, 0, 0, 3, fast); int slowCopied = CopyBuffer(slowHandle, 0, 0, 3, slow); //--- Validate that we have enough MA data if(fastCopied < 3 || slowCopied < 3) { Print("Failed to get enough Moving Average Data"); return; }
Все эти проверки позволяют гарантировать, что данные, используемые в торговой логике, всегда корректны.
Проверка результатов
Теперь нам нужно проверить, действительно ли наши торговые операции были успешно исполнены. Класс CTrade возвращает логическое значение, указывающее, был ли торговый запрос успешно отправлен, а также предоставляет доступ к дополнительной информации через методы получения результатов. Мы включим это в нашу торговую логику для покупок:
if(canOpenTrade) { double sl = NormalizeDouble(ask - stopDistance, digits); double tp = NormalizeDouble(ask + (stopDistance * TPRatio), digits); // Execute Buy trade and check result if(!trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy")) { PrintFormat("Buy failed. Retcode: %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription()); } } }
А для продаж:
if(canOpenTrade) { double sl = NormalizeDouble(bid + stopDistance, digits); double tp = NormalizeDouble(bid - (stopDistance * TPRatio), digits); // Execute Sell trade and check result if(!trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Sell")) { PrintFormat("Sell failed. Retcode: %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription()); } } }Методы ResultRetcode() и ResultRetcodeDescription() предоставляют информацию от брокера о причинах успешного исполнения ордера или отказа в его исполнении, что значительно упрощает диагностику проблем с исполнением торговых операций.
Примечание о распространенных кодах ошибок
При неудачном выполнении операции полезно понимать значение некоторых наиболее распространенных кодов возврата торгового сервера:| Код ошибки | Значение | Вероятная причина в торговом советнике |
|---|---|---|
| 10014 | Недопустимый объем в запросе | Объем в лотах выходит за пределы допустимого диапазона |
| 10015 | Неверная цена в запросе | Цена не нормализована или выходит за пределы диапазона |
| 10016 | Недопустимые стоп-уровни в запросе | Стоп-лосс или тейк-профит расположены слишком близко к цене входа в рынок |
Распространенные ошибки времени выполнения часто связаны с недопустимыми параметрами, недоступностью данных индикаторов или отклоненными торговыми запросами. При устранении неполадок документация MQL5 по-прежнему остается лучшим справочным материалом для расшифровки конкретных кодов ошибок.
Вот наглядный пример того, как ведение журнала ошибок помогает трейдерам быстро выявлять ошибки времени выполнения:
Анимация 3.
Преимущества ведения журнала ошибок хорошо видны в тестере стратегий, где проблемы, возникающие во время выполнения, значительно проще выявлять и диагностировать.
Проверка входных параметров, задаваемых пользователем
Перед завершением инициализации рекомендуется проверить допустимость значений входных параметров, задаваемых пользователем. Недопустимые значения могут привести к сбою при создании индикатора, некорректным расчетам или к тому, что торговые запросы так и не будут исполнены. Вместо того чтобы допускать дальнейшую работу советника в некорректном состоянии, мы можем выявлять ошибки конфигурации в функции OnInit() и прерывать процесс инициализации, выводя информативное сообщение.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the Magic Number for all trades trade.SetExpertMagicNumber(MagicNumber); //--- Initialize Handles fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE); slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE); atrHandle = iATR(_Symbol, _Period, ATRPeriod); if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE) { return INIT_FAILED; } //--- Validate user inputs if(FastMA <= 0 || SlowMA <= 0) { Print("Moving Average periods must be greater than zero."); return INIT_PARAMETERS_INCORRECT; } if(FastMA >= SlowMA) { Print("FastMA should be smaller than SlowMA."); return INIT_PARAMETERS_INCORRECT; } if(LotSize <= 0) { Print("Lot size must be greater than zero."); return INIT_PARAMETERS_INCORRECT; } if(ATRPeriod <= 0 || ATRMultiplier <= 0) { Print("ATR parameters must be greater than zero."); return INIT_PARAMETERS_INCORRECT; } //--- EA initialization successful return(INIT_SUCCEEDED); }
Возврат значения INIT_PARAMETERS_INCORRECT предотвращает загрузку советника при обнаружении недопустимых настроек. Это позволяет устранять проблемы с настройками сразу же, а не ждать, пока они проявятся позже во время торговли или тестирования на исторических данных.
Дополнительные защитные проверки для реальной эксплуатации
На данном этапе наш торговый советник проверяет данные индикаторов, проверяет результаты исполнения торговых операций и сообщает о сбоях с помощью содержательных сообщений в журнале. Однако прежде чем считать торговый советник готовым к работе, стоит реализовать несколько дополнительных защитных проверок. Эти проверки не привязаны к какой-либо конкретной торговой стратегии. Они помогают отправлять торговые запросы только тогда, когда рыночные условия и ограничения счета позволяют исполнить соответствующие торговые операции.
1. Проверка спреда
Спред отражает стоимость входа в рынок. В периоды низкой ликвидности или выхода важных новостей спреды могут значительно расширяться, делая торговлю по в остальном корректным сигналам экономически невыгодной. Перед размещением ордера мы можем убедиться, что текущий спред остается ниже установленного пользователем порогового значения.
Входной параметр
input double MaxSpreadPips = 2.0; // Maximum allowed spread
Вспомогательная функция
//+------------------------------------------------------------------+ //| Check if current spread is acceptable | //+------------------------------------------------------------------+ bool IsSpreadAcceptable() { MqlTick tick; if(!SymbolInfoTick(_Symbol, tick)) return false; double spreadPoints = (tick.ask - tick.bid) / _Point; if(spreadPoints > MaxSpreadPips * 10.0) { PrintFormat("Spread too high: %.1f points (Maximum %.1f pips)", spreadPoints, MaxSpreadPips); return false; } return true; }
Назначение
Эту проверку можно встроить в любое место кода перед блоком исполнения торговых операций.
//--- Check Spread before trading if(!IsSpreadAcceptable()) return;
2. Торговые ограничения брокера
Брокеры задают минимальные расстояния до стоп-уровней через параметр «минимальный отступ стоп-уровней», а через параметр «уровень заморозки» временно ограничивают изменение ордеров вблизи рыночных цен. Перед отправкой или изменением ордеров эти значения можно запросить с помощью свойств SYMBOL_TRADE_STOPS_LEVEL и SYMBOL_TRADE_FREEZE_LEVEL, чтобы убедиться, что запрашиваемые цены соответствуют торговым правилам брокера. Аналогичным образом советники для реальной эксплуатации часто проверяют, соответствует ли запрашиваемый объем торговой операции требованиям к минимальному и максимальному объему и шагу его изменения для данного торгового символа, а также достаточно ли свободной маржи, прежде чем пытаться исполнить ордер. Хотя эти проверки выходят за рамки данного учебного советника, они станут полезным дополнением при подготовке стратегии к реальной торговле.
Как расширить эту основу
Торговая логика, используемая в данной статье, намеренно проста, однако архитектура системы способна поддерживать множество различных стратегий. Поскольку генерация сигналов отделена от исполнения торговых операций и управления позициями, ту же архитектурную основу можно использовать с другими методами входа в рынок, например с сигналами RSI, системами следования за трендом, пробойными стратегиями или пользовательскими индикаторами.
По мере развития серии мы будем продолжать расширять эту основу, добавляя такие компоненты, как фильтры, средства управления торговыми сессиями, расширенные средства управления рисками, системы управления торговыми операциями и средства отслеживания результатов.
Распространенные ошибки и способы их устранения
| Ошибка | Решение |
|---|---|
| Обработка сигналов при каждом тике. | Используйте определение появления нового бара для стратегий, основанных на барах. |
| Игнорирование открытых позиций. | Отслеживайте позиции с помощью магических номеров и функций подсчета позиций. |
| Использование фиксированных значений стоп-лосса. | Используйте индикатор ATR или другие расчеты на основе волатильности. |
| Торговля с использованием некорректных данных индикатора. | Перед использованием проверяйте корректность данных в индикаторных буферах. |
| Игнорирование результатов исполнения торговых операций. | Проверяйте возвращаемые значения торговых операций и регистрируйте сбои в журнале. |
| Игнорирование спреда и ограничений брокера. | Проверяйте спред перед исполнением торговых операций и корректируйте их параметры в соответствии с требованиями брокера. |
| Разработка слишком большого количества функций одновременно. | Разрабатывайте и тестируйте по одному компоненту за раз. |
Эти проблемы часто встречаются в советниках, созданных начинающими разработчиками. Устранение этих проблем на раннем этапе создает более надежную основу для дальнейшей разработки.
Что ещё не было рассмотрено
Усовершенствования, представленные в этой статье, создают гораздо более прочную архитектурную основу, однако не охватывают все защитные меры, применяемые в торговых советниках для реальной эксплуатации. По мере развития советника можно добавлять более сложные функции. В этой статье показаны основные архитектурные принципы, которые позволяют превратить работоспособный скрипт в более надежную отправную точку для автоматической торговли.
Заключение
Одной только торговой стратегии недостаточно для создания надежного советника. Архитектура, обеспечивающая работу стратегии, — учет открытых позиций, контроль рисков, проверки и обработка результатов исполнения торговых операций — зачастую определяет, будет ли советник надежно работать в реальной торговле.В этой статье мы превратили несовершенный советник на основе пересечения скользящих средних в более надежную основу для дальнейшей разработки, добавив определение появления нового бара, разграничение торговых операций по магическому номеру, учет открытых позиций, управление уровнями стоп-лосса и тейк-профита на основе ATR, а также базовые проверки и обработку ошибок. Хотя сама стратегия по-прежнему остается простой, эта структура теперь может служить отправной точкой для создания более совершенных систем автоматической торговли.
Исходные файлы, использованные в этой статье, приложены ниже, чтобы вы могли сравнить исходную реализацию с усовершенствованной версией, разработанной шаг за шагом.
| Имя файла | Описание |
|---|---|
| FixedMACrossover | Усовершенствованный советник с определением появления нового бара, управлением позициями, уровнями стоп-лосса и тейк-профита на основе ATR и проверками. |
| FlawedMACrossover | Исходная версия, использованная для демонстрации типичных архитектурных проблем. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23429
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.