preview
Графики индекса доллара и индекса евро — пример сервиса в MetaTrader 5

Графики индекса доллара и индекса евро — пример сервиса в MetaTrader 5

MetaTrader 5Примеры | 28 августа 2024, 17:49
97 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

Индекс американского доллара (U.S. Dollar Index) является самым известным и главным индексом валютного рынка. Благодаря ему можно прогнозировать движение курсов валют. Это важный показатель относительной стоимости американского доллара. Индекс американского доллара (USDX) существует с марта 1973 года, когда базовым его значением был принят уровень 100 пунктов. То есть индекс, равный 90 пунктам, сегодня будет значить падение доллара относительно показателя 1973 года на 10%, а индекс, равный 110 пунктов — рост на 10%.

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

USDX рассчитывается как среднее геометрическое взвешенное валютной корзины, в которую входят значимые мировые валюты. Каждая валюта в этой корзине относится к группе шести основных торговых партнеров США, не равных по своим экономическим возможностям, поэтому каждой валюте в индексе отводится конкретная доля влияния (вес):

Валюта
Вес
Евро (EUR)
0.576 (57.6%)
Японская йена (JPY)
0.136 (13.6%)
Британский фунт (GBP)
0.119 (11.9%)
Канадский доллар (CAD)
0.091 (9.1%)
Шведская крона (SEK)
0.042 (4.2%)
Швейцарский франк (CHF)
0.036 (3.6%)

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

USDX = 50.14348112 * EURUSD^(-0.576) * USDJPY^(0.136) * GBPUSD^(-0.119) * USDCAD^(0.091) * USDSEK^(0.042) * USDCHF^(0.036) 

Степени в расчёте, в которые возводятся курсы, соответствуют весу валют в используемой корзине. Коэффициент 50.14348112 приводит индекс доллара к значению 100.0 в случае, если в формулу будут подставлены курсы валют на март 1973 года. Таким образом, текущий USDX отражает изменение стоимости американского доллара по отношению к корзине валют по сравнению с котировками 1973 года. Значение индекса меньше 100 пунктов говорит об удешевлении доллара, а больше 100 пунктов — о повышении стоимости американской валюты по сравнению с 1973 годом.

Индекс евро (Euro Currency Index) — это средний показатель изменения курсов пяти мировых валют (доллара США, британского фунта, японской йены, швейцарского франка и шведской кроны) по отношению к евро.

Как торговый инструмент, индекс евро (EURX) введён 13 января 2006 года на бирже New York Board of Trade (NYBOT), тикеры ECX, EURX или E.

Индекс евро стал "эталоном" текущей стоимости единой европейской валюты для участников международных финансовых рынков, а также инструментом для проведения торговых операций.

Расчёт индекса евро по корзине из пяти валют совпадает с данными, используемыми Европейским Центральным Банком при расчётах торгово-взвешенного индекса евро по валютам тех стран, которые образуют основной внешнеторговый оборот стран Еврозоны. Большая часть международной торговли стран, входящих в Еврозону, приходится на США (31.55 %), далее следуют Великобритания — 30,56 %, Япония — 18,91 %, Швейцария — 11,13 % и Швеция — 7,85 %.

Основные принципы расчёта текущего значения индекса евро аналогичны принципам, применяемым при расчете индекса доллара США (USDX). Индекс евро рассчитывается с использованием метода расчета среднего геометрического взвешенного значения:

EURX = 34.38805726 * EURUSD^(0.3155) * EURGBP^(0.3056) * EURJPY^(0.1891) * EURCHF^(0.1113) * EURSEK^(0.0785)

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


Ставим задачу

Итак, что же мы хотим получить в итоге... Нам нужно создать синтетический инструмент, цена которого рассчитывается по приведённым выше формулам. Нам нужен полноценный график инструмента, который будет обновляться с приходом каждого нового тика на символах, используемых в корзине инструментов, и на этом графике можно запускать любые индикаторы, скрипты и советники.

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

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

Таких пользовательских символов создадим два — индекс американского доллара и индекс евро. Для каждого из этих инструментов будет создана своя программа-сервис. Но созданы они будут на основе одного включаемого файла, в котором разместим все функции для создания нужного инструмента. В программе-сервисе будут лишь указаны требуемые для расчёта инструмента размер корзины, базовый коэффициент и структура символов с их весами. Всё остальное будет происходить внутри подключаемого файла при вызове функций, прописанных в нём. Это позволит на основе созданных функций создавать собственные индексы с иным набором валют и их весов — всего лишь указав список символов и вес каждого из них.


Пишем включаемый файл функций расчёта индексов валют

В каталоге терминала \MQL5\Services\ создадим новую папку Indexes\, а в ней — новый включаемый файл с именем CurrencyIndex.mqh. В него будем записывать все функции, необходимые для работы проекта.

В самом начале файла впишем необходимые для работы макроподстановки структуры и перечисления:

//+------------------------------------------------------------------+
//|                                                CurrencyIndex.mqh |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"

#define SECONDS_IN_DAY    (24*60*60)   // количество секунд в сутках
#define SECONDS_IN_MINUTE 60           // количество секунд в минуте
#define MSECS_IN_MINIUTE  (60*1000)    // количество миллисекунд в минуте

//--- структура символа корзины
struct SymbolWeight
  {
   string            symbol;           // символ
   double            weight;           // вес
  };
  
//--- структура исторических данных
struct str_rates
  {
   int               index;            // индекс данных
   MqlRates          rates[];          // массив исторических данных
  };
  
//--- структура тиковых данных
struct str_ticks
  {
   int               index;            // индекс данных
   MqlTick           ticks[];          // массив тиков
  };
  
//--- перечисление типов цен
enum ENUM_RATES_VALUES
  {
   VALUE_OPEN,                         // цена Open
   VALUE_HIGH,                         // цена High
   VALUE_LOW,                          // цена Low
   VALUE_CLOSE                         // цена Close
  };

int ExtDigits=5;                       // точность измерения цены символа

В структуре символа корзины прописаны два поля: наименование символа и его вес в корзине инструментов. При составлении корзины инструментов для расчёта индекса, удобно будет использовать массив таких структур: в него сразу же при инициализации запишем наименования символов и их веса, а далее из этого массива будем в цикле получать символ и его вес для расчёта цен индекса. При этом в массиве могут быть записаны любые символы с их весами, что даёт гибкость в создании любых корзин инструментов для расчёта индексов.

В структурах исторических и тиковых данных используются массивы соответствующих структур: MqlRates и MqlTick — в них будут содержаться данные по каждому из символов корзины инструментов. И есть ещё индекс данных в каждой из этих структур. Индекс необходим для указания номера существующего бара, с которого берутся данные для расчёта индекса. Например, для расчёта индекса на каком-либо баре, необходимо, чтобы у каждого из символов корзины инструментов, участвующих в расчёте индекса, на этом минутном баре были данные. Но не обязательно они могут быть на каждом из символов — где-то есть пропуски баров (если за эту минуту не было тиков на каком-то из символов). В этом случае и требуется указание индекса бара, с которого берутся данные для расчёта — там, где на символе нет данных, индекс увеличивается — чтобы данные взять с предыдущего бара. А так как мы не знаем заранее количество символов в корзине инструментов для расчёта индекса, то мы не можем заранее объявить в программе нужное количество индексов для каждого инструмента. Поэтому удобно их хранить и использовать в такой структуре.

В перечислении типов цен просто заданы константы, указывающие цену, которую нужно получить для расчёта цен баров.

При запуске сервиса в первую очередь необходимо создать пользовательский символ, построить исторические бары М1 за месяц и историю тиков. Это будет являться инициализацией сервиса. Напишем такую функцию:

//+------------------------------------------------------------------+
//| Инициализация сервиса                                            |
//+------------------------------------------------------------------+
bool InitService(const string custom_symbol,const string custom_group)
  {
   MqlRates rates[100];
   MqlTick  ticks[100];
   
//--- инициализируем пользовательский символ
   if(!CustomSymbolInitialize(custom_symbol,custom_group))
      return(false);
   ExtDigits=(int)SymbolInfoInteger(custom_symbol,SYMBOL_DIGITS);
   
//--- делаем активными все символы корзины инструментов, участвующие в расчёте индекса
   for(int i=0; i<BASKET_SIZE; i++)
     {
      //--- выбираем символ в окне "Обзор рынка"
      if(!SymbolSelect(ExtWeights[i].symbol,true))
        {
         PrintFormat("cannot select symbol %s",ExtWeights[i].symbol);
         return(false);
        }
      //--- запрашиваем исторические данные баров и тиков по выбранному символу
      CopyRates(ExtWeights[i].symbol,PERIOD_M1,0,100,rates);
      CopyTicks(ExtWeights[i].symbol,ticks,COPY_TICKS_ALL,0,100);
     }
     
//--- строим M1 бары за 1 месяц
   if(!PrepareRates(custom_symbol))
      return(false);
      
//--- получаем последние тики после построения M1 баров
   PrepareLastTicks(custom_symbol);
   
//--- сервис инициализирован
   Print(custom_symbol," datafeed started");
   return(true);
  }

Проверка существования и создание пользовательского символа происходит в функции CustomSymbolInitialize():

//+------------------------------------------------------------------+
//| Инициализация пользовательского символа                          |
//+------------------------------------------------------------------+
bool CustomSymbolInitialize(string symbol,string group)
  {
   bool is_custom=false;
//--- если символ выбран в окне "Обзор рынка", получаем флаг, что это пользовательский символ
   bool res=SymbolSelect(symbol,true);
   if(res)
      is_custom=(bool)SymbolInfoInteger(symbol,SYMBOL_CUSTOM);

//--- если выбранный символ не пользовательский - создаём его
   if(!res)
     {
      if(!CustomSymbolCreate(symbol,group,"EURUSD"))
        {
         Print("cannot create custom symbol ",symbol);
         return(false);
        }
      //--- символ успешно создан - устанавливаем флаг, что это пользовательский символ
      is_custom=true;
      
      //--- помещаем созданный символ в окно "Обзор рынка"
      if(!SymbolSelect(symbol,true))
        {
         Print("cannot select custom symbol ",symbol);
         return(false);
        }
     }
     
//--- откроем график созданного пользовательского символа
   if(is_custom)
     {
      //--- получаем идентификатор первого окна открытых графиков
      long chart_id=ChartFirst();
      bool found=false;
      //--- в цикле по списку открытых графиков найдём график созданного пользовательского символа
      while(chart_id>=0)
        {
         //--- если график открыт - сообщаем об этом в журнал, ставим флаг найденного графика и выходим из цикла поиска
         if(ChartSymbol(chart_id)==symbol)
           {
            found=true;
            Print(symbol," chart found");
            break;
           }
         //--- на основании текущего выбранного графика получаем идентификатор следующего для очередной итерации поиска в цикле
         chart_id=ChartNext(chart_id);
        }
      
      //--- если график символа не найден среди открытых графиков
      if(!found)
        {
         //--- сообщаем об открытии графика M1 пользовательского символа,
         //--- получаем идентификатор открываемого графика и переходим на него 
         Print("open chart ",symbol,",M1");
         chart_id=ChartOpen(symbol,PERIOD_M1);
         ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
        }
     }
//--- пользовательский символ инициализирован
   return(is_custom);
  }

Здесь мы проверяем есть ли пользовательский символ с указанным именем. Если нет — создаём его. Далее ищем открытый график этого символа и, если график не найден среди открытых в терминале — открываем график.

После создания пользовательского символа и открытия его графика, необходимо создать историю периода M1 за месяц. Делается это при помощи функции PrepareRates():

//+------------------------------------------------------------------+
//| Подготовка исторических данных                                   |
//+------------------------------------------------------------------+
bool PrepareRates(const string custom_symbol)
  {
   str_rates symbols_rates[BASKET_SIZE];
   int       i,reserve=0;
   MqlRates  usdx_rates[];                                              // массив-таймсерия синтетического инструмента
   MqlRates  rate;                                                      // данные одного бара синтетического инструмента
   datetime  stop=(TimeCurrent()/SECONDS_IN_MINUTE)*SECONDS_IN_MINUTE;  // время бара M1 конечной даты
   datetime  start=stop-31*SECONDS_IN_DAY;                              // время бара M1 начальной даты
   datetime  start_date=0;
   
//--- копируем исторические данные M1 за месяц для всех символов корзины инструментов
   start/=SECONDS_IN_DAY;
   start*=SECONDS_IN_DAY;                                               // время бара D1 начальной даты
   for(i=0; i<BASKET_SIZE; i++)
     {
      if(CopyRates(ExtWeights[i].symbol,PERIOD_M1,start,stop,symbols_rates[i].rates)<=0)
        {
         PrintFormat("cannot copy rates for %s,M1 from %s to %s [%d]",ExtWeights[i].symbol,TimeToString(start),TimeToString(stop),GetLastError());
         return(false);
        }
      PrintFormat("%u %s,M1 rates from %s",ArraySize(symbols_rates[i].rates),ExtWeights[i].symbol,TimeToString(symbols_rates[i].rates[0].time));
      symbols_rates[i].index=0;
      //--- находим и устанавливаем минимальную ненулевую начальную дату из корзины символов
      if(start_date<symbols_rates[i].rates[0].time)
         start_date=symbols_rates[i].rates[0].time;
     }
   Print("start date set to ",start_date);
   
//--- резерв массива исторических данных для избежания перераспределения памяти при изменении размера массива
   reserve=int(stop-start)/60;
   
//--- установим начало всех исторических данных корзины символов на одну дату (start_date)
   for(i=0; i<BASKET_SIZE; i++)
     {
      int j=0;
      //--- до тех пор, пока j меньше количества данных в массиве rates и
      //--- время по индексу j в массиве меньше времени start_date - увеличиваем индекс
      while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<start_date)
         j++;
      //--- если индекс был увеличен, и он в пределах массива rates - уменьшим его на 1 для компенсации последнего приращения
      if(j>0 && j<ArraySize(symbols_rates[i].rates))
         j--;
      //--- запишем полученный индекс в структуру
      symbols_rates[i].index=j;
     }
      
//--- таймсерии USD index
   int    array_size=0;
   
//--- первый бар таймсерии M1
   rate.time=start_date;
   rate.real_volume=0;
   rate.spread=0;

//--- до тех пор, пока время бара меньше времени конечной даты таймсерии M1
   while(!IsStopped() && rate.time<stop)
     {
      //--- если исторические данные бара инструмента рассчитаны
      if(CalculateRate(rate,symbols_rates))
        {
         //--- увеличиваем массив-таймсерию на 1 и добавляем в него рассчитанные данные
         ArrayResize(usdx_rates,array_size+1,reserve);
         usdx_rates[array_size]=rate;
         array_size++;
         //--- сбросим размер резервного значения размера массива, так как он применяется только при первом изменении размера
         reserve=0;
        }
      
      //--- следующий бар таймсерии M1
      rate.time+=PeriodSeconds(PERIOD_M1);
      start_date=rate.time;
      
      //--- в цикле по списку инструментов корзины
      for(i=0; i<BASKET_SIZE; i++)
        {
         //--- получаем текущий индекс данных
         int j=symbols_rates[i].index;
         //--- пока j в пределах данных таймсерии и, если время бара по индексу j меньше времени, установленного для этого бара в rate.time, увеличиваем индекс j
         while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<rate.time)
            j++;
         //--- если j в пределах данных таймсерии и время в start_date меньше времени данных таймсерии по индексу j
         //--- и время в таймсерии по индексу j меньше, либо равно времени в rate.time - записывавем в start_date время из таймсерии по индексу j
         if(j<ArraySize(symbols_rates[i].rates) && start_date<symbols_rates[i].rates[j].time && symbols_rates[i].rates[j].time<=rate.time)
            start_date=symbols_rates[i].rates[j].time;
        }
      
      //--- в цикле по списку инструментов корзины
      for(i=0; i<BASKET_SIZE; i++)
        {
         //--- получаем текущий индекс данных
         int j=symbols_rates[i].index;
         //--- пока j в пределах данных таймсерии и, если время бара по индексу j меньше времени, установленного для этого бара в start_date, увеличиваем индекс j
         while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<=start_date)
            symbols_rates[i].index=j++;
        }
      //--- в rate.time запишем время из start_date для последующего бара
      rate.time=start_date;
     }
     
//--- добавляем в базу созданную таймсерию
   if(array_size>0)
     {
      if(!IsStopped())
        {
         int cnt=CustomRatesReplace(custom_symbol,usdx_rates[0].time,usdx_rates[ArraySize(usdx_rates)-1].time+1,usdx_rates);
         Print(cnt," ",custom_symbol,",M1 rates from ",usdx_rates[0].time," to ",usdx_rates[ArraySize(usdx_rates)-1].time," added");
        }
     }
//--- успешно
   return(true);
  }

В функции сначала копируются данные по каждому из символов корзины инструментов и выставляется общее время начала копирования данных. Затем для каждого из инструментов устанавливается индекс данных, время которого совпадает со временем остальных символов корзины так, чтобы начальная дата была одинакова для всех символов корзины, а если для какого-либо символа нет бара по начальной дате, то для него устанавливается индекс ближайшего предыдущего бара, на котором данные существуют.

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

Исторческие данные одного бара синтетического инструмента рассчитываются в функции CalculateRate():

//+------------------------------------------------------------------+
//| Расчёт цен и объёмов синтетического инструмента                  |
//+------------------------------------------------------------------+
bool CalculateRate(MqlRates& rate,str_rates& symbols_rates[])
  {
   double values[BASKET_SIZE]={0};
   long   tick_volume=0;
   int    i;
//--- получаем цены Open всех символов корзины инструментов в массив values[]
   for(i=0; i<BASKET_SIZE; i++)
      values[i]=GetRateValue(tick_volume,symbols_rates[i],rate.time,VALUE_OPEN);

//--- если тиковый объём нулевой, значит нет данных на этой минуте - возвращаем false
   if(tick_volume==0)
      return(false);
      
//--- запишем совокупный объем всех таймсерий
   rate.tick_volume=tick_volume;
   
//--- рассчитаем цену Open по ценам и весам всех инструментов корзины
   rate.open=MAIN_COEFF;
   for(i=0; i<BASKET_SIZE; i++)
      rate.open*=MathPow(values[i],ExtWeights[i].weight);
      
//--- рассчитаем цену High по ценам и весам всех инструментов корзины
   for(i=0; i<BASKET_SIZE; i++)
      values[i]=GetRateValue(tick_volume,symbols_rates[i],rate.time,VALUE_HIGH);
   rate.high=MAIN_COEFF;
   for(i=0; i<BASKET_SIZE; i++)
      rate.high*=MathPow(values[i],ExtWeights[i].weight);
      
//--- рассчитаем цену Low по ценам и весам всех инструментов корзины
   for(i=0; i<BASKET_SIZE; i++)
      values[i]=GetRateValue(tick_volume,symbols_rates[i],rate.time,VALUE_LOW);
   rate.low=MAIN_COEFF;
   for(i=0; i<BASKET_SIZE; i++)
      rate.low*=MathPow(values[i],ExtWeights[i].weight);
      
//--- рассчитаем цену Close по ценам и весам всех инструментов корзины
   for(i=0; i<BASKET_SIZE; i++)
      values[i]=GetRateValue(tick_volume,symbols_rates[i],rate.time,VALUE_CLOSE);
   rate.close=MAIN_COEFF;
   for(i=0; i<BASKET_SIZE; i++)
      rate.close*=MathPow(values[i],ExtWeights[i].weight);
      
//--- возвращаем результат проверки цен на корректность
   return(CheckRate(rate));
  }

Для каждой из цен бара (Open, High, Low и Close) синтетического инструмента производится расчёт цен по формуле синтетического инструмента:

Open USDX = 50.14348112 * Open EURUSD^(-0.576) * Open USDJPY^(0.136) * Open GBPUSD^(-0.119) * Open USDCAD^(0.091) * Open USDSEK^(0.042) * Open USDCHF^(0.036);
High USDX = 50.14348112 * High EURUSD^(-0.576) * High USDJPY^(0.136) * High GBPUSD^(-0.119) * High USDCAD^(0.091) * High USDSEK^(0.042) * High USDCHF^(0.036);
Low  USDX = 50.14348112 * Low  EURUSD^(-0.576) * Low  USDJPY^(0.136) * Low  GBPUSD^(-0.119) * Low  USDCAD^(0.091) * Low  USDSEK^(0.042) * Low  USDCHF^(0.036);
CloseUSDX = 50.14348112 * CloseEURUSD^(-0.576) * CloseUSDJPY^(0.136) * CloseGBPUSD^(-0.119) * CloseUSDCAD^(0.091) * CloseUSDSEK^(0.042) * CloseUSDCHF^(0.036);

Цены каждого символа корзины получаем в функции GetRateValue():

//+------------------------------------------------------------------+
//| Возвращает указанную цену бара                                   |
//+------------------------------------------------------------------+
double GetRateValue(long &tick_volume,str_rates &symbol_rates,datetime time,ENUM_RATES_VALUES num_value)
  {
   double value=0;                  // получаемое значение
   int    index=symbol_rates.index; // индекс данных
   
//--- если индекс в пределах таймсерии
   if(index<ArraySize(symbol_rates.rates))
     {
      //--- в зависимости от типа запрашиваемых данных записываем соответствующее значение в переменную value
      switch(num_value)
        {
         //--- цена Open
         case VALUE_OPEN:
            if(symbol_rates.rates[index].time<time)
               value=symbol_rates.rates[index].close;
            else
              {
               if(symbol_rates.rates[index].time==time)
                 {
                  value=symbol_rates.rates[index].open;
                  //--- при запросе цены Open добавляем тиковый объём к переменной tick_volume, передаваемой по ссылке,
                  //--- для получения суммарного объёма всех символов корзины инструментов
                  tick_volume+=symbol_rates.rates[index].tick_volume;
                 }
              }
            break;
         //--- цена High
         case VALUE_HIGH:
            if(symbol_rates.rates[index].time<time)
               value=symbol_rates.rates[index].close;
            else
              {
               if(symbol_rates.rates[index].time==time)
                  value=symbol_rates.rates[index].high;
              }
            break;
         //--- цена Low
         case VALUE_LOW:
            if(symbol_rates.rates[index].time<time)
               value=symbol_rates.rates[index].close;
            else
              {
               if(symbol_rates.rates[index].time==time)
                  value=symbol_rates.rates[index].low;
              }
            break;
         //--- цена Close
         case VALUE_CLOSE:
            if(symbol_rates.rates[index].time<=time)
               value=symbol_rates.rates[index].close;
            break;
        }
     }
     
//--- возвращаем полученное значение
   return(value);
  }

Из функции CalculateRate() возвращается результат проверки рассчитанных цен синтетического инструмента:

//--- возвращаем результат проверки цен на корректность
   return(CheckRate(rate));

Так как все цены бара синтетического инструмента рассчитываются, то необходимо проверить корректность их расчёта и скорректировать ошибки (если таковые есть).

Всё это делается в функции CheckRate(), результат работы которой и возвращается:

//+------------------------------------------------------------------+
//| Проверка цен на корректность и возврат результата проверки       |
//+------------------------------------------------------------------+
bool CheckRate(MqlRates &rate)
  {
//--- если цены представляют собой не корректные действительные числа, или меньше, либо равны нулю - возвращаем false
   if(!MathIsValidNumber(rate.open) || !MathIsValidNumber(rate.high) || !MathIsValidNumber(rate.low) || !MathIsValidNumber(rate.close))
      return(false);
   if(rate.open<=0.0 || rate.high<=0.0 || rate.low<=0.0 || rate.close<=0.0)
      return(false);
      
//--- нормализуем цены до требуемого количества знаков
   rate.open=NormalizeDouble(rate.open,ExtDigits);
   rate.high=NormalizeDouble(rate.high,ExtDigits);
   rate.low=NormalizeDouble(rate.low,ExtDigits);
   rate.close=NormalizeDouble(rate.close,ExtDigits);
   
//--- корректируем при необходимости цены
   if(rate.high<rate.open)
      rate.high=rate.open;
   if(rate.low>rate.open)
      rate.low=rate.open;
   if(rate.high<rate.close)
      rate.high=rate.close;
   if(rate.low>rate.close)
      rate.low=rate.close;
      
//--- всё успешно
   return(true);
  }

Если любая из рассчитанных цен является плюс или минус бесконечностью, либо "не числом" (NaN — not a number), либо меньше или равна нулю, функция возвращает false.

Далее все цены нормализуются до точности, заданной для символа, и проверяется корректность цен Open, High, Low и Close относительно друг друга. При необходимости, цены корректируются и возвращается true.

В рассматриваемой выше функции InitService() после построения минутных баров за месяц получаем последние тиковые данные синтетического инструмента и возвращаем true:

     
//--- строим M1 бары за 1 месяц
   if(!PrepareRates(custom_symbol))
      return(false);
      
//--- получаем последние тики после построения M1 баров
   PrepareLastTicks(custom_symbol);
   
//--- сервис инициализирован
   Print(custom_symbol," datafeed started");
   return(true);
  }

Тиковые данные получаем в функции PrepareLastTicks():

//+------------------------------------------------------------------+
//| Подготовка последних тиков                                       |
//+------------------------------------------------------------------+
void PrepareLastTicks(const string custom_symbol)
  {
   str_ticks symbols_ticks[BASKET_SIZE];
   int       i,j,cnt,reserve=0;
   MqlTick   usdx_ticks[];                                        // массив тиков синтетического инструмента
   MqlTick   tick={0};                                            // данные одного тика синтетического инструмента

   long time_to=TimeCurrent()*1000;                               // время конца тиковых данных в миллисекундах
   long start_date=(time_to/MSECS_IN_MINIUTE)*MSECS_IN_MINIUTE;   // время открытия бара в миллисекундах со временем TimeCurrent()
   long time_from=start_date-MSECS_IN_MINIUTE;                    // время начала копирования тиковых данных в миллисекундах 

//--- если были тики за последнюю минуту
   if(SymbolInfoTick(custom_symbol,tick) && tick.time_msc>=start_date)
     {
      Print(custom_symbol," last tick at ",datetime(tick.time_msc/1000),":",IntegerToString(tick.time_msc%1000,3,'0'));
      str_rates symbols_rates[BASKET_SIZE];
      bool      copy_error=false;
      
      //--- в цикле по количеству символов в корзине инструментов
      for(i=0; i<BASKET_SIZE; i++)
        {
         //--- копируем два последних бара исторических данных инструмента
         if(CopyRates(ExtWeights[i].symbol,PERIOD_M1,0,2,symbols_rates[i].rates)!=2)
           {
            Print("cannot copy ",ExtWeights[i].symbol," rates [",GetLastError(),"]");
            copy_error=true;
            break;
           }
         symbols_rates[i].index=1;
        }
      
      //--- рассчитываем данные последней минуты
      if(!copy_error)
        {
         MqlRates rate;
         double   values[BASKET_SIZE]={0};
         rate.time=datetime(start_date/1000);
         rate.real_volume=0;
         rate.spread=0;
         
         //--- если исторические данные бара инструмента рассчитаны
         if(CalculateRate(rate,symbols_rates))
           {
            MqlRates usdx_rates[1];
            
            //--- заменяем рассчитанными данными бара историю последнего бара M1 пользовательского инструмента
            usdx_rates[0]=rate;
            cnt=CustomRatesUpdate(custom_symbol,usdx_rates);
            if(cnt==1)
              {
               Print(custom_symbol,",M1 last minute rate ",rate.time," added");
               //--- время в миллисекундах последующих добавляемых тииков
               start_date=tick.time_msc+1;
              }
           }
          else
            Print(custom_symbol,",M1 last minute rate ",rate.time," ",rate.open," ",rate.high," ",rate.low," ",rate.close," not updated");
        }
     }
     
//--- получаем все тики с начала предыдущей минуты
   for(i=0; i<BASKET_SIZE; i++)
     {
      if(CopyTicksRange(ExtWeights[i].symbol,symbols_ticks[i].ticks,COPY_TICKS_ALL,time_from,time_to)<=0)
        {
         PrintFormat("cannot copy ticks for %s",ExtWeights[i].symbol);
         return;
        }
      PrintFormat("%u %s ticks from %s",ArraySize(symbols_ticks[i].ticks),ExtWeights[i].symbol,TimeToString(symbols_ticks[i].ticks[0].time,TIME_DATE|TIME_SECONDS));
      symbols_ticks[i].index=0;
     }
     
//--- резерв массива тиков для избегания перераспределения памяти при изменении размера
   reserve=ArraySize(symbols_ticks[0].ticks);
   
//--- установим начало всех тиков на одну дату start_date
   j=0;
   while(j<ArraySize(symbols_ticks[0].ticks) && symbols_ticks[0].ticks[j].time_msc<start_date)
      j++;
   if(j>=ArraySize(symbols_ticks[0].ticks))
     {
      Print("no ticks at ",datetime(start_date/1000),":",IntegerToString(start_date%1000,3,'0')," (",start_date/1000,")" );
      return;
     }
   symbols_ticks[0].index=j;
   long time_msc=symbols_ticks[0].ticks[j].time_msc;
   for(i=1; i<BASKET_SIZE; i++)
     {
      j=0;
      while(j<ArraySize(symbols_ticks[i].ticks) && symbols_ticks[i].ticks[j].time_msc<time_msc)
         j++;
      if(j>0 && j<ArraySize(symbols_ticks[i].ticks))
         j--;
      symbols_ticks[i].index=j;
     }
     
//--- тики USD index
   double values[BASKET_SIZE]={0};
   int    array_size=0;
   
//--- первый тик
   tick.last=0;
   tick.volume=0;
   tick.flags=0;

//--- в цикле от индекса j (от начальной даты всех тиков корзины инструментов)
//--- по количеству полученных тиков первого инструмента корзины
   for(j=symbols_ticks[0].index; j<ArraySize(symbols_ticks[0].ticks); j++)
     {
      //--- записываем данные тика по индексу цикла j
      tick.time=symbols_ticks[0].ticks[j].time;          // время тика
      tick.time_msc=symbols_ticks[0].ticks[j].time_msc;  // время тика в миллисекундах
      
      //--- рассчитаем значение цены Bid по весам всех символов корзины инструментов
      values[0]=symbols_ticks[0].ticks[j].bid;
      symbols_ticks[0].index++;
      for(i=1; i<BASKET_SIZE; i++)
         values[i]=GetTickValue(symbols_ticks[i],symbols_ticks[0].ticks[j].time_msc);
      tick.bid=MAIN_COEFF;
      for(i=0; i<BASKET_SIZE; i++)
         tick.bid*=MathPow(values[i],ExtWeights[i].weight);
      //--- цена Ask равна рассчитанной цене Bid инструмента
      tick.ask=tick.bid;
      
      //--- добавляем рассчитанный тик в массив тиков синтетического инструмента
      ArrayResize(usdx_ticks,array_size+1,reserve);
      usdx_ticks[array_size]=tick;
      array_size++;
      
      //--- обнуляем размер резервированной памяти, так как он нужен только при первом ArrayResize
      reserve=0;
     }
     
//--- Добавляем в ценовую историю пользовательского инструмента данные из собранного массива тиков
   if(array_size>0)
     {
      Print(array_size," ticks from ",usdx_ticks[0].time,":",IntegerToString(usdx_ticks[0].time_msc%1000,3,'0')," prepared");
      cnt=CustomTicksAdd(custom_symbol,usdx_ticks);
      if(cnt>0)
         Print(cnt," ticks applied");
      else
         Print("no ticks applied");
     }
  }

Логика функции расписана в комментариях к коду. Сначала получаем два последних бара инструмента и заменяем ими данные в истории инструмента. Затем получаем тиковые данные с начала предыдущего бара, рассчитываем по ним значение тика и добавляем его в массив таймсерии инструмента. По окончании расчёта всех тиков, заменяем исторические тиковые данные инструмента данными из заполненного массива тиков.

Данные из массива тиков для расчёта тика синтетического инструмента получаем функцией GetTickValue():

//+------------------------------------------------------------------+
//| Возвращает значение тика                                         |
//+------------------------------------------------------------------+
double GetTickValue(str_ticks &symbol_ticks,long time_msc)
  {
   double value=0;
//--- если индекс данных, записанный в структуре symbol_ticks, находится в пределах массива тиков структуры
   if(symbol_ticks.index<ArraySize(symbol_ticks.ticks))
     {
      //--- получаем значение цены Bid из структуры по индексу данных
      value=symbol_ticks.ticks[symbol_ticks.index].bid;
      //--- если время в структуре в миллисекундах по индексу в структуре меньше переданного в функцию времени
      if(symbol_ticks.ticks[symbol_ticks.index].time_msc<time_msc)
        {
         //--- до тех пор, пока индекс находится в пределах таймсерии в структуре и
         //--- если время в структуре меньше переданного в функцию времени - увеличиваем индекс
         while(symbol_ticks.index<ArraySize(symbol_ticks.ticks) && symbol_ticks.ticks[symbol_ticks.index].time_msc<time_msc)
            symbol_ticks.index++;
        }
     }
//--- возвращаем полученное значение
   return(value);
  }

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

//+------------------------------------------------------------------+
//| Обработка тиков                                                  |
//+------------------------------------------------------------------+
void ProcessTick(const string custom_symbol)
  {
   static long    last_time_msc=0;     // время в миллисекундах последнего тика
   static MqlTick synth_tick[1];       // структура последнего тика синтетического инструмента
   static MqlTick ticks[BASKET_SIZE];  // массив данных последних тиков корзины символов
   static MqlTick tick;                // вспомогательная переменная для получения данных и поиска времени
   int success_cnt=0;
   int change_cnt=0;
   
//--- инициализируем время тика синтетического символа
   synth_tick[0].time=0;
   
//--- в цикле по количеству символов в корзине инструментов
   for(int i=0; i<BASKET_SIZE; i++)
     {
      //--- получаем данные очередного символа
      if(SymbolInfoTick(ExtWeights[i].symbol,tick))
        {
         //--- увеличиваем количество успешных запросов данных
         success_cnt++;
         //--- получаем самое свежее время из списка символов корзины
         if(synth_tick[0].time==0)
           {
            synth_tick[0].time=tick.time;
            synth_tick[0].time_msc=tick.time_msc;
           }
         else
           {
            if(synth_tick[0].time_msc<tick.time_msc)
              {
               synth_tick[0].time=tick.time;
               synth_tick[0].time_msc=tick.time_msc;
              }
           }
         //--- сохраняем полученные данные по символу в массиве ticks в соответствии с индексом символа корзины
         ticks[i]=tick;
        }
     }
   //--- если получены тики всех инструментов корзины, и это новый тик
   if(success_cnt==BASKET_SIZE && synth_tick[0].time!=0 && last_time_msc<synth_tick[0].time_msc)
     {
      //--- сохраняем время последнего тика
      last_time_msc=synth_tick[0].time_msc;
      
      //--- рассчитываем значение цены Bid синтетического инструмента
      synth_tick[0].bid=MAIN_COEFF;
      for(int i=0; i<BASKET_SIZE; i++)
         synth_tick[0].bid*=MathPow(ticks[i].bid,ExtWeights[i].weight);
         
      //--- цена Ask равна цене Bid
      synth_tick[0].ask=synth_tick[0].bid;
      
      //--- добавляем в ценовую историю пользовательского инструмента новый тик
      CustomTicksAdd(custom_symbol,synth_tick);
     }
  }

Здесь мы в цикле получаем тики всех инструментов корзины, одновременно рассчитывая время тика. За время тика берётся время последнего пришедшего тика из всех символов корзины инструмента. Если получены тики от каждого инструмента корзины, то рассчитываем значение цены Bid тика синтетического инструмента по формуле расчёта индекса. После расчёта цены Bid приравниваем значение цены Ask к значению рассчитанной цены Bid синтетического инструмента. По окончании всех расчётов добавляем в ценовую историю пользовательского символа данные из массива тиков synth_tick, эмулируя тем самым приход нового тика синтетического инструмента.

Все функции для создания индексов валют и для работы с их данными готовы. Теперь, на основе созданных функций, напишем программы-сервисы для создания индекса американского доллара и индекса евро.


Тестируем программы-сервисы

В папке, в которой написали файл включаемых функций \MQL5\Services\Indexes\, создадим новый файл программы с типом Сервис с именем USD_Index.mq5.

Сразу же определим макроподстановки для имени пользовательского символа и его группы. Укажем количество символов в корзине инструментов и базовый коэффициент.

Подключим файл написанных выше функций для работы с индексами валют и определим структуру символа корзины, указав наименования всех символов и их веса:

//+------------------------------------------------------------------+
//|                                                    USD_Index.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property service
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define CUSTOM_SYMBOL     "USDX.synthetic"
#define CUSTOM_GROUP      "Synthetics"
#define BASKET_SIZE       6
#define MAIN_COEFF        50.14348112

#include "CurrencyIndex.mqh"

SymbolWeight ExtWeights[BASKET_SIZE]=
  {
     { "EURUSD",-0.576 },
     { "USDJPY", 0.136 },
     { "GBPUSD",-0.119 },
     { "USDCAD", 0.091 },
     { "USDSEK", 0.042 },
     { "USDCHF", 0.036 } 
  };

Теперь в единственном обработчике сервиса OnStart() инициализируем пользовательский символ и запустим обработку поступающих новых тиков в бесконечном цикле:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(!InitService(CUSTOM_SYMBOL,CUSTOM_GROUP))
      return;
//---
   while(!IsStopped())
     {
      ProcessTick(CUSTOM_SYMBOL);
      Sleep(10);
     }
//---
   Print(CUSTOM_SYMBOL," datafeed stopped");
  }

Скомпилируем файл сервиса и запустим его:


После добавления сервиса и его запуска, в журнале увидим сообщения:

USD_Index       open chart USDX.synthetic,M1
USD_Index       31117 EURUSD,M1 rates from 2024.07.29 00:00
USD_Index       31101 USDJPY,M1 rates from 2024.07.29 00:01
USD_Index       31114 GBPUSD,M1 rates from 2024.07.29 00:00
USD_Index       31112 USDCAD,M1 rates from 2024.07.29 00:01
USD_Index       30931 USDSEK,M1 rates from 2024.07.29 00:00
USD_Index       31079 USDCHF,M1 rates from 2024.07.29 00:01
USD_Index       start date set to 2024.07.29 00:01:00
USD_Index       31119 USDX.synthetic,M1 rates from 2024.07.29 00:01:00 to 2024.08.27 14:45:00 added
USD_Index       44 EURUSD ticks from 2024.08.27 14:45:01
USD_Index       45 USDJPY ticks from 2024.08.27 14:45:00
USD_Index       40 GBPUSD ticks from 2024.08.27 14:45:00
USD_Index       45 USDCAD ticks from 2024.08.27 14:45:00
USD_Index       66 USDSEK ticks from 2024.08.27 14:45:00
USD_Index       29 USDCHF ticks from 2024.08.27 14:45:00
USD_Index       12 ticks from 2024.08.27 14:46:02:319 prepared
USD_Index       12 ticks applied
USD_Index       USDX.synthetic datafeed started

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


Теперь создадим сервис для создания и работы с графиком индекса евро.

В папке \MQL5\Services\Indexes\, создадим новый файл программы с типом Сервис с именем EUR_Index.mq5:

//+------------------------------------------------------------------+
//|                                                    EUR_Index.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property service
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define CUSTOM_SYMBOL     "EURX.synthetic"
#define CUSTOM_GROUP      "Synthetics"
#define BASKET_SIZE       5
#define MAIN_COEFF        34.38805726

#include "CurrencyIndex.mqh"

SymbolWeight ExtWeights[BASKET_SIZE]=
  {
   { "EURUSD", 0.3155 },
   { "EURGBP", 0.3056 },
   { "EURJPY", 0.1891 },
   { "EURCHF", 0.1113 }, 
   { "EURSEK", 0.0785 }
  };
//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(!InitService(CUSTOM_SYMBOL,CUSTOM_GROUP))
      return;
//---
   while(!IsStopped())
     {
      ProcessTick(CUSTOM_SYMBOL);
      Sleep(10);
     }
//---
   Print(CUSTOM_SYMBOL," datafeed stopped");
  }
//+------------------------------------------------------------------+

Всё отличие от файла программы-сервиса для создания и работы с графиком индекса американского доллара — это размер корзины символов, базовый коэффициент и структура символов корзины с их весами.

Скомпилируем программу и запустим сервис. В журнале увидим записи:

EUR_Index       open chart EURX.synthetic,M1
EUR_Index       31148 EURUSD,M1 rates from 2024.07.29 00:00
EUR_Index       31137 EURGBP,M1 rates from 2024.07.29 00:01
EUR_Index       31148 EURJPY,M1 rates from 2024.07.29 00:01
EUR_Index       31123 EURCHF,M1 rates from 2024.07.29 00:00
EUR_Index       30898 EURSEK,M1 rates from 2024.07.29 00:00
EUR_Index       start date set to 2024.07.29 00:01:00
EUR_Index       31151 EURX.synthetic,M1 rates from 2024.07.29 00:01:00 to 2024.08.27 15:16:00 added
EUR_Index       53 EURUSD ticks from 2024.08.27 15:16:00
EUR_Index       65 EURGBP ticks from 2024.08.27 15:16:00
EUR_Index       109 EURJPY ticks from 2024.08.27 15:16:00
EUR_Index       68 EURCHF ticks from 2024.08.27 15:16:00
EUR_Index       57 EURSEK ticks from 2024.08.27 15:16:00
EUR_Index       15 ticks from 2024.08.27 15:17:00:877 prepared
EUR_Index       15 ticks applied
EUR_Index       EURX.synthetic datafeed started

Далее будет открыт график индекса евро, с которым можно полноценно работать:


Заключение

На примере программ-сервисов мы научились создавать пользовательские символы и поддерживать полноценное обновление их графиков. При перезапуске терминала также будут перезапущены и все сервисы, которые работали в момент закрытия клиентского терминала. Таким образом, единожды запустив сервис, он будет автоматически стартовать при запуске терминала. А данные сервисы будут постоянно "прокидывать" на график новые тики, постоянно поддерживая в актуальном состоянии историю синтетического инструмента.

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

Все файлы данного проекта прикреплены к статье, и их можно использовать как есть, либо на их основе создать что-либо для собственных нужд по собственным корзинам инструментов. Файл архива MQL5.zip, также прилагаемый к статье, можно просто распаковать в свой рабочий каталог терминала MQL5\ и все файлы проекта будут сразу помещены в нужные подпапки.


Прикрепленные файлы |
CurrencyIndex.mqh (51.64 KB)
USD_Index.mq5 (1.41 KB)
EUR_Index.mq5 (1.38 KB)
MQL5.zip (8.76 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Универсальная формула оптимизации (GOF) при реализации режима Custom Max с ограничениями Универсальная формула оптимизации (GOF) при реализации режима Custom Max с ограничениями
В статье представлен способ реализации задач оптимизации с несколькими целями и ограничениями при выборе режима Custom Max в настройках терминала MetaTrader 5. Например, задача оптимизации может быть следующей: максимизировать фактор прибыли, чистую прибыль и фактор восстановления таким образом, чтобы просадка была менее 10%, количество последовательных убытков было менее 5, а количество сделок в неделю было более 5.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Иерархический векторный Transformer (Окончание) Нейросети в трейдинге: Иерархический векторный Transformer (Окончание)
Продолжаем изучение метода Иерархического Векторного Transformer. И в данной статье мы завершим построение модели. А также проведем её обучение и тестирование на реальных исторических данных.