Осваиваем графики Kagi в MQL5 (Часть 2): Реализация автоматизированной торговли на основе Kagi
Введение
В первой части этой серии мы создали полноценный движок для построения графиков Kagi на языке MQL5. Мы узнали, как собирать ценовые данные, строить структуру Kagi и наносить на график каждый отрезок линии. К концу первой части у нас был полностью работоспособный график Kagi, который обновляется при появлении каждого нового бара.
Во второй части мы переходим от построения графиков к реальной торговле. Наша цель — превратить график Kagi в работающий советник, способный реагировать на изменения рыночной структуры. Мы внедрим новые функции, которые позволят советнику распознавать сигналы разворота, открывать позиции, управлять риском и сопровождать открытые позиции. Кроме того, мы добавим визуальные маркеры, которые помогут трейдеру видеть, когда именно появляются сигналы.
Эта часть построена непосредственно на базе движка Kagi. Каждая новая функция будет добавляться понятным и простым способом, чтобы читатели могли без труда следить за логикой. К концу этой статьи у вас будет готова полноценная торговая система на основе графиков Kagi, которую можно использовать с любыми инструментами в MetaTrader 5.
Новые функции торгового модуля
В этой части серии мы расширяем функциональность графика Kagi, превращая его в полноценную торговую систему. Прежде чем приступить к программированию, читателю необходимо запустить MetaEditor 5 и загрузить исходный файл из первой части. Файл называется KagiTraderPart1.mq5 и прилагается к статье. Вся новая логика будет построена на этой основе.
В этом разделе мы кратко расскажем о каждой новой функции. Кроме того, мы добавляем небольшой фрагмент подготовительного кода, который понадобится нам позже. Цель состоит в том, чтобы помочь читателю понять, что будет добавлено и почему это важно для торгового движка.
Визуальные маркеры сигналов на покупку и продажу
Первое нововведение — добавление визуальных маркеров. Эти маркеры будут отображать точные моменты появления сигналов на покупку или продажу. Они помогают трейдеру понять, как советник реагирует на развороты Kagi. Советник будет отображать небольшую стрелку над ценовым баром или под ним, чтобы чётко обозначать сигналы для открытия длинных и коротких позиций.
Возможность включать и отключать торговлю
Некоторые пользователи могут захотеть, чтобы советник работал только как средство визуальной индикации. Другие пользователи могут захотеть, чтобы советник автоматически совершал сделки. По этой причине мы вводим входной параметр, который определяет, активна ли торговля. Параметр размещается ниже уже существующих входных параметров.
input group "Trading" input bool enableTrading = true;
Если enableTrading имеет значение true, советник будет открывать новые позиции. Если параметр имеет значение false, советник не будет совершать сделки, но график Kagi по-прежнему будет обновляться в обычном режиме.
Управление направлением торговли
У разных трейдеров разные предпочтения. Некоторые предпочитают открывать только длинные позиции. Другие же предпочитают открывать исключительно короткие позиции. Многие хотят торговать в обоих направлениях. Чтобы обеспечить такую гибкость, мы создаем небольшое перечисление. Оно размещается ниже существующего перечисления.
enum ENUM_TRADE_DIRECTION
{
ONLY_LONG,
ONLY_SHORT,
TRADE_BOTH
};
После его определения мы добавляем новый входной параметр.
input ENUM_TRADE_DIRECTION direction = TRADE_BOTH;
В результате получается простой переключатель. Пользователь может выбрать один из трёх режимов. Советник будет проверять это значение перед открытием любых новых позиций. Если пользователь выберет ONLY_LONG, советник будет игнорировать сигналы на открытие коротких позиций. Если пользователь выберет ONLY_SHORT, советник будет игнорировать сигналы на открытие длинных позиций. Если выбран режим TRADE_BOTH, советник торгует в обоих направлениях.
Открытие длинных позиций при развороте от Инь к Ян

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

