Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi
Введение
Графики Kagi — это особый тип ценового графика, ориентированный на реальное движение рынка. Впервые такие графики появились в Японии много лет назад. В отличие от обычных свечных графиков, которые обновляются через фиксированные интервалы времени, график Kagi меняет направление только тогда, когда цена проходит заданную величину. Благодаря этому графики Kagi получаются очень «чистыми» и отлично отфильтровывают лишний шум.
Поскольку график Kagi не зависит от времени, он дает трейдеру ясную картину тренда. Когда цена растет, линия тянется вверх. Когда цена падает, линия движется вниз. Кроме того, график меняет стиль, когда спрос переходит в предложение. Такая простая структура помогает трейдерам видеть силу тренда без лишних отвлекающих деталей.
На обычном свечном графике рынок формирует новый бар через фиксированные интервалы времени. График Kagi ведет себя иначе. Он продолжается вверх только при росте цены и разворачивается вниз только тогда, когда цена падает на заданную величину разворота. Когда баланс сил смещается от предложения к спросу, линия Kagi меняет толщину или цвет. Именно такое наглядное поведение делает графики Kagi мощным инструментом анализа тренда.
Прежде чем приступать к построению собственной системы Kagi, полезно визуально сравнить график Kagi со стандартным свечным графиком. Ниже приведено простое концептуальное сравнение:
Традиционный свечной график

График Kagi

В этой серии из двух частей мы создадим полноценную торговую систему на основе Kagi в MQL5. В первой части мы создадим работающий в реальном времени, полностью функциональный и неперерисовывающийся график Kagi, который рисуется прямо в главном окне графика с помощью графических объектов MQL5, таких как OBJ_TREND. Это даст нам чистую и точную визуализацию Kagi, мгновенно реагирующую на движения рынка.
Во второй части мы расширим эту работу: будем выявлять сигналы Kagi и использовать их для автоматического открытия сделок. К концу серии вы точно поймете, как строятся графики Kagi, и получите собственный торговый движок на базе Kagi, работающий внутри MQL5.
Основы построения графика Kagi
Прежде чем перейти к коду, нужно получить простое представление о том, как строится график Kagi. В сети есть много подробных объяснений, включая известное описание в Wikipedia, поэтому здесь мы не будем повторять всю теорию. Вместо этого сосредоточимся на ключевых элементах, необходимых для правильной реализации графика в MQL5. Эти понятия определят, как мы будем рассчитывать направление, обнаруживать развороты и рисовать линии Kagi на графике.
Ценовые колебания и смена направления
График Kagi движется только тогда, когда цена проходит достаточно, чтобы сформировать значимое колебание. Мы отслеживаем два типа движения:
- Движение вверх.
- Движение вниз.
Когда цена продолжает идти в том же направлении, линия Kagi просто продлевается. Новое направление начинается только тогда, когда цена движется против текущего тренда больше выбранной величины разворота.
Плечи (локальные максимумы)
Плечо, также называемое локальным максимумом, появляется, когда цена достигает нового максимума, а затем разворачивается вниз. Этот максимум становится уровнем плеча. Позже, если цена поднимется выше этого плеча, это подтвердит бычью силу и может вызвать смену толщины линии с тонкой на толстую.
Талии (локальные минимумы)
Талия — это локальный минимум. Она появляется, когда цена формирует новый минимум, а затем разворачивается вверх. Этот минимум становится уровнем талии. Позже, если цена опустится ниже этой талии, это подтвердит медвежью силу и может вызвать смену толщины линии с толстой на тонкую.
Величина разворота
Величина разворота определяет, когда линия Kagi меняет направление. Мы позволим трейдеру выбрать один из вариантов:
- Фиксированная величина.
- Процент от цены.
Основная идея проста: если цена движется против текущего направления больше этой величины, линия Kagi рисует горизонтальный сегмент, а затем начинает новый вертикальный сегмент в противоположном направлении.
Линии Инь и Ян
В графиках Kagi используются два стиля линий, показывающие изменения спроса и предложения:
- Линия Инь (тонкая линия): показывает ослабление спроса или преобладание продавцов.
- Линия Ян (толстая линия): показывает усиление спроса или преобладание покупателей.
Переход от Инь к Ян происходит, когда линия Kagi пробивает предыдущее плечо вверх. Аналогично, переход от Ян к Инь происходит, когда линия Kagi пробивает предыдущую талию вниз.
Такие изменения стиля формируют классические торговые сигналы Kagi:
- Тонкая => Толстая = сигнал на покупку.
- Толстая => Тонкая = сигнал на продажу.
Структура проекта и входные параметры
В этом разделе мы подготовим основу проекта графика Kagi. Программа будет создана как советник. Это важно сразу уточнить, поскольку в MQL5 есть несколько типов программ: скрипты, индикаторы, советники и сервисы. Каждый тип служит своей цели. Для данного проекта лучше всего подходит именно советник, поскольку он дает полный контроль над работой с графическими объектами в главном окне графика. Мы будем использовать графический объект трендовой линии OBJ_TREND, чтобы рисовать сегменты Kagi прямо на ценовом графике. Это создает чистое и интерактивное отображение без пользовательских буферов индикатора. Кроме того, весь проект остается в одном файле.
Теперь можно начать писать код. Откройте MetaEditor 5 и создайте новый файл советника. Назовите его KagiTrader.mq5. Файл будет содержать стандартные обработчики событий, от которых зависит каждый советник: инициализацию, деинициализацию, обработчик тиков и обработчик торговых транзакций. Они задают структуру, в которой будет работать логика нашего графика Kagi. Ниже приведен минимальный каркас, на котором мы будем строить проект.
//+------------------------------------------------------------------+ //| KagiTrader.mq5 | //| Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian | //| https://www.mql5.com/en/users/chachaian | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #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(){ } //+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { } //+------------------------------------------------------------------+
Функция инициализации используется при запуске советника. В ней мы разместим логику настройки. Функция деинициализации вызывается при удалении советника. Позже она поможет нам очищать графические объекты с графика. Обработчик тиков будет обновлять график Kagi по мере изменения рыночных цен. Функция обработки торговых транзакций в первой части не требуется, но остается в структуре для полноты, поскольку во второй части мы используем ее при добавлении автоматической торговли.
Теперь, когда структура проекта готова, можно определить входные параметры. Эти настройки позволяют читателю управлять поведением графика Kagi. Также они упрощают настройку графика без редактирования кода.
Перед тем как перейти к определению входных параметров нашего советника Kagi, сначала нужно создать пользовательское перечисление, которое будет определять интерпретацию условий разворота. Графики Kagi меняют направление только тогда, когда цена разворачивается на значимую величину, а разные трейдеры могут предпочитать разные способы определения того, что считать «значимым».
Чтобы обеспечить такую гибкость, введем следующее перечисление:
//+------------------------------------------------------------------+ //| Custom Enumerations | //+------------------------------------------------------------------+ enum ENUM_KAGI_REVERSAL_TYPE { REVERSAL_BY_PERCENTAGE, REVERSAL_BY_PRICE_STEP };
Это перечисление дает нашему советнику два режима определения разворотов:
- REVERSAL_BY_PERCENTAGE
Линия Kagi развернется, когда цена пройдет против текущего направления заданный процент от текущей цены. Этот вариант адаптируется к рыночной волатильности, потому что порог разворота автоматически масштабируется вместе с ценой.
- REVERSAL_BY_PRICE_STEP
Здесь разворот происходит, когда цена проходит фиксированную величину разворота. Это делает поведение более равномерным и предсказуемым независимо от текущего уровня цены.
Ниже приведен раздел входных параметров с понятными пояснениями.
//+------------------------------------------------------------------+ //| User input variables | //+------------------------------------------------------------------+ input group "Information" input ENUM_TIMEFRAMES kagiTimeframe = PERIOD_M10; input ENUM_KAGI_REVERSAL_TYPE reversalType = REVERSAL_BY_PERCENTAGE; input double reversalValue = 4.0; input color yangLineColor = C'38,166,154'; input color yinLineColor = C'239,83,80'; input bool overlayKagi = true;
Ниже приведено объяснение каждого входного параметра:
- kagiTimeframe
Задает таймфрейм, используемый для расчета графика Kagi. Советник считывает ценовые данные с этого таймфрейма независимо от графика, открытого пользователем.
- reversalType
Определяет, как интерпретируется величина разворота. Трейдер может выбрать фиксированное значение или значение в процентах. Это обеспечивает гибкость в разных рыночных условиях.
- reversalValue
Задает величину разворота. В зависимости от выбранного типа разворота это может быть статическое число или процент.
- yangLineColor
Задает цвет для восходящих или бычьих сегментов Kagi. Эти сегменты представляют линии Ян.
- yinLineColor
Задает цвет для нисходящих или медвежьих сегментов Kagi. Эти сегменты представляют линии Инь.
- overlayKagi
Позволяет читателю решить, должен ли отображаться график Kagi. Если отключить эту настройку, советник перестанет рисовать сегменты Kagi. Это полезно при сравнении поведения графика или тестировании производительности.
Позже, по мере развития проекта, мы можем добавить больше входных параметров. Это могут быть толщина линии, смещения при рисовании или параметры очистки объектов. Пока перечисленных параметров достаточно, чтобы начать создавать первую рабочую версию нашего графика Kagi.
Переменные состояния графика Kagi и внутренняя структура данных
Перед реализацией логики построения Kagi нам нужен надежный способ хранить и обновлять внутреннее состояние нашего графика. График Kagi динамичен по своей природе: он постоянно реагирует на изменения цены, развороты, переходы тренда и переключения между линиями Инь и Ян. Чтобы управлять всеми этими движущимися частями, мы определим пользовательскую структуру, которая будет хранить все важные переменные, необходимые при расчете и рисовании.
Сразу после входных параметров мы вводим следующую структуру:
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ struct MqlKagiData{ double closePrice[]; datetime openTime[]; double referencePrice; datetime referenceTime; double localMaximum; double localMinimum; bool isUptrend; bool isDowntrend; bool isYang; bool isYin; int lookBackBars; datetime lastBarOpenTime; }; //--- Initialize the state container MqlKagiData kagiData;
Эта структура будет выполнять роль внутренней «памяти» нашего советника Kagi. Ниже приведено объяснение каждого поля и причины, по которой оно важно:
- closePrice[]
График Kagi строится по ценам закрытия, поэтому этот динамический массив будет хранить исторические цены закрытия, извлеченные с графика. Эти значения станут основой каждого сегмента Kagi.
- openTime[]
Чтобы рисовать линии на графике с помощью объектов вроде OBJ_TREND, нужны и ценовые, и временные координаты. Этот массив хранит соответствующие времена открытия всех исторических баров, чтобы каждый сегмент Kagi можно было правильно закрепить на графике.
- referencePrice
Переменная хранит последнюю значимую цену, использованную для построения текущей линии Kagi. Новые проверки выполняются только при закрытии каждого бара, и именно цены закрытия определяют, продолжается линия или происходит разворот.
- referenceTime
Подобно referencePrice, эта переменная хранит метку времени, связанную с последним обновлением структуры Kagi. Она помогает размещать изменения линии Kagi в нужный момент на графике.
- localMaximum
Это поле представляет последнее плечо — последний локальный максимум, достигнутый перед разворотом. Оно используется для оценки переходов от тонких линий (Инь) к толстым линиям (Ян), когда цена пробивает предыдущие максимумы.
- localMinimum
Здесь хранится последняя талия— последний локальный минимум, достигнутый перед разворотом цены вверх. Это важно для обнаружения обратных переходов к линиям Инь, когда цена падает ниже предыдущих минимумов.
- isUptrend
Простой булев флаг, показывающий, движется ли текущая линия Kagi вверх. Он поможет решить, продлевают ли входящие цены текущую линию или запускают разворот.
- isDowntrend
Подобно isUptrend, этот флаг подтверждает, что последний сегмент Kagi движется вниз. Наличие обоих флагов делает логику обновления очень ясной и явной.
- isYang
Линии Ян обозначают силу или спрос. Этот флаг становится true, когда цена поднимается выше последнего плеча. Он помогает определить, когда нужно рисовать толстые линии на графике.
- isYin
Линии Инь обозначают предложение или слабость. Этот флаг становится true, когда цена падает ниже последней талии. Он сообщает советнику, что линия должна быть тонкой.
- lookBackBars
Графикам Kagi не нужно отображать на экране всю историческую информацию. Эта переменная ограничивает видимую часть графика заданным числом последних баров. Ограничение истории повышает производительность и сокращает время начальной загрузки, при этом вся необходимая глубокая история по-прежнему рассчитывается внутри.
- lastBarOpenTime
Эта переменная хранит время открытия последнего обработанного бара. Позже она поможет определить появление нового бара, чтобы советник обновлялся только при необходимости.
После определения структуры и инициализации экземпляра kagiData у нас появляется хорошо организованный контейнер для всего внутреннего управления состоянием. Это позволяет остальной части реализации аккуратно и последовательно читать, обновлять и рисовать сегменты Kagi.
Инициализация состояния Kagi и загрузка исторических данных для обработки Kagi
При запуске советника нужно подготовить его внутреннее состояние и загрузить исторические данные, которые станут базой графика Kagi. Эта инициализация выполняется один раз и задает все, что требуется советнику перед началом рисования на графике.
Сначала зададим простые булевы флаги и основные параметры.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- Initialize global variables kagiData.isUptrend = false; kagiData.isDowntrend = false; kagiData.isYang = false; kagiData.isYin = false; kagiData.lookBackBars = 100; kagiData.lastBarOpenTime = 0; return INIT_SUCCEEDED; }
Мы отмечаем, что график пока не находится ни в восходящем, ни в нисходящем тренде. Также устанавливаем оба флага Инь и Ян в false потому что ни плечо, ни талия еще не подтверждены. Значение lookBackBars ограничивает количество последних баров, которые мы держим видимыми для рисования. Наконец, мы устанавливаем lastBarOpenTime в ноль, чтобы первый обработанный бар всегда определялся как новый.
Далее подготовим массивы, в которых будут храниться цены закрытия и времена открытия.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Array Set As Series ArraySetAsSeries(kagiData.closePrice, true); ArraySetAsSeries(kagiData.openTime, true); return INIT_SUCCEEDED; }
Вызов функции ArraySetAsSeries со значением true делает массивы упорядоченными так, что самый новый элемент находится по индексу ноль. Такой порядок соответствует тому, как MetaTrader 5 возвращает скопированную историю, и упрощает индексацию при обновлениях. Использование series-массивов также ускоряет копирование и доступ при обработке последних баров.
Перед копированием данных проверим, что доступен достаточный объем истории.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Get the total number of historical bars on both the lower and the higher timeframes int totalNumberOfHistoricalBarsOnKagiTimeframe = Bars(_Symbol, kagiTimeframe); if(totalNumberOfHistoricalBarsOnKagiTimeframe < 10){ return INIT_FAILED; } return INIT_SUCCEEDED; }
Функция Bars возвращает количество свечей, загруженных для выбранного таймфрейма и символа. Если баров слишком мало, мы останавливаем инициализацию и возвращаем статус ошибки. Это предотвращает последующие ошибки и не позволяет советнику работать с недостаточными данными.
Если истории достаточно, затем копируем цены закрытия.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Get the actual close and opening times int closePricesCount = CopyClose(_Symbol, kagiTimeframe, 0, totalNumberOfHistoricalBarsOnKagiTimeframe, kagiData.closePrice); if(closePricesCount == -1){ Print("Error while copying historical close prices ", GetLastError()); return INIT_FAILED; } return INIT_SUCCEEDED; }
CopyClose заполняет массив closePrice значениями закрытия баров с выбранного таймфрейма. Мы проверяем возвращенное количество. Если функция завершается с ошибкой, выводим ошибку и прерываем работу. Этот защитный шаг гарантирует, что советник не продолжит работу с неполными или отсутствующими ценовыми данными.
Затем таким же образом копируем времена открытия.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Get the actual close and opening times ... int openTimesCount = CopyTime(_Symbol, kagiTimeframe, 0, totalNumberOfHistoricalBarsOnKagiTimeframe, kagiData.openTime); if(openTimesCount == -1){ Print("Error while copying historical open times ", GetLastError()); return INIT_FAILED; } return INIT_SUCCEEDED; }
Эти временные метки необходимы, потому что графическим объектам, таким как OBJ_TREND, нужны и цена, и время. Снова проверяем возвращаемое значение и прерываем работу, если копирование не удалось.
На этом этапе у нас есть два согласованных series-массива. Каждый элемент с индексом ноль — это самый последний бар. При необходимости referencePrice и referenceTime теперь можно инициализировать из этих массивов. Мы будем использовать эти значения, чтобы построить начальные сегменты Kagi по истории до последнего завершенного бара.
Настройка внешнего вида графика
Перед построением графика Kagi нужно подготовить окно графика так, чтобы наши линии Kagi были хорошо видны. Стандартный вид графика MetaTrader 5 не идеален для рисования пользовательских графических объектов. Часто он имеет цветной фон, видимую сетку и свечи, которые могут скрывать или искажать нашу пользовательскую линию Kagi. Поэтому мы создаем отдельную функцию, которая приводит график к чистому и простому виду.
//+------------------------------------------------------------------+ //| This function configures the chart's appearance. | //+------------------------------------------------------------------+ bool ConfigureChartAppearance() { if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){ Print("Error while setting chart background, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){ Print("Error while setting chart grid, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_MODE, CHART_LINE)){ Print("Error while setting chart mode, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){ Print("Error while setting chart foreground, ", GetLastError()); return false; } return true; }
Функция ConfigureChartAppearance задает эти визуальные свойства. Сначала она меняет цвет фона графика на белый. Это дает чистую основу, на которой цветные линии Kagi читаются гораздо легче. Если действие не удалось, функция печатает сообщение об ошибке и возвращает false.
Затем она отключает сетку графика. Линии сетки могут мешать четкости структуры Kagi, поэтому их удаление помогает сохранить аккуратный вид. Если платформа не может применить эту настройку, функция снова останавливается и возвращает false.
После этого функция переводит график в режим линии. Поскольку мы будем рисовать график Kagi с помощью объектов OBJ_TREND, линейный график дает более чистый фон по сравнению со свечами или барами. Выделяться будут только наши собственные объекты. Если режим графика применить не удалось, функция возвращает false.
Наконец, функция устанавливает цвет переднего плана в черный. Цвет переднего плана управляет подписями осей и базовыми элементами графика. Черный цвет на белом фоне сохраняет хорошую читаемость и не мешает видимости линий Kagi. Если действие не удалось, функция останавливается и сообщает об ошибке.
Если все настройки выполнены успешно, функция возвращает true. Это сообщает советнику, что график готов к рисованию. Мы должны вызвать эту функцию до любого построения Kagi. Лучшее место для этого — самое начало функции OnInit Вызов выглядит так:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- To configure the chart's appearance if(!ConfigureChartAppearance()){ Print("Error while configuring chart appearance", GetLastError()); return INIT_FAILED; } ... return INIT_SUCCEEDED; }
Так график будет правильно подготовлен каждый раз при подключении советника. Это гарантирует, что график Kagi будет корректно отображаться с самого начала, и предотвращает ситуации, когда объекты рисования становится трудно увидеть.
Построение исторического графика Kagi
Перед построением исторического графика Kagi нам нужен набор вспомогательных функций, которые умеют рисовать отрезки линий на графике. График Kagi многократно меняет форму по мере движения цены. Он изгибается в точках разворота и утолщается в бычьи периоды. Поэтому каждый сегмент мы рисуем отдельным объектом OBJ_TREND.
Следующие функции выполняют всю работу по рисованию за нас. Они делают код чистым и удобным для чтения. Все четыре функции создают линию на графике по заданным координатам времени и цены. Каждая функция также задает цвет и толщину линии и гарантирует, что объект нельзя случайно выбрать или переместить. Все они возвращают true при успешном рисовании линии и false при ошибке.
//+------------------------------------------------------------------+ //| This function is used to draw a new yang line | //+------------------------------------------------------------------+ bool DrawYangLine(string line_name, datetime time1, double price1, datetime time2, double price2, color line_color = clrGreen, int line_width=5) { ResetLastError(); //--- Create a Yang Line if(!ObjectCreate(0, line_name, OBJ_TREND, 0, time1, price1, time2, price2)){ Print("Error while creating a Yin line: ", GetLastError()); return false; } //--- Set some vital object properties ObjectSetInteger(0, line_name, OBJPROP_COLOR, line_color); ObjectSetInteger(0, line_name, OBJPROP_WIDTH, line_width); ObjectSetInteger(0, line_name, OBJPROP_SELECTED, false); ObjectSetInteger(0, line_name, OBJPROP_SELECTABLE, false); ChartRedraw (0); return true; } //+------------------------------------------------------------------+ //| This function is used to draw a new yin line | //+------------------------------------------------------------------+ bool DrawYinLine(string line_name, datetime time1, double price1, datetime time2, double price2, color line_color = clrRed , int line_width=3) { ResetLastError(); //--- Create a Yin Line if(!ObjectCreate(0, line_name, OBJ_TREND, 0, time1, price1, time2, price2)){ Print("Error while creating a Yin line: ", GetLastError()); return false; } //--- Set some vital object properties ObjectSetInteger(0, line_name, OBJPROP_COLOR, line_color); ObjectSetInteger(0, line_name, OBJPROP_WIDTH, line_width); ObjectSetInteger(0, line_name, OBJPROP_SELECTED, false); ObjectSetInteger(0, line_name, OBJPROP_SELECTABLE, false); ChartRedraw (0); return true; } //+------------------------------------------------------------------+ //| This function is used to draw a bend top line | //+------------------------------------------------------------------+ bool DrawBendTop(string line_name, datetime time1, double price1, datetime time2, double price2, color line_color = clrGreen, int line_width=5) { ResetLastError(); //--- Create a Bend Top Line if(!ObjectCreate(0, line_name, OBJ_TREND, 0, time1, price1, time2, price2)){ Print("Error while creating a Bend Top line: ", GetLastError()); return false; } //--- Set some vital object properties ObjectSetInteger(0, line_name, OBJPROP_COLOR, line_color); ObjectSetInteger(0, line_name, OBJPROP_WIDTH, line_width); ObjectSetInteger(0, line_name, OBJPROP_SELECTED, false); ObjectSetInteger(0, line_name, OBJPROP_SELECTABLE, false); ChartRedraw (0); return true; } //+------------------------------------------------------------------+ //| This function is used to draw a bend bottom line | //+------------------------------------------------------------------+ bool DrawBendBottom(string line_name, datetime time1, double price1, datetime time2, double price2, color line_color = clrRed , int line_width=3) { ResetLastError(); //--- Create a Bend Bottom Line if(!ObjectCreate(0, line_name, OBJ_TREND, 0, time1, price1, time2, price2)){ Print("Error while creating a Bend Bottom line: ", GetLastError()); return false; } //-- Set some vital object properties ObjectSetInteger(0, line_name, OBJPROP_COLOR, line_color); ObjectSetInteger(0, line_name, OBJPROP_WIDTH, line_width); ObjectSetInteger(0, line_name, OBJPROP_SELECTED, false); ObjectSetInteger(0, line_name, OBJPROP_SELECTABLE, false); ChartRedraw (0); return true; }
- DrawYangLine
Эта функция рисует линию Ян. Линия Ян обозначает силу. Она используется при движении вверх. Функция создает трендовую линию, которая толще и обычно окрашена в цвет Ян, выбранный пользователем. Она принимает имя линии, две временные точки, две ценовые точки, а также необязательные настройки цвета и ширины. После создания линии график перерисовывается, чтобы обновление появилось сразу.
- DrawYinLine
Эта функция рисует линию Инь. Линия Инь обозначает слабость. Она используется при движении вниз. Она работает так же, как функция DrawYangLine. Главное отличие — цвет по умолчанию и толщина линии. Линии Инь обычно тоньше. Это сохраняет визуальный стиль традиционного графика Kagi.
- DrawBendTop
Эта функция рисует верхний изгиб структуры Kagi. Изгиб возникает, когда цена достигает нового максимума и разворачивается вниз. Линия верхнего изгиба формирует верхнюю часть этой точки разворота. Функция создает трендовую линию по заданным координатам и применяет правильный стиль изгиба. Параметры и внутренняя логика такие же, как в предыдущих функциях.
- DrawBendBottom
Эта функция рисует нижний изгиб структуры Kagi. Нижний изгиб появляется, когда цена достигает нового минимума и разворачивается вверх. Функция работает как остальные и создает аккуратный отрезок линии, отмечающий эту точку разворота.
Поскольку все четыре функции следуют одному шаблону, их использование в логике построения делает код простым и последовательным. Каждый сегмент графика Kagi будет рисоваться наиболее подходящей функцией в зависимости от направления тренда и типа точки разворота.
Каждый объект графика в MQL5 должен иметь собственное уникальное имя. Два объекта не могут иметь одинаковое имя. График Kagi содержит много небольших отрезков линий. По мере роста графика количество сегментов может стать очень большим. Непрактично хранить все имена объектов в массиве и проверять их вручную. Чтобы решить эту задачу, мы используем вспомогательную функцию, которая создает новое уникальное имя каждый раз, когда нужно нарисовать свежий сегмент.
Функция GenerateUniqueName строит имя на основе префикса и случайного числа. Это повышает вероятность того, что каждое имя будет отличаться от всех предыдущих. Функция выполняет цикл до тех пор, пока не получит имя, которого еще нет на графике.
//+------------------------------------------------------------------+ //| Function to generate a unique object name with a given prefix | //+------------------------------------------------------------------+ string GenerateUniqueName(string prefix) { int attempt = 0; string uniqueName; while(true) { uniqueName = prefix + IntegerToString(MathRand() + attempt); if(ObjectFind(0, uniqueName) < 0) break; attempt++; } return uniqueName; }
Функция работает следующим образом:
- Она начинает с пустого счетчика, равного нулю.
- Внутри цикла она создает имя-кандидат. Имя состоит из префикса, случайного числа и счетчика попыток.
- Затем она проверяет, существует ли на графике объект с таким именем.
- Если имя свободно, цикл останавливается, и функция возвращает новое имя.
- Если имя занято, счетчик попыток увеличивается, и цикл продолжается, пока не будет найдено свободное имя.
Такой простой подход гарантирует, что каждый новый создаваемый сегмент Kagi получит уникальный идентификатор. Он также сохраняет логику рисования чистой, потому что нам больше не нужно вручную отслеживать имена объектов.
Поскольку нашей функции GenerateUniqueName требуется строковый префикс, нам нужно фиксированное и надежное значение, которое можно использовать каждый раз при создании нового сегмента Kagi. Для этого мы определяем следующий макрос сразу под директивами свойств:
//+------------------------------------------------------------------+ //| Macros | //+------------------------------------------------------------------+ #define TRENDLINE "standardKagi"
Макрос TRENDLINE задает фиксированный префикс, который мы используем при генерации имен новых сегментов Kagi. Это дает единообразную отправную точку для всех имен объектов и предотвращает ошибки, которые могут возникнуть при многократном вводе одной и той же строки. Используя единый префикс во всем коде, мы делаем систему именования чище и надежнее, и она хорошо работает с функцией GenerateUniqueName, которая строит итоговое уникальное имя на основе этого префикса.
Теперь, когда базовые компоненты готовы, можно перейти к созданию первой функции отрисовки — той, которая отвечает за построение всего графика Kagi при инициализации. Эта функция обрабатывает все исторические бары, пошагово применяет нашу логику Kagi и рисует начальный набор линий на графике. Завершив этот этап, мы получим полностью сформированную структуру Kagi, которая будет служить базой перед началом обновлений в реальном времени. Назовем эту функцию ConstructKagiOnInitialization.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { }
Мы начинаем с установки начальной опорной цены и времени по самому старому доступному бару.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { //--- The very first historical bar serves as the initial reference point kagiData.referencePrice = kagiData.closePrice[ArraySize(kagiData.closePrice) - 1]; kagiData.referenceTime = kagiData.openTime [ArraySize(kagiData.openTime) - 1]; }
Это делает самый левый бар начальной точкой привязки для всех будущих сравнений. От этой опорной точки функция будет проходить вперед по истории и строить структуру Kagi.
Основная работа выполняется внутри цикла, который проходит от более старых баров к самому последнему завершенному бару.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ } }
Цикл пропускает самый новый бар с индексом ноль, потому что мы строим историю только до последнего закрытого бара. Каждый проход обрабатывает одно закрытие бара и его время открытия, рассматривая это закрытие как цену-кандидат для продолжения или разворота.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ //--- During every iteration, we record the current bar’s close price and open time. double currentClosePrice = kagiData.closePrice[i]; datetime currentOpenTime = kagiData.openTime[i]; } }
Для каждого бара мы рассчитываем величину разворота.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... double reversalAmount = 0.0; if(reversalType == 0){ reversalAmount = NormalizeDouble((reversalValue / 100.0) * kagiData.referencePrice, Digits()); } if(reversalType == 1){ reversalAmount = NormalizeDouble(reversalValue, Digits()); } } }
Если пользователь выбрал процентный режим, разворот равен заданному проценту от текущей referencePrice. Если выбран режим ценового шага, величина разворота равна заданному фиксированному значению. Оба значения нормализуются до точности символа.
Первое важное решение проверяет начало исходного тренда, когда состояние Kagi еще нейтрально.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... //--- Handle the initial execution when the EA is first attached to the chart. if(!kagiData.isUptrend && !kagiData.isDowntrend && !kagiData.isYang && !kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount)){ kagiData.isUptrend = true; kagiData.isYang = true; if(overlayKagi && i < kagiData.lookBackBars){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } if(!kagiData.isUptrend && !kagiData.isDowntrend && !kagiData.isYang && !kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount)){ kagiData.isDowntrend = true; kagiData.isYin = true; if(overlayKagi && i < kagiData.lookBackBars){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = currentClosePrice; kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; } } }
Если не установлены ни флаги направления, ни флаги стиля, и закрытие выходит выше опорной цены на величину разворота, функция запускает восходящий тренд и помечает линию как Ян. Если закрытие опускается ниже опорной цены на величину разворота, функция запускает нисходящий тренд и помечает линию как Инь.
Затем идут простые проверки продолжения.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... //--- Handle a normal continuation if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } } }
Когда текущее состояние Kagi — восходящая линия Ян, функция продлевает линию Ян только в том случае, если закрытие превышает опорную цену плюс разворот и одновременно обновляет последний записанный локальный максимум. Для нисходящего состояния Инь функция продлевает линию Инь только если закрытие падает ниже опорной цены минус разворот и одновременно пробивает последний записанный локальный минимум.
После этого функция обрабатывает обычные развороты, которые остаются внутри предыдущих экстремумов.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... //--- Handle a normal reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice >= kagiData.localMinimum)){ if(overlayKagi && i < kagiData.lookBackBars){ DrawBendTop(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isDowntrend = true; kagiData.isUptrend = false; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice <= kagiData.localMaximum)){ if(overlayKagi && i < kagiData.lookBackBars){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isDowntrend = false; kagiData.isUptrend = true; } } }
Если восходящий тренд выполняет условие разворота, но остается выше или равным localMinimum, рисуется верхний изгиб, и новый вертикальный сегмент начинается вниз. Симметричный блок обрабатывает противоположный случай, когда нисходящий тренд разворачивается вверх, но остается ниже или равным localMaximum.
Сложные развороты возникают, когда разворот выходит за пределы предыдущего локального экстремума.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... //--- Handle a complex reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum)){ if(overlayKagi && i < kagiData.lookBackBars){ 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)){ if(overlayKagi && i < kagiData.lookBackBars){ 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; } } }
В этом случае функция рисует изгиб, затем короткий сегмент до предыдущего экстремума и, наконец, новый сегмент, который продолжается за пределы экстремума. Так создается двухсегментное изменение, визуально представляющее разворот с пробоем предыдущего плеча или талии.
После разворотов код содержит много детализированных ветвей для продолжений и встречных разворотов.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi on Initialization | //+------------------------------------------------------------------+ void ConstructKagiOnInitialization() { ... for(int i = ArraySize(kagiData.closePrice) - 2; i > 0; i--){ ... //--- Handle a normal continuation after reversal if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice >= kagiData.localMinimum))){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; } if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice <= kagiData.localMaximum))){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; } //--- Handle a complex continuation after reversal if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum))){ if(overlayKagi && i < kagiData.lookBackBars){ 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))){ if(overlayKagi && i < kagiData.lookBackBars){ 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 normal counter-reversal if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice <= kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars ){ DrawBendTop(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isUptrend = true; kagiData.isDowntrend = false; } if(kagiData.isUptrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice >= kagiData.localMinimum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isUptrend = false; kagiData.isDowntrend = true; } //--- Handle a complex counter-reversal if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawBendTop(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMaximum = currentClosePrice; kagiData.isUptrend = true; kagiData.isDowntrend = false; } if(kagiData.isUptrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMinimum = currentClosePrice; kagiData.isDowntrend = true; kagiData.isUptrend = false; } //--- Handle a normal continuation after counter-reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice <= kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice >= kagiData.localMinimum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; } //--- Handle a complex continuation after counter-reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; kagiData.localMaximum = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi && i < kagiData.lookBackBars){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; kagiData.localMinimum = currentClosePrice; } //--- Handle a weird scenario if(kagiData.isUptrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi && i < kagiData.lookBackBars){ 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){ if(overlayKagi && i < kagiData.lookBackBars){ 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; } } }
Эти блоки покрывают такие случаи, как продолжение внутри предыдущих экстремумов, продолжение с выходом за экстремум, встречные развороты с повторной сменой направления и сложные встречные развороты, которые также задают новые локальные экстремумы.
Каждая ветвь обновляет одни и те же основные переменные состояния, чтобы внутренняя память оставалась согласованной. Все вызовы рисования защищены флагом overlayKagi и ограничением lookBackBars. Это означает, что функция рассчитывает всю историю Kagi для корректности, но отображает только самые последние сегменты, необходимые для показа. Имена объектов берутся из генератора уникальных имен, а каждый нарисованный сегмент использует подходящую вспомогательную функцию для Ян, Инь, верхнего или нижнего изгиба.
На протяжении цикла функция обновляет эти ключевые поля по мере выполнения условий. referencePrice перемещается к самой последней цене, определяющей последний сегмент, referenceTime записывает время разворота, когда он происходит, localMaximum и localMinimum фиксируют последнее плечо и талию, а булевы флаги отражают текущее направление и стиль.
Эти обновления гарантируют, что обновитель во время выполнения сможет продолжить работу из точного состояния. В конце цикла внутренний объект kagiData содержит полное состояние Kagi, представляющее всю обработанную историю до последнего закрытого бара. Если наложение включено, на графике отображаются графические объекты за последние lookBackBars баров.
Из этого подготовленного состояния функция обновления в реальном времени может продолжить работу и обрабатывать каждый новый закрытый бар с правильным контекстом.
//+------------------------------------------------------------------+ //| This function is used to construct Kagi in real time | //+------------------------------------------------------------------+ void ConstructKagiInRealTime(double bidPr, double askPr){ if(IsNewBar(_Symbol, kagiTimeframe, kagiData.lastBarOpenTime)){ double currentClosePrice = iClose(_Symbol, kagiTimeframe, 1); datetime currentOpenTime = iTime( _Symbol, kagiTimeframe, 1); double reversalAmount = 0.0; if(reversalType == 0){ reversalAmount = NormalizeDouble((reversalValue / 100.0) * kagiData.referencePrice, Digits()); } if(reversalType == 1){ reversalAmount = NormalizeDouble(reversalValue, Digits()); } //--- Handle a normal continuation if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.localMaximum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.localMinimum = currentClosePrice; kagiData.referencePrice = currentClosePrice; } //--- Handle a normal 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, currentClosePrice, yangLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isDowntrend = true; kagiData.isUptrend = false; } 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, currentClosePrice, yinLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isDowntrend = false; kagiData.isUptrend = true; } //--- 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; } 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; } //--- Handle a normal 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, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; } if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice <= kagiData.localMaximum))){ if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; } //--- 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; } 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; } //--- Handle a normal counter-reversal if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice <= kagiData.localMaximum){ if(overlayKagi){ DrawBendTop(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isUptrend = true; kagiData.isDowntrend = false; } if(kagiData.isUptrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice >= kagiData.localMinimum){ if(overlayKagi){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.isUptrend = false; kagiData.isDowntrend = true; } //--- Handle a complex counter-reversal if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi){ DrawBendTop(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor); DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yangLineColor); } kagiData.localMinimum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMaximum = currentClosePrice; kagiData.isUptrend = true; kagiData.isDowntrend = false; } if(kagiData.isUptrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi){ DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor); DrawYinLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, currentClosePrice, yinLineColor); } kagiData.localMaximum = kagiData.referencePrice; kagiData.referencePrice = currentClosePrice; kagiData.referenceTime = currentOpenTime; kagiData.localMinimum = currentClosePrice; kagiData.isDowntrend = true; kagiData.isUptrend = false; } //Handle a normal continuation after counter-reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice <= kagiData.localMaximum){ if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice >= kagiData.localMinimum){ if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; } //--- Handle a complex continuation after counter-reversal if(kagiData.isUptrend && kagiData.isYang && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){ if(overlayKagi){ DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yangLineColor); } kagiData.referencePrice = currentClosePrice; kagiData.localMaximum = currentClosePrice; } if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){ if(overlayKagi){ DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, currentClosePrice, yinLineColor); } kagiData.referencePrice = currentClosePrice; kagiData.localMinimum = currentClosePrice; } //--- 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; } 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 при закрытии нового бара. Она запускается только после обнаружения нового закрытого бара с помощью функции IsNewBar и сохраненного lastBarOpenTime. Функция считывает цену закрытия только что закрытого бара и время его открытия с выбранного таймфрейма Kagi с помощью iClose и iTime.
Затем она рассчитывает величину разворота тем же способом, что и процедура инициализации. Если режим разворота процентный, она вычисляет процент от текущей referencePrice. Если режим — ценовой шаг, используется фиксированное значение. Значение нормализуется до точности инструмента.
Основная логика повторяет исторический конструктор, но только для одного нового бара. Она проверяет продолжение, обычный разворот, сложный разворот, встречный разворот и связанные случаи продолжения. Каждое подходящее условие может нарисовать один или несколько объектов с помощью вспомогательных функций рисования и GenerateUniqueName для имен объектов. Рисование выполняется только если входной параметр overlayKagi имеет значение true.
После любого рисования функция обновляет внутреннее состояние Kagi. Она устанавливает referencePrice и referenceTime, когда создается новый сегмент или разворот. Она корректирует localMaximum и localMinimum, чтобы фиксировать плечи и талии. Она при необходимости переключает флаги направления и стиля isUptrend, isDowntrend, isYang и isYin.
Функция принимает текущие Bid и Ask как параметры, но для решений Kagi использует только цену закрытия закрытого бара. В конце выполнения внутреннее состояние готово к следующему новому бару, а график отражает последние изменения Kagi, если наложение включено.
Подключение функций к событиям советника
Теперь, когда оба конструктора готовы, нужно вызвать их из правильных обработчиков событий советника. Исторический конструктор вызывается один раз во время инициализации. Он строит полное состояние Kagi по истории и рисует начальные сегменты.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Construct Kagi On Initialization ConstructKagiOnInitialization(); return INIT_SUCCEEDED; }
На каждом тике мы считываем текущие рыночные цены, а затем вызываем конструктор реального времени. Мы передаем текущие Bid и Ask, потому что процедура реального времени принимает их как параметры. Сама логика Kagi использует для принятия решений закрытие последнего закрытого бара.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables double askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); double bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); //--- Construct Kagi In Real Time ConstructKagiInRealTime(bidPrice, askPrice); }
С этими вызовами советник один раз построит Kagi по истории, а затем будет поддерживать его в актуальном состоянии по мере закрытия новых баров.
Полный исходный код этого советника приложен в конце статьи. Если при прохождении руководства вы пропустили какой-либо фрагмент, откройте полный файл и последовательно просмотрите разделы. Полный файл содержит блок свойств, перечисления, входные параметры, структуру, инициализаторы, вспомогательные функции рисования, генератор уникальных имен, оба конструктора и привязку к событиям, которую мы только что описали.
Проверка советника: выдерживает ли логика Kagi практику?
Теперь, когда наш советник графика Kagi полностью собран, следующий шаг — запустить его на живом графике и убедиться, что все работает ожидаемым образом. Для демонстрации мы подключили советник к индексу Nikkei (JPN225) и загрузили файл конфигурации kagitrader.set, который поставляется вместе с проектом. После применения настроек мы запустили советник на графике, и результат стал виден сразу. Структура Kagi начала формироваться именно так, как задумано, плавно обновляясь по мере появления новых баров. Ниже приведен скриншот советника, работающего в реальном времени, где линии Kagi корректно нарисованы поверх ценового движения.

Заключение
В этой части проекта мы успешно построили полноценную основу функционального советника графика Kagi. Вы узнали, как подготовить окружение графика, генерировать уникальные имена объектов, управлять внутренним состоянием Kagi и строить график как по историческим данным, так и в реальном времени. Благодаря совместной работе этих компонентов у вас теперь есть полноценный инструмент визуализации Kagi, который точно отслеживает колебания тренда, развороты и переходы толщины линий. Это дает вам надежную графическую основу, на которую можно опираться при интерпретации рыночной структуры и принятии обоснованных торговых решений.
Подключив советник к живому символу и протестировав его с предоставленным файлом kagitrader.set, вы также убедились, что логика Kagi на практике работает корректно. Советник рисует аккуратные сегменты, обновляется на каждом новом баре и реагирует на движения цены строго по заданным правилам. На этом этапе у вас есть полностью рабочий движок графика Kagi, запущенный прямо внутри MetaTrader 5.
Во второй части мы пойдем дальше. Мы расширим систему торговой логикой, добавим элементы управления конфигурацией и интегрируем функции принятия решений, которые превратят этот графический советник в настоящий торговый инструмент. Цель следующего этапа — превратить график Kagi в полноценный рабочий инструмент: использовать его поведение для генерации сигналов, сопровождения входов и выходов и поддержки разработки автоматизированных стратегий.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20239
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
От начального до среднего уровня: События в объектах (II)
Автоматизация торговых стратегий с помощью MQL5 (Часть 47): Торговая система Nick Rypock Trailing Reverse (NRTR) с поддержкой хеджирования
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования