preview
Математика волатильности: Почему индикатор GRI достоин возвращения в ваш торговый терминал

Математика волатильности: Почему индикатор GRI достоин возвращения в ваш торговый терминал

MetaTrader 5Примеры |
183 1
Artyom Trishkin
Artyom Trishkin

Содержание



Введение

В современном трейдинге важно не только определять направление движения цены, но и понимать, насколько рынок "живой" или "хаотичный" в данный момент. Многие трейдеры сталкиваются с ситуацией, когда стандартные индикаторы не дают чёткого ответа: стоит ли ожидать сильного движения, или рынок находится в фазе затишья. В поисках простого и наглядного инструмента для оценки волатильности и "хаотичности" рынка можно обратить внимание на индикатор Gopalakrishnan Range Index (GRI), также известный как Range of Chaos Index (ROCI).

GRI был впервые опубликован в январе 2001 года в авторитетном журнале Technical Analysis of Stocks & Commodities (TASC). Его автор — Jayanthi Gopalakrishnan, известный аналитик и редактор TASC. В то время индикатор был предложен как простой способ количественно оценить "хаотичность" (размах) движения цены за определённый период. Несмотря на свою простоту и наглядность, GRI не получил широкой популярности и редко встречается в современных торговых платформах и стратегиях.

В истории технического анализа было множество инструментов, которые были созданы в эпоху, когда не было мощных компьютеров и сложных алгоритмов. Их отличает простота, прозрачность логики и универсальность. Такие индикаторы, как GRI, могут быть легко реализованы в современных торговых терминалах, а их идеи — адаптированы под новые рыночные условия. Иногда именно забытые инструменты становятся ключом к успеху, когда стандартные методы перестают давать желаемый результат.


Реализуем индикатор для MetaTrader 5

GRI измеряет, насколько сильно изменяется цена закрытия за выбранный период (ChaoticPeriod), нормируя этот диапазон по логарифмической шкале.

Формула расчёта индикатора:

GRI = log10(High(Close, N) - Low(Close, N)) / log10(N)

где:

  • High(Close, N) — максимальное значение цены закрытия за N баров,
  • Low(Close, N) — минимальное значение цены закрытия за N баров,
  • N — период расчёта (ChaoticPeriod).

Логика расчёта:

  1. Для каждого бара находим максимальное и минимальное значение цены закрытия за N последних баров.
  2. Вычисляем размах (разницу между максимумом и минимумом).
  3. Берём десятичный логарифм от размаха и делим на логарифм периода.
  4. Если размах равен нулю, индикатор возвращает 0.

На основании вышеприведённых формул и логики, создадим индикатор для MetaTrader 5:

//+------------------------------------------------------------------+
//|                                                          GRI.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

//--- plot GRI
#property indicator_label1  "GRI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

//--- input parameters
input(name="ChaoticPeriod") int  InpPeriod = 5; // Период расчёта

//--- indicator buffers
double         BufferGRI[];

//--- global variables
int            ExtPeriod;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferGRI,INDICATOR_DATA);
   ArraySetAsSeries(BufferGRI,true);                     // буфер как таймсерия
   
//--- Устанавливаем параметры индикатора
   ExtPeriod=(InpPeriod<2 ? 2 : InpPeriod);              // корректируем период расчёта
   string short_name=StringFormat("GRI(%d)",ExtPeriod);  // определяем короткое имя
   IndicatorSetString(INDICATOR_SHORTNAME,short_name);   // устанавливаем короткое имя
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits);        // точность отображения
   
//--- Всё успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//--- Массив close - как таймсерия
   ArraySetAsSeries(close,true);
   
//--- Проверка количества доступных баров
   if(rates_total<fmax(ExtPeriod,5))
      return 0;
      
//--- Проверка и расчёт количества просчитываемых баров
   int limit=rates_total-prev_calculated;

//--- Если первый запуск или изменения исторических данных
   if(limit>1)
     {
      limit=rates_total-ExtPeriod-1;            // расчёт начинаем от начала исторических данных
      ArrayInitialize(BufferGRI,EMPTY_VALUE);   // инициализируем буфер индикатора пустым значением
     }
   
//--- Основной цикл
   for(int i=limit;i>=0;i--)
     {
      //--- рассчитываем диапазон цен закрытия за ExtPeriod баров
      int mx=ArrayMaximum(close,i,ExtPeriod);
      int mn=ArrayMinimum(close,i,ExtPeriod);
      if(mx==WRONG_VALUE || mn==WRONG_VALUE)
         return 0;
      //--- максимальное и минимальное значения Close за период ExtPeriod
      double max=close[mx];
      double min=close[mn];
      double range = max-min;
      //--- рассчитаем и запишем в буфер значение "хаотичности" за ExtPeriod баров
      BufferGRI[i]=(range>0 ? MathLog(range)/MathLog((double)ExtPeriod) : 0);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Чем выше значение GRI, тем более "хаотичным" (волатильным) было движение цены за период. Если диапазон мал — GRI стремится к нулю.


Особенности интерпретации

При расчёте GRI, выполненном в представленном выше коде, значения индикатора могут быть как положительными, так и отрицательными. Это связано с тем, что при малых диапазонах цен (range <1) логарифм становится отрицательным. В результате шкала индикатора не ограничена снизу и зависит от волатильности инструмента и выбранного периода, что создаёт некоторые проблемы при автоматической интерпретации значений индикатора.

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

Для этого можно использовать логарифм сдвинутого диапазона:

GRI = log10(1 + High(Close, N) - Low(Close, N)) / log10(N)

При такой формуле расчёт индикатора будет таким:

//--- Основной цикл
   for(int i=limit;i>=0;i--)
     {
      //--- рассчитываем диапазон цен закрытия за ExtPeriod баров
      int mx=ArrayMaximum(close,i,ExtPeriod);
      int mn=ArrayMinimum(close,i,ExtPeriod);
      if(mx==WRONG_VALUE || mn==WRONG_VALUE)
         return 0;
      //--- максимальное и минимальное значения Close за период ExtPeriod
      double max=close[mx];
      double min=close[mn];
      double range = max-min;
      //--- рассчитаем и запишем в буфер значение "хаотичности" за ExtPeriod баров
      //BufferGRI[i]=(range>0 ? MathLog(range)/MathLog((double)ExtPeriod) : 0);  // Произвольный диапазон
      BufferGRI[i] = MathLog(1.0 + range) / MathLog((double)ExtPeriod);          // Диапазон значений от нуля и выше 
     }

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

Высокие значения GRI (резкие всплески на графике индикатора) указывают на то, что за выбранный период рынок был очень волатильным, происходили сильные движения цены. Это может быть признаком начала или продолжения тренда, выхода важных новостей, либо просто фазы высокой активности на рынке.

Низкие значения GRI (плоские участки, близкие к нулю) говорят о том, что рынок находится в состоянии консолидации, "затишья", когда цена движется в узком диапазоне. В такие моменты часто наблюдается отсутствие выраженного тренда.

Есть ещё один момент, который хочется отметить и исправить. Этот расчёт даёт слишком маленькие результирующие значения индикатора:

BufferGRI[i] = MathLog(1.0 + range) / MathLog((double)ExtPeriod);          // Диапазон значений от нуля и выше

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

BufferGRI[i] = (MathLog(1.0 + range)/MathLog((double)ExtPeriod))/_Point;   // Диапазон значений от нуля и выше

При таком расчёте индикатора и его использования в советнике, в настройках советника можно будет вводить значения в виде 10, 50, 100, 150 и так далее. Это удобнее ввода маленьких дробных значений.

Рассмотрим практические сценарии применения индикатора.

  • Фильтрация сигналов других индикаторов.
    GRI можно использовать как фильтр в трендовых стратегиях, например, открывать сделки только тогда, когда GRI превышает определённый уровень, что говорит о наличии волатильности и потенциала для движения.
  • Поиск точек входа и выхода.
    Всплеск GRI после длительного затишья может сигнализировать о начале нового движения — это может быть точкой входа. Наоборот, резкое падение GRI после сильного движения может говорить о завершении импульса — повод зафиксировать прибыль.
  • Определение рыночной фазы.
    Если GRI долгое время остаётся низким, рынок, скорее всего, находится во флэте. В такие периоды лучше избегать трендовых стратегий и использовать методы, рассчитанные на торговлю внутри диапазона.
  • Адаптация параметров торговых систем.
    Можно динамически менять параметры других индикаторов или советников в зависимости от текущего значения GRI, например, увеличивать стоп-лосс и тейк-профит при высокой волатильности и уменьшать их при низкой.

Индикатор имеет всего один настраиваемый параметр — период расчёта (ChaoticPeriod).

Короткий период (3–5) делает индикатор более чувствительным к локальным всплескам, длинный период (10–20) — сглаживает значения и показывает более глобальные изменения волатильности.

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

Для повышения точности сигналов необходимо использовать GRI в сочетании с трендовыми индикаторами и осцилляторами.
Не стоит использовать GRI как единственный источник сигналов — он показывает только степень "хаотичности", но не направление движения.
Тестируйте индикатор на истории и подбирайте параметры под ваш стиль торговли и инструмент.

Давайте попробуем разработать простую торговую систему с использованием этого индикатора, и протестируем её в советнике.


Тестируем индикатор в советнике

Для тестирования индикатора GRI создадим советник, который торгует по сигналам двух скользящих средних — TEMA и AMA. Идея ТС будет такой: TEMA показывает направление движения (вверх или вниз), а AMA фильтрует сигналы, подтверждая их положением цены относительно своей линии.

Индикатор GRI будет использоваться как фильтр состояния рынка: если по состоянию индикатора рынок активный (волатильность растёт или начинается новый импульс роста волатильности), то торговля разрешается. Если рынок переходит к падению волатильности, затухает, или волатильность падает несколько периодов подряд — торговля запрещена.

Советник будет работать по такой логике:

  • Если TEMA растёт и цена выше растущей AMA, это сигнал на покупку.
  • Если TEMA падает и цена ниже падающей AMA, это сигнал на продажу.
  • Перед открытием сделки проверяется состояние рынка по GRI. Если индикатор показывает рост или начало роста волатильности — позиция открывается в направлении скользящих средних. 
    Если волатильность падает или рынок во флэте — советник не торгует.
  • Стоп-лосс и тейк-профит рассчитываются автоматически по значениям индикаторов Bollinger Bands и ATR соответственно, либо используются заданные в пунктах значения.

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

Одновременно можно будет открывать позиции в обоих направлениях — как длинные, так и короткие. Советник не будет отслеживать флэт или тренд на рынке, смену текущего тренда, его наличие или отсутствие. Направление позиции будет определяться по двум скользящим средним, а решение о входе в рынок в направлении скользящих средних будет определяться по состоянию индикатора GRI (если фильтр разрешён).

Рассмотрим код советника полностью:

//+------------------------------------------------------------------+
//|                                              ExpGRI_AMA_TEMA.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <Arrays\ArrayLong.mqh>

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
//--- Состояния GRI
enum ENUM_GRI_STATE
  {
   GRI_STATE_UNKNOWN,                                                // Нет данных
   GRI_STATE_FLAT,                                                   // Низкая волатильность
   GRI_STATE_VOLATILITY_HIGH,                                        // Высокая волатильность
   GRI_STATE_GROWING,                                                // Волатильность растёт
   GRI_STATE_FALLING,                                                // Волатильность падает
   GRI_STATE_TURN_TO_GROW,                                           // Переход к росту волатильности
   GRI_STATE_TURN_TO_FALL                                            // Переход к снижению волатильности
  };
//--- Типы сигналов скользящих средних
enum ENUM_SIGNAL_TYPE
  {
   SIGNAL_TYPE_NONE,                                                 // Нет сигнала
   SIGNAL_TYPE_LONG,                                                 // Сигнал на покупку
   SIGNAL_TYPE_SHORT,                                                // Сигнал на продажу
   SIGNAL_TYPE_CLOSE,                                                // Сигнал на закрытие позиций
  };

//--- Структура позиций
struct SData
  {
   CArrayLong  list_tickets;                                         // Список тикетов открытых позиций
   double      total_volume;                                         // Общий объём открытых позиций
  };

//--- Структура данных позиций по типам
struct SDataPositions
  {
   SData       Buy;                                                  // Данные позиций Buy
   SData       Sell;                                                 // Данные позиций Sell
  }
Data;

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  DATA_COUNT        3                                         // Количество получаемых данных от индикаторов (3 и более)
#define  ENV_ATTEMPTS      3                                         // Количество попыток ожидания получения окружения
#define  ENV_WAIT_ATTEMPT  1000                                      // Количество миллисекунд ожидания обновления окружения
#define  SPREAD_MLTP       3                                         // Множитель спреда для дистанции стоп-приказов

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
//--- GRI
input int                  InpPeriodGRI      =  9;                   /* GRI calculation period                             */ // Период расчёта GRI
input bool                 InpUseGRI         =  true;                /* Use GRI filtering                                  */ // Использовать фильтрацию по GRI
input double               InpThresholdGRI   =  50.0;                /* GRI Volatility threshold                           */ // Порог волатильности GRI

//--- TEMA - указывает направление торговли
input int                  InpPeriodTEMA     =  14;                  /* TEMA calculation period                            */ // Период расчёта TEMA
input ENUM_APPLIED_PRICE   InpPriceTEMA      =  PRICE_CLOSE;         /* TEMA applied price                                 */ // Цена расчёта TEMA
input int                  InpShiftTEMA      =  0;                   /* TEMA shift                                         */ // Сдвиг линии TEMA

//--- AMA - фильтрует сигналы по положению цены выше/ниже
input int                  InpPeriodAMA      =  9;                   /* AMA calculation period                             */ // Период расчёта AMA
input int                  InpFastEmaAMA     =  2;                   /* AMA fast EMA period                                */ // Период быстрого EMA AMA
input int                  InpSlowEmaAMA     =  30;                  /* AMA slow EMA period                                */ // Период медленного EMA AMA
input ENUM_APPLIED_PRICE   InpPriceAMA       =  PRICE_CLOSE;         /* AMA applied price                                  */ // Цена расчёта AMA
input int                  InpShiftAMA       =  0;                   /* AMA shift                                          */ // Сдвиг линии AMA

//--- BB - установка стопов по значениям
input int                  InpPeriodBB       =  58;                  /* BB calculation period                              */ // Период расчёта BB
input double               InpDeviationBB    =  2.0;                 /* BB deviations                                      */ // Отклонения BB
input int                  InpShiftBB        =  0;                   /* BB shift                                           */ // Сдвиг BB
input ENUM_APPLIED_PRICE   InpPriceBB        =  PRICE_CLOSE;         /* BB applied price                                   */ // Цена расчёта BB

//--- ATR - расчёт величины тейк-профит по значению
input int                  InpPeriodATR      =  64;                  /* ATR calculation period                             */ // Период расчёта ATR

//--- Торговля
input double               InpVolume         =  0.1;                 /* Position volume                                    */ // Объем позиции
sinput ulong               InpDeviation      =  10;                  /* Slippage (in points)                               */ // Проскальзывание (в пунктах)
sinput ulong               InpMagic          =  123456;              /* Magic number                                       */ // Магик
input int                  InpStopLoss       =  -1;                  /* Stop loss (in points), 0 - none, -1 - half of BB   */ // Stop loss (в пунктах), 0 - отсутствует, -1 - половина BB
input int                  InpTakeProfit     =  -1;                  /* Take profit (in points), 0 - none, -1 - ATR value  */ // Take profit (в пунктах), 0 - отсутствует, -1 - значение ATR
input double               InpSLMltp         =  2.6;                 /* Stop loss size multiplier, if SL==-1               */ // Множитель размера Stop loss, если Stop loss==-1
input double               InpTPMltp         =  1.3;                 /* Take profit size multiplier, if TP==-1             */ // Множитель размера Take profit, если Take profit==-1

//+------------------------------------------------------------------+
//| Глобальные переменные                                            |
//+------------------------------------------------------------------+
CTrade   trade;                                                      // Объект торгового класса
int      handle_gri;                                                 // Хэндл индикатора GRI
int      handle_tema;                                                // Хэндл индикатора TEMA
int      handle_ama;                                                 // Хэндл индикатора AMA
int      handle_bb;                                                  // Хэндл индикатора BB
int      handle_atr;                                                 // Хэндл индикатора ATR

double   gri[DATA_COUNT]={};                                         // Массив значений GRI
double   tema[DATA_COUNT]={};                                        // Массив значений TEMA
double   ama[DATA_COUNT]={};                                         // Массив значений AMA
double   bb0[DATA_COUNT]={};                                         // Массив значений BB, буфер 0 (Upper)
double   bb1[DATA_COUNT]={};                                         // Массив значений BB, буфер 1 (Lower)
double   bb2[DATA_COUNT]={};                                         // Массив значений BB, буфер 2 (Middle)
double   atr[DATA_COUNT]={};                                         // Массив значений ATR
MqlRates prc[DATA_COUNT]={};                                         // Массив цен и времени

//--- GRI
int      period_gri;                                                 // Период расчёта GRI
//--- TEMA
int      period_tema;                                                // Период расчёта TEMA
//--- AMA
int      period_ama;                                                 // Период расчёта AMA
int      fast_ema_ama;                                               // Период быстрого EMA AMA
int      slow_ema_ama;                                               // Период медленного EMA AMA
//--- BB
int      period_bb;                                                  // Период расчёта BB
double   deviation_bb;                                               // Отклонения BB
int      shift_bb;                                                   // Сдвиг BB
//--- ATR
int      period_atr;                                                 // Период расчёта ATR

//---
double   lot;                                                        // Объём позиции
int      prev_total;                                                 // Количество позиций на прошлой проверке
string   program_name;                                               // Имя программы
bool     netto;                                                      // Признак нетто-счёта

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Если счёт не с типом хеджинг - ставим флаг и сообщаем о некорректной работе советника
   netto=false;
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
      Print("The advisor is designed for use on a hedging account. Correct operation on a netting account is not guaranteed.");
      netto=true;
     }
   
//--- Устанавливаем и корректируем входные переменные индикаторов
//--- GRI
   period_gri=(InpPeriodGRI<1 ? 5 : InpPeriodGRI);
//--- TEMA
   period_tema=(InpPeriodTEMA<1 ? 14 : InpPeriodTEMA);
//--- AMA
   period_ama=(InpPeriodAMA<1 ? 9 : InpPeriodAMA);
   fast_ema_ama=(InpFastEmaAMA<1 ? 2 : InpFastEmaAMA);
   slow_ema_ama=(InpSlowEmaAMA<1 ? 30 : InpSlowEmaAMA);
//--- BB
   period_bb=(InpPeriodBB<2 ? 20 : InpPeriodBB);
   deviation_bb=InpDeviationBB;
   shift_bb=InpShiftBB;
//--- ATR
   period_atr=(InpPeriodATR<1 ? 14 : InpPeriodATR);
   
//--- Инициализируем массивы значений индикаторов
   ArrayInitialize(gri,EMPTY_VALUE);
   ArrayInitialize(tema,EMPTY_VALUE);
   ArrayInitialize(bb0,EMPTY_VALUE);
   ArrayInitialize(bb1,EMPTY_VALUE);
   ArrayInitialize(bb2,EMPTY_VALUE);
   ArrayInitialize(atr,EMPTY_VALUE);
   ZeroMemory(prc);

//--- Создаём хэндлы индикаторов
//--- GRI
   handle_gri=iCustom(Symbol(),PERIOD_CURRENT,"GRI",period_gri);
   if(handle_gri==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create handle for custom GRI(%d) indicator",__FUNCTION__,period_gri);
      return INIT_FAILED;
     }
//--- TEMA
   handle_tema=iTEMA(Symbol(),PERIOD_CURRENT,period_tema,InpShiftTEMA,InpPriceTEMA);
   if(handle_tema==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create iTEMA(%d) handle",__FUNCTION__,period_tema);
      return INIT_FAILED;
     }
//--- AMA
   handle_ama=iAMA(Symbol(),PERIOD_CURRENT,period_ama,fast_ema_ama,slow_ema_ama,InpShiftAMA,InpPriceAMA);
   if(handle_ama==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create iAMA(%d,%d,%d) handle",__FUNCTION__,period_ama,fast_ema_ama,slow_ema_ama);
      return INIT_FAILED;
     }
//--- BB
   handle_bb=iBands(Symbol(),PERIOD_CURRENT,period_bb,InpShiftBB,InpDeviationBB,InpPriceBB);
   if(handle_bb==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create iBands(%d,%d,%.3f,%s) handle",__FUNCTION__,period_bb,shift_bb,deviation_bb,EnumToString(InpPriceBB));
      return INIT_FAILED;
     }
//--- ATR
   handle_atr=iATR(Symbol(),PERIOD_CURRENT,period_atr);
   if(handle_atr==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create iATR(%d) handle",__FUNCTION__,period_atr);
      return INIT_FAILED;
     }

//--- Имя программы и количество позиций на прошлой проверке
   program_name=MQLInfoString(MQL_PROGRAM_NAME);
   prev_total=0;
   
//--- Автоматическая установка типа заполнения
   trade.SetTypeFilling(GetTypeFilling());
//--- Установка магика
   trade.SetExpertMagicNumber(InpMagic);
//--- Установка проскальзывания
   trade.SetDeviationInPoints(InpDeviation);
//--- Установка лота с корректировкой введённого значения
   lot=CorrectLots(InpVolume);
   
//--- Всё успешно
   PrintFormat("%s::%s: Initialization was successful",program_name,__FUNCTION__);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Получаем в массивы данные трёх баров индикаторов и цен
   if(!CopyIndicatorsData() || !CopyPricesData())
      return;
   
//--- Заполняем списки тикетов позиций
   int positions_total=PositionsTotal();
   if(prev_total!=positions_total)
     {
      if(!FillingListTickets(Symbol(),InpMagic))
         return;
      prev_total=positions_total;
     }
   
//--- Получаем сигналы от индикаторов
   ENUM_SIGNAL_TYPE signal_tema=SignalTEMA();   // Направление вверх/вниз (SIGNAL_TYPE_LONG/SIGNAL_TYPE_SHORT)
   ENUM_SIGNAL_TYPE signal_ama =SignalAMA();    // Цена выше/ниже (SIGNAL_TYPE_LONG/SIGNAL_TYPE_SHORT)

//--- Сигналы должны совпадать у TEMA и AMA (TEMA - направление, AMA - положение цены (выше/ниже))
   ENUM_SIGNAL_TYPE signal=(signal_tema==signal_ama ? signal_tema : SIGNAL_TYPE_NONE);

//--- Если включена фильтрация по GRI
   if(InpUseGRI)
     {
      //--- Корректируем сигнал по состоянию GRI
      signal=SignalCorrectionByGRI(signal);
      //--- Корректируем сигнал по порогу волатильности GRI
      if(!IsHighVolatility(InpThresholdGRI) && signal!=SIGNAL_TYPE_CLOSE)
         signal=SIGNAL_TYPE_NONE;
     }
      
//--- Торгуем по сигналам
   TradeProcess(signal);   
  }
//+------------------------------------------------------------------+
//| Получает значения OHLCTV для трёх баров                          |
//+------------------------------------------------------------------+
bool CopyPricesData(void)
  {
   ResetLastError();
   if(CopyRates(Symbol(),PERIOD_CURRENT,0,DATA_COUNT,prc)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get price Open data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Получает значения GRI для трёх баров                             |
//+------------------------------------------------------------------+
bool CopyGRIData(void)
  {
   ResetLastError();
   if(CopyBuffer(handle_gri,0,0,DATA_COUNT,gri)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get GRI data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Получает значения TEMA для трёх баров                            |
//+------------------------------------------------------------------+
bool CopyTEMAData(void)
  {
   ResetLastError();
   if(CopyBuffer(handle_tema,0,0,DATA_COUNT,tema)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get TEMA data. Error %d", __FUNCTION__, GetLastError());
      return false;
     }
   return true;
  }  
//+------------------------------------------------------------------+
//| Получает значения TEMA для трёх баров                            |
//+------------------------------------------------------------------+
bool CopyAMAData(void)
  {
   ResetLastError();
   if(CopyBuffer(handle_ama,0,0,DATA_COUNT,ama)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get AMA data. Error %d", __FUNCTION__, GetLastError());
      return false;
     }
   return true;
  }  
//+------------------------------------------------------------------+
//| Получает значения BB для трёх баров                              |
//+------------------------------------------------------------------+
bool CopyBBData(void)
  {
   ResetLastError();
   if(CopyBuffer(handle_bb,UPPER_BAND,0,DATA_COUNT,bb0)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get BB Upper Line data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   if(CopyBuffer(handle_bb,LOWER_BAND,0,DATA_COUNT,bb1)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get BB Lower Line data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   if(CopyBuffer(handle_bb,BASE_LINE,0,DATA_COUNT,bb2)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get BB Base Line data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Получает значения ATR для трёх баров                             |
//+------------------------------------------------------------------+
bool CopyATRData(void)
  {
   ResetLastError();
   if(CopyBuffer(handle_atr,0,0,DATA_COUNT,atr)!=DATA_COUNT)
     {
      PrintFormat("%s: Failed to get ATR data. Error %d",__FUNCTION__,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Получает значения индикаторов для трёх баров                     |
//+------------------------------------------------------------------+
bool CopyIndicatorsData(void)
  {
   bool res=CopyTEMAData();   // Результат получения данных TEMA
   res &=CopyAMAData();       // Результат получения данных AMA
   res &=CopyGRIData();       // Результат получения данных GRI
   res &=CopyBBData();        // Результат получения данных BB
   res &=CopyATRData();       // Результат получения данных ATR
   return res;
  }
//+------------------------------------------------------------------+
//| Возвращает из массива цену Open по индексу таймсерии (0 - 2)     |
//+------------------------------------------------------------------+
double PriceOpen(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? 0 : prc[DATA_COUNT-index-1].open);
  }
//+------------------------------------------------------------------+
//|Возвращает из массива цену High по индексу таймсерии (0 - 2)|
//+------------------------------------------------------------------+
double PriceHigh(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? 0 : prc[DATA_COUNT-index-1].high);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива цену Low по индексу таймсерии (0 - 2)      |
//+------------------------------------------------------------------+
double PriceLow(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? 0 : prc[DATA_COUNT-index-1].low);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива цену Close по индексу таймсерии (0 - 2)    |
//+------------------------------------------------------------------+
double PriceClose(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? 0 : prc[DATA_COUNT-index-1].close);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива время бара по индексу таймсерии (0 - 2)    |
//+------------------------------------------------------------------+
datetime Time(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? 0 : prc[DATA_COUNT-index-1].time);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива данные GRI по индексу таймсерии (0 - 2)    |
//+------------------------------------------------------------------+
double GRI(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : gri[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива данные TEMA по индексу таймсерии (0 - 2)   |
//+------------------------------------------------------------------+
double TEMA(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : tema[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//| Возвращает из массива данные AMA по индексу таймсерии (0 - 2)    |
//+------------------------------------------------------------------+
double AMA(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : ama[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//|Возвращает из массива данные BB Upper по индексу таймсерии (0 - 2)|
//+------------------------------------------------------------------+
double BBUpper(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : bb0[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//|Возвращает из массива данные BB Lower по индексу таймсерии (0 - 2)|
//+------------------------------------------------------------------+
double BBLower(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : bb1[DATA_COUNT-index-1]);
  }
//+-------------------------------------------------------------------+
//|Возвращает из массива данные BB Middle по индексу таймсерии (0 - 2)|
//+-------------------------------------------------------------------+
double BBMiddle(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : bb2[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//| Возвращает половину ширины BB в пунктах                          |
//+------------------------------------------------------------------+
int HalfSizeBB(const int index)
  {
   double up=BBUpper(index);
   double dn=BBLower(index);
   if(up==EMPTY_VALUE || dn==EMPTY_VALUE)
      return 0;
   return (int)round(((up-dn)*0.5)/Point());
  }
//+------------------------------------------------------------------+
//| Возвращает из массива данные ATR по индексу таймсерии (0 - 2)    |
//+------------------------------------------------------------------+
double ATR(const int index)
  {
   return(index<0 || index>DATA_COUNT-1 ? EMPTY_VALUE : atr[DATA_COUNT-index-1]);
  }
//+------------------------------------------------------------------+
//| Состояние GRI                                                    |
//+------------------------------------------------------------------+
ENUM_GRI_STATE GRIState(void)
  {
//--- Получаем данные GRI
   double gri0=GRI(0);
   double gri1=GRI(1);
   double gri2=GRI(2);
//--- Ошибка получения данных - нет сигнала
   if(gri0==EMPTY_VALUE || gri1==EMPTY_VALUE || gri2==EMPTY_VALUE)
      return GRI_STATE_UNKNOWN;
      
//--- Волатильность растёт
   if(gri0>gri1 && gri1>gri2)
      return GRI_STATE_GROWING;
//--- Волатильность падает
   if(gri0<gri1 && gri1<gri2)
      return GRI_STATE_FALLING;
//--- Переход к росту (был спад/флэт, теперь рост)
   if(gri0>gri1 && gri1<=gri2)
      return GRI_STATE_TURN_TO_GROW;
//--- Переход к падению (был рост/флэт, теперь спад)
   if(gri0<gri1 && gri1>=gri2)
      return GRI_STATE_TURN_TO_FALL;
//--- Флэт
   return GRI_STATE_FLAT;   
  }
//+------------------------------------------------------------------+
//| Корректирует сигнал по состоянию GRI                             |
//+------------------------------------------------------------------+
ENUM_SIGNAL_TYPE SignalCorrectionByGRI(const ENUM_SIGNAL_TYPE signal_type) 
  {
//--- Текущий сигнал
   ENUM_SIGNAL_TYPE signal=signal_type;
//--- В зависимости от состояния GRI корректируем текущий сигнал
   ENUM_GRI_STATE   state =GRIState();
   switch(state)
     {
      //--- Переход к росту волатильности
      case GRI_STATE_TURN_TO_GROW :
        // Здесь сигнал без изменений, 
        // но можно как-то ещё обрабатывать начало роста
        break;
      
      //--- Волатильность растёт
      case GRI_STATE_GROWING :
        // Здесь сигнал без изменений, 
        // но можно как-то ещё обрабатывать постоянный рост
        break;
      
      //--- Высокая волатильность
      case GRI_STATE_VOLATILITY_HIGH :
        // Здесь сигнал без изменений, 
        // но можно как-то ещё обрабатывать высокую волатильность
        break;
      
      //--- Переход к снижению волатильности
      case GRI_STATE_TURN_TO_FALL :
        signal=SIGNAL_TYPE_NONE;    // Нет сигнала
        // Здесь просто нет сигнала, 
        // но можно как-то иначе обрабатывать это состояние,
        // например, здесь можно закрыть часть позиции или выставить стоп в безубыток,
        // либо сделать и то, и то
        break;
      
      //--- Волатильность падает
      case GRI_STATE_FALLING :
        signal=SIGNAL_TYPE_NONE;    // Нет сигнала
        // Здесь просто нет сигнала, 
        // но можно как-то иначе обрабатывать это состояние,
        // например, включить трейлинг-стоп или закрыть позиции:
        //signal=SIGNAL_TYPE_CLOSE;   // Сигнал на закрытие всех позиций (три бара подряд снижается волатильность)
        break;
      
      //--- Низкая волатильность
      case GRI_STATE_FLAT :
        signal=SIGNAL_TYPE_NONE;    // Нет сигнала
        // Здесь просто нет сигнала, 
        // но можно как-то иначе обрабатывать это состояние,
        // например, здесь можно выставлять отложенные ордера
        break;
      
      //--- GRI_STATE_UNKNOWN Ошибка получения данных данных
      default:
        signal=SIGNAL_TYPE_NONE; // Нет сигнала
        break;
     }
   return signal;
  }
//+------------------------------------------------------------------+
//| Превышение порога волатильности GRI                              |
//+------------------------------------------------------------------+
bool IsHighVolatility(double threshold)
  {
//--- Получаем данные GRI
   double gri0=GRI(0);
//--- Ошибка получения данных - false
   if(gri0==EMPTY_VALUE)
      return false;
   return(gri0>=threshold);
  }  
//+------------------------------------------------------------------+
//| Сигнал TEMA                                                      |
//+------------------------------------------------------------------+
ENUM_SIGNAL_TYPE SignalTEMA(void)
  {
   double tema0=TEMA(0);
   double tema1=TEMA(1);
//--- Ошибка - нет сигнала
   if(tema0==EMPTY_VALUE || tema1==EMPTY_VALUE)
      return SIGNAL_TYPE_NONE;

//--- Сигнал на покупку: TEMA растёт
   if(tema0>tema1)
      return SIGNAL_TYPE_LONG;

//--- Сигнал на продажу: TEMA падает
   if(tema0<tema1)
      return SIGNAL_TYPE_SHORT;

//--- Нет сигнала
   return SIGNAL_TYPE_NONE;
  }  
//+------------------------------------------------------------------+
//| Сигнал AMA                                                       |
//+------------------------------------------------------------------+
ENUM_SIGNAL_TYPE SignalAMA(void)
  {
   double ama0=AMA(0);
   double ama1=AMA(1);
   double price=PriceClose(0);
//--- Ошибка - нет сигнала
   if(ama0==EMPTY_VALUE || ama1==EMPTY_VALUE || price==0)
      return SIGNAL_TYPE_NONE;

//--- Сигнал на покупку: цена выше AMA, AMA растёт
   if(price>ama0 && ama0>ama1)
      return SIGNAL_TYPE_LONG;

//--- Сигнал на продажу: цена ниже AMA, AMA падает
   if(price<ama0 && ama0<ama1)
      return SIGNAL_TYPE_SHORT;

//--- Нет сигнала
   return SIGNAL_TYPE_NONE;
  }
//+------------------------------------------------------------------+
//| Возвращает тип исполнения ордера, равный type,                   |
//| если он доступен на символе, иначе - корректный вариант          |
//| https://www.mql5.com/ru/forum/170952/page4#comment_4128864       |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING GetTypeFilling(const ENUM_ORDER_TYPE_FILLING type=ORDER_FILLING_RETURN)
  {
   const ENUM_SYMBOL_TRADE_EXECUTION exe_mode=(ENUM_SYMBOL_TRADE_EXECUTION)::SymbolInfoInteger(Symbol(),SYMBOL_TRADE_EXEMODE);
   const int filling_mode=(int)::SymbolInfoInteger(Symbol(),SYMBOL_FILLING_MODE);

   return((filling_mode==0 || (type>=ORDER_FILLING_RETURN) || ((filling_mode &(type+1))!=type+1)) ?
          (((exe_mode==SYMBOL_TRADE_EXECUTION_EXCHANGE) || (exe_mode==SYMBOL_TRADE_EXECUTION_INSTANT)) ?
          ORDER_FILLING_RETURN :((filling_mode==SYMBOL_FILLING_IOC) ? ORDER_FILLING_IOC : ORDER_FILLING_FOK)) : type);
  }
//+------------------------------------------------------------------+
//| Возвращает корректный лот                                        |
//+------------------------------------------------------------------+
double CorrectLots(const double lots,const bool to_min_correct=true)
  {
   double min=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   double max=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX);
   double step=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP);
   return(to_min_correct ? VolumeRoundToSmaller(lots,min,max,step) : VolumeRoundToCorrect(lots,min,max,step));
  }
//+------------------------------------------------------------------+
//| Возвращает ближайший корректный лот                              |
//+------------------------------------------------------------------+
double VolumeRoundToCorrect(const double volume,const double min,const double max,const double step)
  {
   return(step==0 ? min : fmin(fmax(round(volume/step)*step,min),max));
  }
//+------------------------------------------------------------------+
//| Возвращает ближайший в меньшую сторону корректный лот            |
//+------------------------------------------------------------------+
double VolumeRoundToSmaller(const double volume,const double min,const double max,const double step)
  {
   return(step==0 ? min : fmin(fmax(floor(volume/step)*step,min),max));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг не превышения общего объёма на счёте             |
//+------------------------------------------------------------------+
bool CheckLotForLimitAccount(const ENUM_POSITION_TYPE position_type,const double volume)
  {
   double lots_limit=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT);
   if(lots_limit==0)
      return true;
   double total_volume=(position_type==POSITION_TYPE_BUY ? Data.Buy.total_volume : Data.Sell.total_volume);
   return(total_volume+volume<=lots_limit);
  }
//+------------------------------------------------------------------+
//| Возвращает корректный StopLoss относительно StopLevel            |
//+------------------------------------------------------------------+
double CorrectStopLoss(const ENUM_POSITION_TYPE position_type,const int stop_loss)
  {
   if(stop_loss==0)
      return 0;
   double pt=Point();
   double price=(position_type==POSITION_TYPE_BUY ? SymbolInfoDouble(Symbol(),SYMBOL_ASK) : SymbolInfoDouble(Symbol(),SYMBOL_BID));
   int lv=StopLevel(), dg=Digits();
   return(position_type==POSITION_TYPE_BUY   ?  NormalizeDouble(fmin(price-lv*pt,price-stop_loss*pt),dg) :
                                                NormalizeDouble(fmax(price+lv*pt,price+stop_loss*pt),dg));
  }
//+------------------------------------------------------------------+
//| Возвращает корректный TakeProfit относительно StopLevel          |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const ENUM_POSITION_TYPE position_type,const int take_profit)
  {
   if(take_profit==0)
      return 0;
   double pt=Point();
   double price=(position_type==POSITION_TYPE_BUY ? SymbolInfoDouble(Symbol(),SYMBOL_ASK) : SymbolInfoDouble(Symbol(),SYMBOL_BID));
   int lv=StopLevel(), dg=Digits();
   return(position_type==POSITION_TYPE_BUY   ?  NormalizeDouble(fmax(price+lv*pt,price+take_profit*pt),dg) :
                                                NormalizeDouble(fmin(price-lv*pt,price-take_profit*pt),dg));
  }
//+------------------------------------------------------------------+
//| Возвращает рассчитанный StopLevel                                |
//+------------------------------------------------------------------+
int StopLevel(void)
  {
   int sp=(int)SymbolInfoInteger(Symbol(),SYMBOL_SPREAD);
   int lv=(int)SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL);
   return(lv==0 ? sp*SPREAD_MLTP : lv);
  }
//+------------------------------------------------------------------+
//| Возвращает "неопределённое" состояние торгового окружения        |
//+------------------------------------------------------------------+
bool IsUncertainStateEnv(const string symbol_name,const ulong magic_number)
  {
//--- В тестере сосотояние окружения всегда корректно
   if(MQLInfoInteger(MQL_TESTER))
      return false;
//--- В цикле по количеству ордеров
   int total=OrdersTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- выбираем ордер для получения его свойств
      if(OrderGetTicket(i)==0)
         continue;
      //--- если магик ордера не соответствует искомому - пропускаем
      if(OrderGetInteger(ORDER_MAGIC)!=magic_number)
         continue;
      //--- если тип ордера не Buy и не Sell - пропускаем
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if(type!=ORDER_TYPE_BUY && type!=ORDER_TYPE_SELL)
         continue;
      //--- если символ ордера соответствует искомому, но в ордере нет записи об идентификаторе позиции,
      //--- значит данные об открывающейся позиции ещё не добавлены в историю ордеров.
      //--- Это неопределённое состояние окружения - возвращаем true
      if(!OrderGetInteger(ORDER_POSITION_ID) && OrderGetString(ORDER_SYMBOL)==symbol_name)
         return true;
     }
//--- Окружение в порядке
   return false;
  }
//+------------------------------------------------------------------+
//| Проверка состояния окружения                                     |
//+------------------------------------------------------------------+
bool CheckUncertainStateEnv(const string symbol_name,const ulong magic_number,const int attempts,const int wait)
  {
//--- Если окружение в порядке - возвращаем true
   if(IsUncertainStateEnv(symbol_name,magic_number))
      return true;
//--- Делаем цикле ENV_ATTEMPTS попыток получения корректного окружения с ожиданием ENV_WAIT_ATTEMPT между попытками
   int n=0;
   while(!IsStopped() && n<attempts && IsUncertainStateEnv(symbol_name,magic_number))
     {
      n++;
      Sleep(wait);
     }
//--- Если по завершении ожидания окружение всё ещё неопределённое - сообщаем об этом и возвращаем false
   if(n>=attempts && IsUncertainStateEnv(symbol_name,magic_number))
     {
      PrintFormat("%s: Uncertain state of the environment. Please try again.",__FUNCTION__);
      return false;
     }
//--- Окружение корректно
   return true;
  }
//+------------------------------------------------------------------+
//| Заполняет массивы тикетов позиций                                |
//+------------------------------------------------------------------+
bool FillingListTickets(const string symbol_name,const ulong magic_number)
  {
//--- Если торговое окружение не корректно - возвращаем false
   if(!CheckUncertainStateEnv(symbol_name,magic_number,ENV_ATTEMPTS,ENV_WAIT_ATTEMPT))
      return false;

//--- Очищаем списки и инициализируем переменные
   Data.Buy.list_tickets.Clear();
   Data.Sell.list_tickets.Clear();
   Data.Buy.total_volume=0;
   Data.Sell.total_volume=0;
   
//--- В цикле по открытым позициям
   int total=PositionsTotal();
   for(int i=total-1; i>WRONG_VALUE; i--)
     {
      //--- выбираем позицию для получения свойств
      ulong ticket=PositionGetTicket(i);
      if(ticket==0)
         continue;
      //--- Если магик или символ не соответствуют переданным в функцию - идём дальше
      if(PositionGetInteger(POSITION_MAGIC)!=InpMagic || PositionGetString(POSITION_SYMBOL)!=symbol_name)
         continue;
      //--- Получаем тип и объём позиции
      ENUM_POSITION_TYPE type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double volume=PositionGetDouble(POSITION_VOLUME);
      //--- В зависимости от типа позиции добавляем тикет и объём в соответствующие списки
      if(type==POSITION_TYPE_BUY)
        {
         Data.Buy.list_tickets.Add(ticket);
         Data.Buy.total_volume+=volume;
        }
      //--- POSITION_TYPE_SELL
      else
        {
         Data.Sell.list_tickets.Add(ticket);
         Data.Sell.total_volume+=volume;
        }
     }
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает количество позиций Buy                                |
//+------------------------------------------------------------------+
int TotalBuy(void)
  {
   return Data.Buy.list_tickets.Total();
  }
//+------------------------------------------------------------------+
//| Возвращает количество позиций Sell                               |
//+------------------------------------------------------------------+
int TotalSell(void)
  {
   return Data.Sell.list_tickets.Total();
  }
//+------------------------------------------------------------------+
//| Возвращает последний добавленный тикет позиции по типу           |
//+------------------------------------------------------------------+
ulong LastAddedTicket(const ENUM_POSITION_TYPE type)
  {
   return(type==POSITION_TYPE_BUY ? (TotalBuy()>0 ? Data.Buy.list_tickets.At(0) : 0) : (TotalSell()>0 ? Data.Sell.list_tickets.At(0) : 0));
  }
//+------------------------------------------------------------------+
//| Возвращает номер бара, на котором была открыта позиция           |
//+------------------------------------------------------------------+
int PositionBar(const ulong ticket)
  {
//--- Выбираем позицию по тикету
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket #%I64u. Error %d",__FUNCTION__,ticket,GetLastError());
      return -1;
     }
//--- Получаем время открытия и символ позиции
   datetime time=(datetime)PositionGetInteger(POSITION_TIME);
   string   symbol=PositionGetString(POSITION_SYMBOL);
   
//--- Возвращаем номер бара по времени открытия позиции
   return iBarShift(symbol,PERIOD_CURRENT,time);
  }
//+------------------------------------------------------------------+
//| Возвращает наличие указанной позиции, открытой на текущем баре   |
//+------------------------------------------------------------------+
bool IsPresentPosOnCurrentBar(const ENUM_POSITION_TYPE type)
  {
   ulong ticket=LastAddedTicket(type);
   return(ticket>0 ? PositionBar(ticket)==0 : false);
  }
//+------------------------------------------------------------------+
//| Возвращает цену открытия позиции по тикету                       |
//+------------------------------------------------------------------+
double PositionPriceOpen(const ulong ticket)
  {
//--- Проверяем тикет
   if(ticket==0)
      return 0;
//--- Выбираем позицию по тикету
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket #%I64u. Error %d",__FUNCTION__,ticket,GetLastError());
      return 0;
     }
//--- Возвращаем цену открытия позиции
   return PositionGetDouble(POSITION_PRICE_OPEN);
  }
//+------------------------------------------------------------------+
//| Закрывает позиции Buy                                            |
//+------------------------------------------------------------------+
bool CloseBuy(void)
  {
   int total=TotalBuy();
   bool res=true;
   for(int i=total-1; i>=0; i--)
     {
      ulong ticket=Data.Buy.list_tickets.At(i);
      if(ticket==NULL)
         continue;
      if(!trade.PositionClose(ticket,InpDeviation))
         res=false;
     }
   return res;
  }
//+------------------------------------------------------------------+
//| Закрывает позиции Sell                                           |
//+------------------------------------------------------------------+
bool CloseSell(void)
  {
   int total=TotalSell();
   bool res=true;
   for(int i=total-1; i>=0; i--)
     {
      ulong ticket=Data.Sell.list_tickets.At(i);
      if(ticket==NULL)
         continue;
      if(!trade.PositionClose(ticket,InpDeviation))
         res=false;
     }
   return res;
  }
//+------------------------------------------------------------------+
//| Открытие позиции                                                 |
//+------------------------------------------------------------------+
bool OpenPosition(const string symbol_name,const ENUM_POSITION_TYPE type,const double volume,const string comment)
  {
//--- Рассчитываем значения для стоп-приказов
   int    bb=int(HalfSizeBB(0)*InpSLMltp);
   double atrd=ATR(0);
   int    atrp=(atrd!=EMPTY_VALUE ? int(round(atrd*InpTPMltp/Point())) : 0);
   double sl=(InpStopLoss==0   ? 0 : (InpStopLoss<0 ? (bb!=0   ? CorrectStopLoss(type,bb)     : 0) : CorrectStopLoss(type,InpStopLoss)));
   double tp=(InpTakeProfit==0 ? 0 : (InpStopLoss<0 ? (atrp!=0 ? CorrectTakeProfit(type,atrp) : 0) : CorrectTakeProfit(type,InpTakeProfit)));

//--- На неттинговом счёте убираем стоп-приказы
   if(netto)
      sl=tp=0;

//--- Получаем цены
   MqlTick tick={};
   if(!SymbolInfoTick(symbol_name,tick))
     {
      PrintFormat("%s: Unable to get prices");
      return false;
     }
//--- Проверяем и получаем нормализованный лот открываемой позиции
   double ll=trade.CheckVolume(symbol_name,volume,(type==POSITION_TYPE_BUY ? tick.ask : tick.bid),(ENUM_ORDER_TYPE)type);
   if(ll==0)
     {
      PrintFormat("%s: Error. CheckVolume() returned a zero lot",__FUNCTION__);
      return false;
     }

//--- Проверяем ограничение на максимальный объём открытых позиций на счёте
   if(!CheckLotForLimitAccount(type,ll))
     {
      PrintFormat("%s: CheckLotForLimitAccount() returned an error",__FUNCTION__);
      return false;
     }

//--- Может быть ситуация, когда торговый приказ уже был отправлен, но он ещё не полностью обработан,
//--- что может привести к задвоению открываемой позиции.
//--- Если торговое окружение не корректно - возвращаем false
   if(!CheckUncertainStateEnv(symbol_name,InpMagic,ENV_ATTEMPTS,ENV_WAIT_ATTEMPT))
      return false;

//--- Ожидание получения корректного торгового окружения может занять некоторое время
//--- Ещё раз получим цены
   if(!SymbolInfoTick(symbol_name,tick))
     {
      PrintFormat("%s: Unable to get prices");
      return false;
     }

//--- Возвращаем результат отправки торгового запроса на сервер
   return(type==POSITION_TYPE_BUY ? trade.Buy(ll,symbol_name,tick.ask,sl,tp,comment) : trade.Sell(ll,symbol_name,tick.bid,sl,tp,comment));
  }
//+------------------------------------------------------------------+
//| Процесс торговли                                                 |
//+------------------------------------------------------------------+
void TradeProcess(const ENUM_SIGNAL_TYPE signal)
  {
//--- Нет сигнала - уходим
   if(signal==SIGNAL_TYPE_NONE)
      return;
   
//--- Сигнал на закрытие
   if(signal==SIGNAL_TYPE_CLOSE)
     {
      CloseBuy();
      CloseSell();
     }

//--- Сигнал на покупку
   if(signal==SIGNAL_TYPE_LONG)
     {
      //--- Если нет открытой позиции Buy на этом баре
      if(!IsPresentPosOnCurrentBar(POSITION_TYPE_BUY))
        {
         //--- Получаем цену последней открытой позиции Buy
         double price_last=PositionPriceOpen(LastAddedTicket(POSITION_TYPE_BUY));
         //--- Если это самая первая позиция Buy, либо цена открытия лучше цены открытия прошлой позиции -
         //--- отсылаем запрос на открытие позиции Buy
         if(price_last==0 || price_last>SymbolInfoDouble(Symbol(),SYMBOL_ASK))
           {
            //--- Если позиция открыта - обновляем списки тикетов открытых позиций
            if(OpenPosition(Symbol(),POSITION_TYPE_BUY,lot,""))
               FillingListTickets(Symbol(),InpMagic);
           }
        }
     }
   
//--- Сигнал на продажу
   if(signal==SIGNAL_TYPE_SHORT)
     {
      //--- Если нет открытой позиции Sell на этом баре
      if(!IsPresentPosOnCurrentBar(POSITION_TYPE_SELL))
        {
         //--- Получаем цену последней открытой позиции Sell
         double price_last=PositionPriceOpen(LastAddedTicket(POSITION_TYPE_SELL));
         //--- Если это самая первая позиция Sell, либо цена открытия лучше цены открытия прошлой позиции -
         //--- отсылаем запрос на открытие позиции Sell
         if(price_last<SymbolInfoDouble(Symbol(),SYMBOL_ASK))
           {
            //--- Если позиция открыта - обновляем списки тикетов открытых позиций
            if(OpenPosition(Symbol(),POSITION_TYPE_SELL,lot,""))
               FillingListTickets(Symbol(),InpMagic);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Данный советник сделан на основе "Простого советника на основе индикаторов WPR, Bollinger Bands и ATR" в CodeBase. Основная логика и решения в коде прокомментированы и, думаю, легки для понимания. В любом случае, все интересующие вопросы можно задать в комментариях к статье.

Сохраним советник в папке \MQL5\Experts\STOCKS_COMMODITIES\Gopalakrishnan Range Index\ под именем ExpGRI_AMA_TEMA.mq5.

Скомпилируем советник, отключим в настройках фильтрацию по GRI

и запустим его тестирование на периоде графика H4 по всем тикам на EURUSD на периоде с 2024.01.01 до 2026.01.05. 

В итоге получим такой график тестирования:

Теперь включим фильтрацию сигналов по индикатору GRI

и заново запустим тестирование с теми же параметрами. Получим такой график тестирования:

При использовании входов только на активном рынке (с фильтрацией сигалов по GRI) разница с тестом без фильтрации сигналов хорошо заметна. Да, здесь точно так же, как и при тестировании без фильтра по волатильности, есть прибыльные и убыточные торговые дни. Но всё же видно, что длительность убыточных периодов меньше, чем прибыльных. А если учесть, что ТС однозначно трендовая, а в ней отсутствует логика определения тренда и флета, то понимаем, что использование индикатора GRI дало положительный результат на выбранном отрезке истории.

Для правильной работы советника нужно, чтобы индикатор GRI был расположен в месте расположения советника — в той же папке, например \MQL5\Experts\STOCKS_COMMODITIES\Gopalakrishnan Range Index\.



Заключение

В мире финансового анализа погоня за инновациями часто заставляет нас искать сложные алгоритмы, углубляться в нейронные сети и искать некие нестандартные решения, в то время как проверенные временем идеи остаются в тени. Реализация Gopalakrishnan Range Index для MetaTrader 5 показывает: то, что работало десятилетия назад, не теряет своей актуальности, а лишь требует современной формы воплощения.

Это еще одно подтверждение того, что всё новое — это хорошо забытое старое. Вернув этот инструмент из архивов журнала Technical Analysis of Stocks & Commodities в терминалы современных трейдеров, мы не просто отдаем дань истории, но и получаем надежный, очищенный от лишнего рыночного шума метод оценки волатильности, который прошел проверку временем.

Подводя итог, стоит отметить, что GPI — это яркий пример того, как фундаментальные математические концепции 90-х годов находят вторую жизнь в эпоху алгоритмической торговли. Часто самые эффективные инструменты — это не те, что используют самые сложные формулы, а те, что опираются на базовую логику поведения цены. Перенос индикатора на платформу MetaTrader 5 напоминает нам: прежде чем искать "святой Грааль" в новейших разработках, стоит заглянуть в классику. Ведь часто хорошее решение уже было найдено — нам просто нужно было адаптировать его под современные вычислительные мощности.

Все файлы — индикатора и советника прикреплены к статье для самостоятельного изучения.

Прикрепленные файлы |
GRI.mq5 (7.73 KB)
ExpGRI_AMA_TEMA.mq5 (91.39 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Maxim Kuznetsov
Maxim Kuznetsov | 20 янв. 2026 в 09:06

я-бы за подобное банил :-)

потому-что ответа на поставленный в теме вопрос нет, запросов со стороны нет, но есть безумная сумрачная простыня кода. Зарплата модератора она такая

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Алгоритм поисковой оптимизации Эбола — Ebola Optimization Search Algorithm (EOSA) Алгоритм поисковой оптимизации Эбола — Ebola Optimization Search Algorithm (EOSA)
В статье рассматривается алгоритм EOSA, вдохновлённый механизмами распространения вируса Эбола: короткодистанционной передачей через близкий контакт (эксплуатация) и длиннодистанционной передачей через путешествия (исследование). Анализ оригинальной публикации выявил критические проблемы в математических формулах и нереализуемую на практике эпидемиологическую модель, что потребовало существенной переработки алгоритма для получения работоспособной реализации.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Возмущённые модели пространства состояний для анализа рыночной динамики (Основные компоненты) Нейросети в трейдинге: Возмущённые модели пространства состояний для анализа рыночной динамики (Основные компоненты)
В данной статье представлен практический подход к адаптации современного фреймворка для анализа финансовых потоков средствами MQL5. Рассмотрены ключевые компоненты модели — Depth-Wise свёртки с остаточными связями, конусные Super Kernel Block и модуль глобальной агрегации движения (GMA).