Переход от линии Ян к линии Инь свидетельствует о том, что инициативу перехватили продавцы. Это часто знаменует начало нисходящего движения. Советник откроет короткую позицию, когда линия Kagi перейдет с линии Ян на линию Инь. Это дополняет логику открытия покупок. В совокупности эти две функции составляют основу торговой стратегии. В разделе, посвященном реализации, мы покажем, как надежно зафиксировать этот переход.
Выбор между ручным и автоматическим расчетом размера лота
Трейдеры по-разному управляют рисками. Некоторые предпочитают устанавливать фиксированный размер лота. Другие хотят, чтобы размер лота рассчитывался на основе баланса счета. Для поддержки этих двух стилей мы вводим простое перечисление.
enum ENUM_LOT_SIZE_INPUT_MODE
{
MODE_MANUAL,
MODE_AUTO
};
Это перечисление определяет, каким образом будет формироваться размер лота. После его определения мы добавляем следующий входной параметр.
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO;
Когда пользователь выбирает MODE_AUTO, размер лота рассчитывается исходя из выбранного процента риска. По этой причине мы вводим входной параметр, определяющий процент баланса счета, которым можно рисковать.
input double riskPerTradePercent = 1.0;
Если у пользователя счет на 10 000 долларов и он установит riskPerTradePercent равным 1,0, советник рассчитает размер позиции таким образом, чтобы убыток составил не более 100 долларов. Благодаря этому риск становится предсказуемым и стабильным.
Когда пользователь выбирает MODE_MANUAL, советник игнорирует процент риска. Вместо этого советник использует фиксированный размер лота. По этой причине мы добавляем параметр размера лота, задаваемого вручную.
input double lotSize = 0.1;
Это позволяет пользователю полностью контролировать объем каждой сделки. Оба метода широко распространены среди трейдеров. Советник будет поддерживать оба метода.
Установка стоп-лосса на уровне предыдущего локального экстремума
График Kagi выделяет ценовые колебания. Каждый разворот формирует локальный минимум или максимум. Эти точки важны, поскольку их пробой указывает на возможное изменение тренда. По этой причине стоп-лосс для длинной позиции будет установлен на уровне предыдущего локального минимума. Для короткой позиции стоп-лосс будет установлен на уровне предыдущего локального максимума. Этот метод позволяет держать риск под контролем и согласовывает сделку с лежащей в основе структурой Kagi.
Тейк-профит на основе соотношения риска и прибыли
Следующая функция — система соотношения риска и прибыли. Трейдеры часто выбирают фиксированное соотношение риска и прибыли. Для этого мы вводим еще одно перечисление.
enum ENUM_RISK_REWARD_RATIO
{
ONE_TO_ONE,
ONE_TO_ONEandHALF,
ONE_TO_TWO,
ONE_TO_THREE,
ONE_TO_FOUR,
ONE_TO_FIVE,
ONE_TO_SIX
};
Каждое значение определяет, насколько большим будет тейк-профит относительно стоп-лосса. Например, ONE_TO_THREE означает, что прибыль будет в три раза больше риска. Такой подход к управлению рисками широко распространен среди систем следования тренду. После определения перечисления добавляем входной параметр.
input ENUM_RISK_REWARD_RATIO riskRewardRatio = ONE_TO_THREE;
Это позволяет пользователю выбрать соотношение, соответствующее его стилю торговли.
Необязательный трейлинг-стоп
Последняя функция — трейлинг-стоп. Если эта функция включена, стоп-лосс будет следовать за ценой по мере её движения в направлении сделки. Это позволяет защитить прибыль при сильных движениях рынка. Пока что мы добавляем только основной управляющий параметр.
input bool enableTrailingStop = false;
В дальнейшем могут быть добавлены дополнительные параметры, но этого достаточно, чтобы мы могли продолжить разработку.
Обнаружение переходов от линии Инь к линии Ян и от линии Ян к линии Инь
Прежде чем открывать какие-либо позиции, нам необходимо определить, когда линия Kagi действительно меняет направление. На графиках Kagi это изменение направления выражается изменением толщины или цвета линии:
- Переход от линии Инь к линии Ян (сигнал на покупку)
- Переход от линии Ян к линии Инь (сигнал на продажу)
Эти переключения происходят внутри нашей функции ConstructKagiInRealTime и уже аккуратно сгруппированы в три набора условий. Каждый набор обновляет kagiData.isYin и kagiData.isYang, благодаря чему эти участки кода идеально подходят для подключения нашей торговой логики
1. Сложный разворот
Это классические развороты Kagi. Когда цена продвигается достаточно далеко в противоположном направлении, тренд полностью меняется, и толщина линии изменяется.
Переход от линии Ян к линии Инь
Вы найдёте его здесь:
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a complex reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum)){ if(overlayKagi){ DrawBendTop (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMinimum, yangLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMinimum, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMinimum = currentClosePrice; kagiData.isDowntrend = true; kagiData.isUptrend = false; kagiData.isYang = false; kagiData.isYin = true; } }
Переход от линии Инь к линии Ян
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a complex reversal if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum)){ if(overlayKagi){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMaximum, yinLineColor); DrawYangLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMaximum, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMaximum = currentClosePrice; kagiData.isDowntrend = false; kagiData.isUptrend = true; kagiData.isYang = true; kagiData.isYin = false; } }
Это самые сильные типы сигналов Kagi.
2. Сложное продолжение после разворота
Такие ситуации возникают, когда цена выходит за пределы предыдущего локального экстремума после того, как разворот уже произошёл. Толщина линии Kagi снова меняется, и тренд продолжается с новой силой.
Переход от Ян к Инь
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a complex continuation after reversal if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum))){ if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; kagiData.isYang = false; kagiData.isYin = true; } }
Переход от Инь к Ян
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a complex continuation after reversal if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum))){ if(overlayKagi){ DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor); DrawYangLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; kagiData.isYang = true; kagiData.isYin = false; } }
Эти переходы по-прежнему отражают значимые изменения настроений
3. Редкий (необычный) сценарий
Эта категория охватывает необычное поведение цены, которое тем не менее приводит к корректной смене полярности. Хотя это случается редко, необходимо учитывать эту возможность, чтобы не упустить ни одного допустимого перехода.
Переход от Инь к Ян
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a weird scenario if(kagiData.isUptrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.isYin = false; kagiData.isYang = true; kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } }
Переход от Ян к Инь
void ConstructKagiInRealTime(double bidPr, double askPr){ ... //--- Handle a weird scenario if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.isYang = false; kagiData.isYin = true; kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } }
В каждом из этих условий линия Kagi явно меняет одну толщину на другую. Для нашей стратегии этого вполне достаточно. В следующем разделе мы привяжем нашу торговую логику непосредственно к этим точкам перехода. Именно на этом этапе мы также учтём торговый режим пользователя (только длинные позиции, только короткие позиции или оба направления), применим выбранный метод расчёта размера лота и подготовим уровни стоп-лосса и тейк-профита.
Интеграция торговой логики в переходы Kagi
Теперь, когда наша структура Kagi уже сформирована, пришло время связать переходы на графике (от Инь к Ян и от Ян к Инь) с реальными торговыми действиями. В этом разделе мы подготовим торговую среду, представим простую структуру данных для хранения сведений о сделках, а затем рассмотрим функции, используемые для открытия позиций на покупку и продажу. Каждый шаг представлен так, чтобы вы могли следовать ему и сразу реализовать его на практике.
Первым делом необходимо убедиться, что советник может отправлять ордера. Для этого мы подключаем стандартную торговую библиотеку. Разместите эту строку сразу после директив #property:
//+------------------------------------------------------------------+ //| Standard Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh>
Это даёт нам доступ к классу CTrade, который отвечает за все операции отправки ордеров.
Далее нам понадобится простая структура, которая поможет отслеживать последнюю сделку. В ней будут храниться номер тикета, цена входа, стоп-лосс, тейк-профит и другие полезные сведения. Разместите следующую структуру сразу под имеющимися определениями структур:
struct MqlTradeInfo { ulong orderTicket; ENUM_ORDER_TYPE type; ENUM_POSITION_TYPE posType; double entryPrice; double takeProfitLevel; double stopLossLevel; datetime openTime; double lotSize; };
Эта структура будет заполняться каждый раз при открытии новой позиции, что позволит вашему советнику точно знать, какую операцию он только что выполнил.
Сразу после структуры добавьте следующие два глобальных экземпляра:
//--- Instantiate the trade information data structure MqlTradeInfo tradeInfo; //--- Create a CTrade object to handle trading operations CTrade Trade;
Объект Trade отправляет ордера, а tradeInfo выступает в качестве контейнера для хранения результатов последней сделки.
Наша торговая функция будет опираться на точные значения цен Bid и Ask, поэтому мы объявляем две глобальные переменные:
//--- Bid and Ask double askPrice; double bidPrice;
Затем мы обновляем эти значения внутри функции MQL5 OnTick, чтобы они всегда отражали самое актуальное состояние рынка.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); }
Эти цены впоследствии будут переданы в наши функции покупки и продажи.
Чтобы код советника оставался чистым и модульным, мы разместим торговую логику в двух вспомогательных функциях: OpenBuy и OpenSel. Обе функции выполняют практически одинаковые шаги, но в противоположных направлениях. Чтобы не повторять одни и те же объяснения, давайте разберём логику одной функции. Та же логика применима и к другой функции, только в обратном направлении. Ниже приведена функция OpenBuy. Разместите её в том месте кода, где хранятся ваши вспомогательные функции:
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ENUM_ORDER_TYPE action = ORDER_TYPE_BUY; ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY; datetime currentTime = TimeCurrent(); double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); double rewardValue = 1.0; switch(riskRewardRatio){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } double stopLevel = NormalizeDouble(kagiData.localMinimum, Digits()); double stopDistance = NormalizeDouble(askPr - stopLevel, Digits()); double targetLevel = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits()); double volume = NormalizeDouble(lotSize, 2); if(lotSizeMode == MODE_AUTO){ double amountAtRisk = (riskPerTradePercent / 100.0) * accountBalance; volume = amountAtRisk / (contractSize * stopDistance); volume = NormalizeDouble(volume, 2); } if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){ Print("Error while opening a long position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); tradeInfo.orderTicket = result.order; tradeInfo.type = action; tradeInfo.posType = positionType; tradeInfo.entryPrice = result.price; tradeInfo.takeProfitLevel = targetLevel; tradeInfo.stopLossLevel = stopLevel; tradeInfo.openTime = currentTime; tradeInfo.lotSize = result.volume; return true; } return false; }
Чтобы вам было проще понять, что делает эта функция, ниже приведён разбор основных шагов:
Для начала мы сообщаем функции, что открываем позицию на покупку, и фиксируем текущее время.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ENUM_ORDER_TYPE action = ORDER_TYPE_BUY; ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY; datetime currentTime = TimeCurrent(); ... }
Затем мы получаем размер контракта и баланс счета. Эти значения помогают нам автоматически рассчитывать размер лота, когда входной параметр lotSizeMode установлен в MODE_AUTO.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); ... }
Следующий шаг — определить множитель соотношения риска и прибыли.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... double rewardValue = 1.0; switch(riskRewardRatio){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } ... }
Оператор switch преобразует выбранное пользователем значение riskToRewardRatio в числовое значение.
Затем мы рассчитываем уровни стоп-лосса и тейк-профита.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... double stopLevel = NormalizeDouble(kagiData.localMinimum, Digits()); double stopDistance = NormalizeDouble(askPr - stopLevel, Digits()); double targetLevel = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits()); ... }
Уровень стоп-лосса устанавливается на последнем локальном минимуме структуры Kagi. Затем мы измеряем расстояние от точки входа до уровня стоп-лосса и присваиваем это значение переменной stopDistance. Уровень тейк-профита определяется с помощью множителя соотношения риска и прибыли.
Далее мы рассчитываем объем сделки.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... double volume = NormalizeDouble(lotSize, 2); if(lotSizeMode == MODE_AUTO){ double amountAtRisk = (riskPerTradePercent / 100.0) * accountBalance; volume = amountAtRisk / (contractSize * stopDistance); volume = NormalizeDouble(volume, 2); } ... }
Если пользователь выбрал MODE_MANUAL для входного параметра lotSizeMode, мы используем значение, указанное для входного параметра lotSize. Если выбран AUTO_MODE, мы рассчитываем размер лота на основе заданного процента риска.
Следующий шаг заключается в отправке ордера на мгновенное открытие длинной позиции по рыночной цене.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){ Print("Error while opening a long position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; } ... }
Команда Trade.Buy() отправляет ордер. В случае сбоя функция выводит сообщение об ошибке для отладки. Когда сделка успешно выполняется, мы сохраняем номер тикета, цену входа, стоп-лосс, тейк-профит и размер лота в структуре tradeInfo.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){ ... }else{ MqlTradeResult result = {}; Trade.Result(result); tradeInfo.orderTicket = result.order; tradeInfo.type = action; tradeInfo.posType = positionType; tradeInfo.entryPrice = result.price; tradeInfo.takeProfitLevel = targetLevel; tradeInfo.stopLossLevel = stopLevel; tradeInfo.openTime = currentTime; tradeInfo.lotSize = result.volume; return true; } ... }
Функция OpenSel следует точно той же последовательности действий, но в обратном направлении. Она использует локальный максимум из структуры Kagi и рассчитывает уровни стоп-лосса и тейк-профита в обратном направлении.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenSel( const double bidPr){ ENUM_ORDER_TYPE action = ORDER_TYPE_SELL; ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL; datetime currentTime = TimeCurrent(); double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); double rewardValue = 1.0; switch(riskRewardRatio){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } double stopLevel = NormalizeDouble(kagiData.localMaximum, Digits()); double stopDistance = NormalizeDouble(stopLevel - bidPr, Digits()); double targetLevel = NormalizeDouble(bidPr - (rewardValue * stopDistance), Digits()); double volume = NormalizeDouble(lotSize, 2); if(lotSizeMode == MODE_AUTO){ double amountAtRisk = (riskPerTradePercent / 100.0) * accountBalance; volume = amountAtRisk / (contractSize * stopDistance); volume = NormalizeDouble(volume, 2); } if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){ Print("Error while opening a short position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); tradeInfo.orderTicket = result.order; tradeInfo.type = action; tradeInfo.posType = positionType; tradeInfo.entryPrice = result.price; tradeInfo.takeProfitLevel = targetLevel; tradeInfo.stopLossLevel = stopLevel; tradeInfo.openTime = currentTime; tradeInfo.lotSize = result.volume; return true; } return false; }
Прежде чем интегрировать торговую логику в переходы Kagi, нам сначала необходимо подготовить несколько вспомогательных функций. Эти функции позволят нашему советнику проверять, существует ли уже позиция на покупку или на продажу, а также дадут простой способ закрывать любые открытые позиции, относящиеся к этому советнику. Они помогают нам соблюдать очень важное правило: советник должен открывать только одну позицию одновременно. Если позиция уже открыта, советник не должен открывать новую, пока текущая не будет закрыта.
Эти функции следует разместить в проекте сразу под уже имеющимися служебными функциями. Добавляйте их по одному и уделите немного времени тому, чтобы понять, для чего нужна каждая из них, ведь позже они сыграют важную роль.
Чтобы проверить наличие активной длинной позиции, мы определяем следующую функцию:
//+------------------------------------------------------------------+ //| To verify whether this EA currently has an active buy position. | | //+------------------------------------------------------------------+ bool IsThereAnActiveBuyPosition(ulong magic){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ return true; } } } return false; }
Эта функция просматривает все открытые в данный момент позиции и проверяет, есть ли активная позиция на покупку, относящаяся к данному советнику. Для этого она сравнивает магический номер каждой позиции с магическим номером, присвоенным советнику. Если функция обнаруживает позицию на покупку с соответствующим магическим номером, она возвращает значение true. Если такая позиция не найдена, возвращается значение false. Позже мы будем использовать эту функцию, чтобы не допустить одновременного открытия советником двух позиций на покупку.
Чтобы проверить наличие активной короткой позиции, мы определяем следующую функцию:
//+------------------------------------------------------------------+ //| To verify whether this EA currently has an active sell position. | //+------------------------------------------------------------------+ bool IsThereAnActiveSellPosition(ulong magic){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ return true; } } } return false; }
Эта функция работает так же, как и предыдущая, но проверяет наличие позиции на продажу. Она проходит по всем открытым позициям, извлекает магический номер и тип позиции и возвращает значение true только в том случае, если обнаруживает позицию на продажу, принадлежащую данному советнику. Используя эту функцию при принятии торговых решений, советник не откроет еще одну позицию на продажу, если такая позиция уже открыта.
Нам также нужен способ закрыть все активные позиции, открытые нашим советником. Для этого мы определяем следующую служебную функцию:
//+------------------------------------------------------------------+ //| To close all position with a specified magic number | //+------------------------------------------------------------------+ void ClosePositionsByMagic(ulong magic) { for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetInteger(POSITION_MAGIC) == magic) { ulong positionType = PositionGetInteger(POSITION_TYPE); double volume = PositionGetDouble(POSITION_VOLUME); if (positionType == POSITION_TYPE_BUY) { Trade.PositionClose(ticket); } else if (positionType == POSITION_TYPE_SELL) { Trade.PositionClose(ticket); } } } } }
Эта функция обеспечивает простой и надежный способ закрытия всех позиций, открытых данным советником. Она просматривает все активные позиции и выбирает каждую из них по номеру тикета. Если она обнаруживает позицию, магический номер которой совпадает с номером, используемым нашим советником, она закрывает эту позицию независимо от того, является ли она позицией на покупку или на продажу. Эта функция очень полезна, когда требуется полный контроль над управлением позициями, особенно при сбросе советника или при реализации торговых режимов, которые разрешают только длинные позиции, только короткие позиции или и те, и другие.
Несмотря на то что пользователь может отключить автоматическую торговлю, советник должен по-прежнему отображать сигналы Kagi непосредственно на графике. Эта функция помогает трейдеру наглядно отслеживать поведение линии Kagi и понимать, где мог бы произойти потенциальный вход. Для этого мы создадим две простые служебные функции. Одна из них будет рисовать маркер покупки, а другая — маркер продажи. Каждый маркер размещается точно в тот момент времени и на том ценовом уровне, где возник сигнал.
Чтобы нарисовать маркер сигнала на покупку, добавьте следующую функцию сразу после служебных функций управления позициями:
//+------------------------------------------------------------------+ //| Draw a Buy Signal Marker | //+------------------------------------------------------------------+ void DrawBuySignalMarker(const datetime time, const double price){ //--- Create a unique name for the object string name = "BuySignal_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(MathRand()); //--- Create the buy arrow if(!ObjectCreate(0, name, OBJ_ARROW_UP, 0, time, price)){ Print("Failed to create Buy Signal marker. Error: ", GetLastError()); return; } //--- Styling (optional) ObjectSetInteger(0, name, OBJPROP_COLOR, yangLineColor); ObjectSetInteger(0, name, OBJPROP_WIDTH, 3); }
Эта функция наносит на график маркер сигнала на покупку. Сначала она генерирует уникальное имя для объекта. Это важно, поскольку каждый графический объект в MetaTrader 5 должен иметь собственное имя. Затем функция создаёт стрелку, направленную вверх, в выбранный момент времени и на выбранном ценовом уровне. Если объект создан успешно, функция задаёт цвет и ширину так, чтобы маркер был хорошо виден и соответствовал вашей теме Kagi. Этот маркер не влияет на торговлю. Его единственная цель — показать трейдеру, где возник сигнал на покупку.
Чтобы нарисовать маркер сигнала на продажу, мы определим следующую функцию:
//+------------------------------------------------------------------+ //| Draw a Sell Signal Marker | //+------------------------------------------------------------------+ void DrawSellSignalMarker(const datetime time, const double price){ //--- Create a unique name for the object string name = "SellSignal_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(MathRand()); //--- Create the sell arrow if(!ObjectCreate(0, name, OBJ_ARROW_DOWN, 0, time, price)){ Print("Failed to create Sell Signal marker. Error: ", GetLastError()); return; } //--- Styling (optional) ObjectSetInteger(0, name, OBJPROP_COLOR, yinLineColor); ObjectSetInteger(0, name, OBJPROP_WIDTH, 3); }
Эта функция работает так же, как функция для маркера покупки, но размещает стрелку, направленную вниз. Имя объекта также генерируется динамически, благодаря чему каждый сигнал сохраняется отдельно. Как только стрелка создана в нужный момент времени и на нужном ценовом уровне, функция задаёт её цвет и ширину так, чтобы она соответствовала стилю вашей линии Инь. Эти визуальные маркеры позволяют трейдеру отслеживать медвежьи сигналы Kagi даже в тех случаях, когда реальная торговля отключена.
Теперь, когда служебные функции отрисовки сигналов готовы, следующим шагом станет привязка сигналов Kagi к реальным торговым операциям. Цель здесь проста: всякий раз, когда советник обнаруживает подтверждённый разворот или продолжение Kagi, которые подходят под сигнал на покупку или продажу, мы выполняем соответствующий торговый блок, а затем ставим маркер на графике. Такой подход позволяет централизовать весь торговый процесс внутри функции ConstructKagiInRealTime, что облегчает читателям понимание того, как сигналы преобразуются в конкретные действия.
Чтобы процесс интеграции прошёл более гладко, важно понимать, что движок Kagi генерирует сигналы в разных точках в зависимости от структуры рынка. Некоторые сигналы возникают в результате явных разворотов, а другие — в результате продолжения движения после того, как разворот уже произошел. Независимо от того, откуда поступает сигнал, советник всегда должен выполнять одни и те же три шага:
- Закрыть любую активную позицию в противоположном направлении.
- Открыть новую позицию в направлении сигнала (если торговля разрешена).
- Разместить на графике визуальный маркер, чтобы обозначить сигнал.
Поскольку эти шаги встречаются несколько раз, блоки кода, используемые для сигналов на покупку, выглядят схоже; такой же единообразный подход применяется и к сигналам на продажу. Ниже мы покажем читателю, как эта логика вписывается в функцию, на примерах из реальных блоков Kagi.
Работа с сигналами на покупку
Когда структура Kagi фиксирует переход в восходящую фазу — будь то полный разворот или продолжение движения — мы запускаем логику обработки покупок. Каждый блок сигнала на покупку строится по одной и той же схеме.
- Закрыть все активные позиции на продажу — перед открытием длинной позиции мы убеждаемся, что не осталось открытых позиций на продажу. Если такая позиция есть, она немедленно закрывается.
- Открыть новую длинную позицию, если торговля разрешена — если включена автоматическая торговля и пользователь разрешил открытие длинных позиций, советник проверяет, открыта ли уже позиция на покупку. Если нет, он открывает ее по текущей цене Ask.
- Разместить на графике маркер сигнала на покупку — независимо от того, включена торговля или отключена, советник всегда размещает визуальный маркер сигнала Kagi на покупку, чтобы помочь трейдеру отслеживать сигнал прямо на графике.
Вот как выглядит блок сигнала на покупку внутри функции:
// Close a short position if it exists if(IsThereAnActiveSellPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a long position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_LONG){ if(!IsThereAnActiveBuyPosition(magicNumber)){ OpenBuy(askPrice); } } } //--- Render a buy signal (up) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice);
Этот же блок будет повторяться во всех ситуациях, когда логика Kagi формирует действительный пробой вверх или разворот.
Работа с сигналами на продажу
Сигналы на продажу зеркально повторяют сигналы на покупку, но работают в противоположном направлении. Всякий раз, когда ценовая структура подтверждает разворот вниз или продолжение движения вниз, мы действуем следующим образом:
- Закрыть любую активную длинную позицию — советник сначала проверяет, открыта ли длинная позиция. Если такая позиция найдена, советник закрывает её, чтобы избежать противоположной рыночной экспозиции.
- Открыть новую короткую позицию, если позволяют условия — если торговля включена и пользователь разрешил открытие коротких позиций, советник открывает ордер на продажу по текущей цене Bid — при условии, что активной короткой позиции ещё нет.
- Добавить на график маркер продажи — маркер наносится на последнем завершённом баре, чтобы точно показать, где произошёл разворот или продолжение по Kagi.
Ниже приведён пример блока сигнала на продажу:
// Close a long position if it exists if(IsThereAnActiveBuyPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a short position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_SHORT){ if(!IsThereAnActiveSellPosition(magicNumber)){ OpenSel(bidPrice); } } } //--- Render a sell signal (down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice);
Как и блок покупки, этот шаблон продажи используется во всех частях движка Kagi, где обнаруживается нисходящий структурный пробой.
Внутри функции ConstructKagiInRealTime алгоритм Kagi обновляет своё состояние каждый раз, когда формируется новый бар. Во время этого обновления несколько условий проверяют, прошла ли цена достаточно далеко, чтобы пробить текущую линию Kagi или развернуть тренд. Каждое из этих условий является потенциальным сигналом на покупку или продажу.
Например:
- Когда в рамках восходящего тренда цена пробивает вниз линию Ян, возникает сигнал на продажу.
- Когда в рамках нисходящего тренда цена пробивает вверх линию Инь, возникает сигнал на покупку.
- Когда после разворота цена уверенно продолжает движение, может появиться сигнал продолжения, который трактуется так же, как и обычный разворот.
В каждой из этих точек мы просто вставляем соответствующий торговый блок — на покупку или на продажу.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi in real time | //+------------------------------------------------------------------+ void ConstructKagiInRealTime(double bidPr, double askPr){ if(IsNewBar(_Symbol, kagiTimeframe, kagiData.lastBarOpenTime)){ ... //--- Handle a complex reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum)){ // Close a long position if it exists if(IsThereAnActiveBuyPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a short position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_SHORT){ if(!IsThereAnActiveSellPosition(magicNumber)){ OpenSel(bidPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawBendTop (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMinimum, yangLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMinimum, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMinimum = currentClosePrice; kagiData.isDowntrend = true; kagiData.isUptrend = false; kagiData.isYang = false; kagiData.isYin = true; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum)){ // Close a short position if it exists if(IsThereAnActiveSellPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a long position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_LONG){ if(!IsThereAnActiveBuyPosition(magicNumber)){ OpenBuy(askPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMaximum, yinLineColor); DrawYangLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMaximum, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMaximum = currentClosePrice; kagiData.isDowntrend = false; kagiData.isUptrend = true; kagiData.isYang = true; kagiData.isYin = false; } ... //--- Handle a complex continuation after reversal if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum))){ // Close a long position if it exists if(IsThereAnActiveBuyPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a short position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_SHORT){ if(!IsThereAnActiveSellPosition(magicNumber)){ OpenSel(bidPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor); DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; kagiData.isYang = false; kagiData.isYin = true; } if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum))){ // Close a short position if it exists if(IsThereAnActiveSellPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a long position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_LONG){ if(!IsThereAnActiveBuyPosition(magicNumber)){ OpenBuy(askPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor); DrawYangLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; kagiData.isYang = true; kagiData.isYin = false; } ... //--- Handle a weird scenario if(kagiData.isUptrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ // Close a short position if it exists if(IsThereAnActiveSellPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a long position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_LONG){ if(!IsThereAnActiveBuyPosition(magicNumber)){ OpenBuy(askPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.isYin = false; kagiData.isYang = true; kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ // Close a long position if it exists if(IsThereAnActiveBuyPosition(magicNumber)){ ClosePositionsByMagic(magicNumber); Sleep(50); } //--- Open a short position if allowed if(enableTrading){ if(direction == TRADE_BOTH || direction == ONLY_SHORT){ if(!IsThereAnActiveSellPosition(magicNumber)){ OpenSel(bidPrice); } } } //--- Render a sell signal(down) arrow datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1); double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1); DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice); if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.isYang = false; kagiData.isYin = true; kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } } }
Проектирование и реализация трейлинг-стопа
Трейлинг-стоп помогает защитить прибыль, когда цена движется в вашу пользу. В этом советнике трейлинг-стоп является необязательным. Пользователь может включить или отключить трейлинг-стоп с помощью входного параметра enableTrailingStop. При включении этой функции советник корректирует стоп-лосс в три последовательных этапа по мере того, как цена достигает заранее заданных пороговых значений между точкой входа и уровнем тейк-профита. Цель состоит в том, чтобы поэтапно фиксировать прибыль, при этом оставляя позиции возможность продолжать движение.
В логике трейлинга в качестве базового значения используется расстояние между точкой входа и уровнем тейк-профита. Мы делим это расстояние на четыре, чтобы получить шаг трейлинга. Затем на основе цены входа рассчитываются пороговые уровни. Для длинной позиции пороговые уровни следующие:
- Цена входа плюс один шаг трейлинга.
- Цена входа плюс два шага трейлинга.
- Цена входа плюс три шага трейлинга.
Когда цена пересекает первый порог, уровень стоп-лосса перемещается вверх на один шаг трейлинга. Если цена пересекает второй порог, уровень стоп-лосса снова перемещается; то же самое происходит и при пересечении третьего порога. Для коротких позиций действует тот же принцип, но в обратном направлении.
Чтобы поддерживать состояние трейлинга в порядке, мы используем небольшую структуру для хранения трёх уровней и соответствующих уровней стоп-лосса. Кроме того, мы сохраняем три логических флага, которые указывают, была ли уже выполнена корректировка на соответствующем уровне. Это предотвращает повторные изменения, когда цена пересекает уровень туда и обратно.
Разместите эту структуру рядом с другими структурами данных:
struct MqlTrailingStop { double level1; double level2; double level3; double stopLevel1; double stopLevel2; double stopLevel3; bool isLevel1Active; bool isLevel2Active; bool isLevel3Active; };
Создайте её экземпляр как глобальную переменную:
//--- Instantiate the trailing stop structure
MqlTrailingStop trailingStop;
Нам также понадобится массив для хранения последних минутных цен закрытия. Это позволяет надежно определять пересечения уровней.
//--- To store minutes data double closePriceMinutesData [];
Внутри OnInit задайте массив минутных данных как таймсерию.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Array Set As Series ... ArraySetAsSeries(closePriceMinutesData, true); ... return INIT_SUCCEEDED; }
Затем заполняйте его заново на каждом тике. Добавьте вызов копирования после обновления цен Bid и Ask в OnTick.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Get some minutes data if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){ Print("Error while copying minutes datas ", GetLastError()); return; } ... }
Использование нескольких последних баров помогает избежать ложных срабатываний, вызванных шумом одного тика.
Чтобы определить, пересекла ли цена пороговый уровень, мы добавим две небольшие вспомогательные функции. Они сравнивают две последние цены закрытия в минутном массиве и возвращают true, когда происходит пересечение.
//+------------------------------------------------------------------+ //| To detect a crossover at a given price level | //+------------------------------------------------------------------+ bool IsCrossOver(const double price, const double &closePriceMinsData[]){ if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){ return true; } return false; } //+------------------------------------------------------------------+ //| To detect a crossunder at a given price level | //+------------------------------------------------------------------+ bool IsCrossUnder(const double price, const double &closePriceMinsData[]){ if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){ return true; } return false; }
Эти функции просты и надежны. Они показывают, что рынок перешел с одной стороны уровня на другую по минутным ценам закрытия.
Когда советник открывает позицию, мы вычисляем пороговые уровни трейлинга и целевые уровни стопа и сохраняем их в структуре trailingStop. Сделайте это сразу после успешного выполнения ордера.
Для длинной позиции мы вычисляем:
- targetDistance равен takeProfit минус цена входа.
- trailingStep равен значению targetDistance, деленному на четыре
- level1 равен цене входа плюс trailingStep
- level2 равен level1 плюс trailingStep
- level3 равен level2 плюс trailingStep
- stopLevel1 равен исходному уровню стопа плюс trailingStep
- stopLevel2 равен stopLevel1 плюс trailingStep
- stopLevel3 равен stopLevel2 плюс trailingStep
Установите для трех флагов типа boolean значение false, чтобы уровни могли сработать.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenBuy(const double askPr){ ... if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){ Print("Error while opening a long position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ ... //--- Refill the trailing Stop struct double targetDistance = targetLevel - askPr; double trailingStep = NormalizeDouble(targetDistance / 4, Digits()); trailingStop.level1 = NormalizeDouble(askPr + trailingStep, Digits()); trailingStop.level2 = NormalizeDouble(trailingStop.level1 + trailingStep, Digits()); trailingStop.level3 = NormalizeDouble(trailingStop.level2 + trailingStep, Digits()); trailingStop.stopLevel1 = NormalizeDouble(stopLevel + trailingStep, Digits()); trailingStop.stopLevel2 = NormalizeDouble(trailingStop.stopLevel1 + trailingStep, Digits()); trailingStop.stopLevel3 = NormalizeDouble(trailingStop.stopLevel2 + trailingStep, Digits()); trailingStop.isLevel1Active = false; trailingStop.isLevel2Active = false; trailingStop.isLevel3Active = false; return true; } return false; }
Для короткой позиции примените ту же логику зеркально, но вычитайте шаги из уровней входа и стопа.
//+------------------------------------------------------------------+ //| Function used to open a market buy order. | //+------------------------------------------------------------------+ bool OpenSel( const double bidPr){ ... if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){ Print("Error while opening a short position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ ... //--- Refill the trailing Stop struct double targetDistance = bidPr - targetLevel; double trailingStep = NormalizeDouble(targetDistance / 4, Digits()); trailingStop.level1 = NormalizeDouble(bidPr - trailingStep, Digits()); trailingStop.level2 = NormalizeDouble(trailingStop.level1 - trailingStep, Digits()); trailingStop.level3 = NormalizeDouble(trailingStop.level2 - trailingStep, Digits()); trailingStop.stopLevel1 = NormalizeDouble(stopLevel - trailingStep, Digits()); trailingStop.stopLevel2 = NormalizeDouble(trailingStop.stopLevel1 - trailingStep, Digits()); trailingStop.stopLevel3 = NormalizeDouble(trailingStop.stopLevel2 - trailingStep, Digits()); trailingStop.isLevel1Active = false; trailingStop.isLevel2Active = false; trailingStop.isLevel3Active = false; return true; } return false; }
Эта инициализация гарантирует, что советник точно знает, когда и куда переносить стоп-лосс по мере движения цены.
Теперь, когда уровни трейлинга и вспомогательные функции готовы, следующим шагом будет добавление самой функции ManageTrailingStop. Эту функцию следует разместить в основной части кода вашего советника, сразу под вспомогательными функциями трейлинга.
//+------------------------------------------------------------------+ //| To track price action and updates the trailing stop | //+------------------------------------------------------------------+ void ManageTrailingStop(){ int totalPositions = PositionsTotal(); //--- Loop through all open positions for(int i = totalPositions - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket != 0){ // Get some useful position properties ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); string symbol = PositionGetString (POSITION_SYMBOL); ulong magic = PositionGetInteger(POSITION_MAGIC); double targetLevel = PositionGetDouble(POSITION_TP); if(positionType == POSITION_TYPE_BUY ){ if(symbol == _Symbol && magic == magicNumber){ if(IsCrossOver(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){ Print("Error while trailing SL at level 1: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel1Active = true; } } if(IsCrossOver(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){ Print("Error while trailing SL at level 2: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel2Active = true; } } if(IsCrossOver(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){ Print("Error while trailing SL at level 3: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel3Active = true; } } } } if(positionType == POSITION_TYPE_SELL){ if(symbol == _Symbol && magic == magicNumber){ if(IsCrossUnder(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){ Print("Error while trailing SL at level 1: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel1Active = true; } } if(IsCrossUnder(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){ Print("Error while trailing SL at level 2: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel2Active = true; } } if(IsCrossUnder(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){ if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){ Print("Error while trailing SL at level 3: ", GetLastError()); Print(Trade.ResultRetcodeDescription()); Print(Trade.ResultRetcode()); }else{ trailingStop.isLevel3Active = true; } } } } } } }
Её задача проста: при поступлении каждого нового тика она проверяет, пересекла ли какая-либо открытая позиция один из пороговых уровней трейлинга, а затем соответствующим образом корректирует стоп-лосс.
Внутри этой функции советник сначала проходит по всем открытым на данный момент позициям. Для каждой из них советник получает важные сведения, такие как номер тикета, символ, магический номер, а также тип позиции — покупка или продажа. Советник будет управлять только теми позициями, которые относятся к символу текущего графика и были открыты с использованием магического номера советника. Это предотвращает случайное изменение позиций, открытых вручную или другими советниками.
Для длинных позиций советник использует вспомогательную функцию crossover, чтобы проверить, поднялась ли цена выше уровня 1, 2 или 3. Если цена пересекает уровень 1, а данная корректировка ещё не применялась, советник вызывает функцию Trade.PositionModify, чтобы переместить стоп-лосс на stopLevel1, и сразу же помечает уровень 1 как активный. Та же логика применима к второму и третьему уровням. Каждый уровень может сработать только один раз, что обеспечивает чёткую и предсказуемую траекторию трейлинга.
Для коротких позиций процесс аналогичен, но используется вспомогательная функция crossunder. Когда цена опускается ниже уровня 1, 2 или 3, советник пошагово корректирует стоп-лосс и помечает каждый уровень как завершённый. Такая симметричная обработка сохраняет согласованность логики трейлинга независимо от направления позиции.
Каждый вызов функции PositionModify проверяется на успешное выполнение. Если брокер отклоняет изменение, советник выводит сообщение на вкладку «Эксперты», помогая пользователю выявить такие проблемы, как минимально допустимое расстояние до стоп-уровня или недостаточная маржа.
Построение логики трейлинга таким структурированным образом гарантирует, что каждая корректировка происходит ровно один раз и только тогда, когда цена действительно выходит за пределы порогового уровня. Опираясь на данные закрытия минутных баров, функция избегает реакции на рыночный шум и защищает позицию контролируемым образом.
Как только функция трейлинга будет готова, останется только включить её внутри OnTick. После обновления котировок и выполнения построения графика Kagi в реальном времени вызовите менеджер трейлинга, если пользователь включил эту функцию:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Trigger the trailing stop functionality if(enableTrailingStop){ ManageTrailingStop(); } }
Теперь, когда все основные компоненты реализованы, мы завершили разработку полной версии нашего торгового советника. В следующем разделе мы сосредоточимся на тестировании системы, и после его завершения будем готовы подготовить советник к использованию в реальных условиях.
Тестирование советника
Теперь, когда все компоненты советника KagiTrader реализованы, следующим шагом станет проверка поведения системы в реальных рыночных условиях. Для этого я провёл полный бэктест по индексу Nikkei (JPN225) за период с января 2024 года по декабрь 2024 года. Этот тест позволяет проследить, как советник обрабатывает реальный ценовой поток, смену направления в структуре Kagi и корректировки трейлинг-стопа в разных рыночных фазах. Для этого теста были включены все функции советника, включая динамическое построение графика Kagi, правила управления позициями и необязательный трейлинг-стоп. Бэктест проводился с помощью Тестера стратегий MetaTrader 5 при стандартных настройках.
Полученная кривая средств продемонстрировала скромную, но стабильную прибыльность, что является хорошим признаком для системы, основанной в первую очередь на изменениях структуры и дисциплинированной логике стоп-лосса и тейк-профита. Советник продемонстрировал стабильную работу: он корректно открывал и закрывал позиции, соблюдал пороговые уровни трейлинга и сохранял устойчивость в разные периоды волатильности.
Ниже приведена кривая средств, построенная по результатам тестирования за весь год:


Этот результат подтверждает, что советник KagiTrader работает корректно и способен отрабатывать циклы тренда и отката без хаотичного поведения. Хотя приведённые здесь результаты относятся лишь к одному инструменту и одному таймфрейму, они служат надёжной отправной точкой, на основе которой трейдеры могут расширить тестирование на другие рынки или дополнительно оптимизировать параметры.
Чтобы вам было проще повторить этот тест, я приложил файл .set, использовавшийся при проведении бэктеста. Вы можете загрузить его непосредственно в Тестер стратегий, чтобы точно воспроизвести условия и параметры, использованные в данной оценке.
Помимо результатов бэктеста я также визуально проверил исполнение сигналов непосредственно на графике. Чтобы наглядно это продемонстрировать, я привёл два скриншота. На первом изображении видно, как советник открывает длинную позицию сразу же при переходе структуры Kagi из Инь в Ян, что подтверждает корректное распознавание бычьих разворотов.

На втором скриншоте показан противоположный сценарий — короткая позиция открывается при развороте Kagi из Ян обратно в Инь, как и ожидается в соответствии с нашим набором правил.

Эти примеры графиков позволяют нам с уверенностью утверждать, что советник реагирует на изменения тренда в соответствии с заложенной концепцией и что торговая логика работает надлежащим образом в условиях реального времени.
Заключение
В этой части серии мы превратили наш KagiTrader из простого интерпретатора сигналов в полноценный советник. Мы добавили визуальные маркеры сигналов, гибкие режимы торговли, более продуманный расчёт размера позиции, динамическое размещение стоп-лосса и структурированную трёхэтапную систему трейлинга. Каждая функция вводилась постепенно, чтобы вы могли проследить логику и понять, как она вписывается в общую картину.
К тому моменту, когда мы интегрировали все компоненты, советник уже мог в режиме реального времени распознавать развороты Kagi, ответственно открывать позиции и управлять ими, а также адаптировать стоп-уровни по мере изменения рынка. Наш бэктест на индексе Nikkei подтвердил, что система ведёт себя стабильно и реализует заложенную логику в точном соответствии с замыслом.
Теперь, когда советник готов и успешно протестирован, у вас есть прочная основа, которую можно дорабатывать, расширять или использовать для экспериментов — будь то добавление фильтров, улучшение управления капиталом или даже изучение альтернативных стилей построения графиков. Цель этой серии заключалась в том, чтобы научить вас практическим навыкам автоматизации, применимым в реальных условиях, и, дойдя до этого этапа, вы создали рабочий торговый инструмент с нуля.
Весь исходный код, использованный в этой статье, приведён ниже. В следующей таблице описан каждый файл и его назначение.
| Имя файла | Описание |
|---|---|
| KagiTraderPart1.mq5 | Исходный код из части 1, который мы расширили и улучшили в ходе работы над частью 2. |
| KagiTrader.mq5 | Исходный код для этой части. |
| KagiTrader.set | Файл .set, использованный для запуска бэктеста для части 2. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20378
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Очереди, списки и деревья (VIII)
Моделирование рынка: Position View (XVII)
Автоматизированный риск-менеджмент для прохождения челленджей проп-фирм
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования