Как получить синхронизированные массивы для использования в алгоритмах портфельной торговли
Введение
По мере эволюции трейдера в торговле, он почти неизбежно приходит к необходимости анализа и использования в торговле портфеля инструментов. Перечислим некоторые варианты портфельной торговли и портфельного анализа:
- nорговля спредом: спот — фьючерс на этот спот;
- анализ корреляций инструментов;
- различные хэджирующие стратегии торговли;
- анализ взаимного движения инструментов (например, так называемая "сила" валют).
Все перечисленные выше варианты портфельной торговли и/или портфельного анализа требуют, чтобы обрабатываемые данные (бары) инструментов были синхронизированы между собой по времени. Иначе результат может сильно отличаться от реального и от задуманного в математическом алгоритме обработки данных. Это особенно актуально на бирже, где встречается большое количество малоликвидных инструментов с многочисленными отсутствующими барами.
Постановка и обсуждение задачи
Постановка задачи: требуется создать библиотеку классов, решающую проблему синхронизации данных между инструментами портфеля в заданном интервале времени. На выходе пользователь должен получить массивы данных (OHLCV-баров) одинаковой размерности, с одинаковыми (синхронными) временами открытия всех баров в заданном временном интервале. На вход пользователь асинхронно по времени передает массивы несинхронизированных данных (OHLCV-баров) инструментов портфеля.
При синхронизации массивов данных с OHLCV-барами возможны два подхода:
- Если в каком-либо из инструментов из портфеля нет бара для данного момента времени (времени открытия бара), то считаем, что этого бара нет и на всех остальных инструментах. Нет с точки зрения возможности полного расчета портфеля. Бар добавляется и помечается как "пустой" на этом инструменте. То есть, фактически, на данном баре получаем "дырку" в портфеле или на графике.
- В описанном выше случае заполняем отсутствующий бар предыдущим баром для данного инструмента. Представляется, что этот подход более правильный — логично, что, если цена для какого-либо инструмента на протяжении времени не менялась, то для отсутствующего бара все OHLC-цены будут равны цене закрытия предыдущего бара. Объем в этом случае равен нулю, поскольку не было сделок.
Мы реализуем оба подхода для большей гибкости. Рассмотрим принцип синхронизации более подробно (см. Рис.1):

Рис.1. Алгоритм синхронизации данных (массивов OHLCV-баров)
Synch Symbol (S) — символ синхронизации (обычно это символ графика для индикаторов и экспертов). Схематично представлен временной ряд из восьми баров с моментами времени открытия баров t[0]...t[7]. Будем считать, что символ синзронихации не содержит "дыр". Первый бар выделен светло-желтым цветом и обозначен как First — он соответствует моменту времени t[0]. Последний по времени бар выделен темно-зеленым цветом и обозначен как Last — он соответствует моменту времени t[7]. Светло-зеленым цветом с надписью Prev выделены остальные бары между первым и последним.
Other Portfolio's Symbols (Pi) — это все остальные символы нашего портфеля, которые нам предстоит синхронизировать с символом S. Они обрабатываются по одинаковому алгоритму — на рисунке схематично изображен один такой временной ряд. Обозначения баров такие же, как у символа S. Отличия в том, что, во-первых, этот временной ряд содержит пустые бары — красного цвета с надписью Empty. Это бары, где в интервале времени выбранного на графике тайм-фрейма не было "тиков", то есть не было изменений цен Bid/Ask/Last. Во-вторых, длина (число баров) этого временного ряда, с учетом пустых баров, больше длины ряда символа синхронизации.
Таким образом, нам нужно выполнить две операции с рядом Pi: заполнить "дыры" в ряде Pi и обрезать крайний левый (см. Рис.1) бар, чтобы выровнять его длину с длиной ряда синхронизации. Двигаясь по ряду S, мы сравниваем моменты времени от t[7] до t[0] с моментами времни в ряде Pi и для всех остальных символов портфеля. При обнаружении отсутствующего бара в ряде Pi, мы вставляем на это место искуственно созданный бар — либо пустой с ценами равными нулю, либо с ценами, равными цене Close предыдущего не пустого бара. Это зависит от заданного пользователем режима синхронизации.
После этого удаляем элемент First из ряда Pi, чтобы уравнять длины рядов S и Pi. В данном примере такой бар (на удаление) только один.
В результате такой обработки мы получаем ряд Pi* одинаковой длины с рядом S и с барами, полностью соответствующими по времени ряду синхронизации S. Искуственно созданные бары на Рис.1 выделены розовым цветом с надписью Close — это подчеркивает, что здесь содержится цена закрытия предыдущего бара.
Еще один вопрос, на который необходимо ответить — по какому из символов портфеля синхронизировать все остальные? Здесь также возможны несколько подходов:
- Символ синхронизации задается пользователем. При этом, надо понимать, что неправильно выбранный синхронизирующий символ может привести к артефактам на графике индикатора и/или не правильным результатам расчетов по алгоритму пользователя.
- Символ синхронизации автоматически берется с того графика, на котором запущен индикатор или эксперт. Данный способ не подходит для сервисов, которые относительно недавно появились в терминале Meta Trader 5, поскольку они не привязаны к графику.
В большинстве случаев рекомендуется выбирать символ графика для синхронизации и запускать индикатор/эксперт на самом ликвидном инструменте портфеля. Это почти единственныый вариант, если мы разрабатываем портфельный индикатор с привязкой к графику.
Есть еще один момент, который необходимо учитывать при синхронизации — направление расположения данных в массивах. Оно может быть как прямое — самый новый бар находится в конце массива, так и обратное — самый новый бар находится в начале массива (с нулевым индексом). Мы реализуем автоматический учет направления данных.
И последний момент — это несинхронное появление новых баров по отдельным интсрументам. Предлагаются следующие варианты обработки данной ситуации:
- Ожидать появления нового бара на всех инструментах и только после этого проводить обработку по заданному пользователем алгоритму.
- Пересчитывать последний бар несколько раз (по числу инструментов) при появлении нового бара на каждом инструменте портфеля. При этом, для "отстающих" инструментов в расчетах используется предыдущий бар.
Думается, что второй вариант в большинстве случаев предпочтительней, поскольку позволяет получить результат обработки портфеля (возможно и торгового сигнала) без задержки. Обратная сторона "медали" — сигнал при появлении одного или нескольких новых баров может измениться несколько раз.
Здесь рекомендуется подходить с такой логикой: если в портфеле все инструменты ликвидные, то использовать первый вариант (ожидать появления новых баров на всех инструментах). Если же в портфеле есть малоликвидные инструменты, то использовать второй вариант (с режимом интерполяции пропущенных баров).
Анализ имеющихся программных библиотек в составе терминала Meta Trader 5
Стандартная библиотека Generic содержит в своем составе ассоциативные массивы CHashMap и CSortedMap. Они позволяют создавать собственные классы-контейнеры для хранения разнородных данных вида [Ключ-Значение]. В нашем случае, очевидно, ключом будет момент времени (а точнее, время открытия бара). Ассоциативные массивы дают очень быстый доступ к элементам по ключу (времени). При этом, каждый ключ (время) уникален в пределах массива. Что нам и нужно для целей синхронизации баров по времени.
Поскольку при синхронизации массивов данных будут использоваться операции вставки/удаления/добавления элементов (баров) в произвольных местах, нужно получать сортированные по ключу массивы данных (баров). Следовательно, оптимально будет использовать класс CSortedMap. Он обеспечивает автоматическую сортировку по ключу при изменении содержимого массива элементов. Реализация CSortedMap основана на так называемых "красно-черных деревьях" (red-black tree), что обеспечивает балансировку доступа к данным по скоростии и по занимаемому объему памяти.
Таким образом, для реализации задуманного нам понадобится класс-контейнер для хранения данных (баров) и класс-манипулятор данными (менеджер данных) для взаимодействия с "внешним миром" — загрузка массивов данных, получение синхронизированных массивов данных, установка и получение различных параметров и т.д.
Проектирование класса-контейнера для хранения данных CSymbolData
Класс CSymbolData унаследован от стандартного класса CObject для того, чтобы можно было использовать стандартные библиотеки, идущие в составе терминала. Полный код находится в файле "TimeSyncManager.mqh", который прилагается к данной статье. Упрощенный код представлен ниже.
Рассмотрим особенности реализации подробней:
Мы добавили перечисляемый тип данных ENUM_BAR_TYPE, по которому можно определить тип бара в массиве данных символа. Декларируется три типа баров — реальный бар (полученный в терминале), пустой бар (изначально отсутствующий во временном ряде) и бар интерполированный (полученный из предыдущего реального бара). Этот тип данных понадобится нам при реализации алгоритма синхронизации с интерполяцией пропущенных баров.
enum ENUM_BAR_TYPE { ENUM_BAR_LIVE, // реальный бар ENUM_BAR_INTERPOLATED, // бар получен интерполяцией из предыдущего ENUM_BAR_EMPTY // отсутствующий бар (пустой) };
Все доступные в терминале данные временного ряда хранятся в классе CSymbolData в виде сортированных ассоциативных массивов шаблонного типа CSortedMap<Key, Value>, где Key во всех массивах это время открытия бара, а Value — это OHLCV-данные временного ряда.
Такая структура контейнера данных дает быстрый доступ по заданному времени как ко всему массиву данных, так и к отдельному его бару. Также, при необходимости, можно легко организовать индексный доступ к отдельным элементам временного ряда (барам).
Дополнительно к стандартным массивам данных временного ряда добавлен массив для хранения типа каждого бара:
- CSortedMap<datetime, ENUM_BAR_TYPE> bar_type;
В остальном набор данных совпадает с полями встроенной в терминал Meta Trader 5 структуры MqlRates. Чтобы не усложнять дополнительно доступ к членам-данным, они сделаны публичными.
// Контейнер данных: class CSymbolData : public CObject { protected: string symbol; // название символа ENUM_TIMEFRAMES data_timeframe; // ТФ ланных datetime new_bar_time; // время появления последнего нового бара public: CSortedMap<datetime, double> open; // цены Open CSortedMap<datetime, double> high; // цены High CSortedMap<datetime, double> low; // цены Low CSortedMap<datetime, double> close; // цены Close CSortedMap<datetime, long> tick_volume; // тиковый объем CSortedMap<datetime, long> real_volume; // биржевой объем CSortedMap<datetime, int> spread; // спред CSortedMap<datetime, datetime> time; // время CSortedMap<datetime, ENUM_BAR_TYPE> bar_type; // тип бара public: // Конструктор: CSymbolData(const string _symbol_name, const ENUM_TIMEFRAMES _data_timeframe) : symbol(_symbol_name), data_timeframe(_data_timeframe), new_bar_time(0) { } // Конструктор без параметров: CSymbolData() : symbol(NULL), data_timeframe(PERIOD_CURRENT), new_bar_time(0) { } // Добавление баров данных: int AddFullData(const MqlRates& _rates[], const int _src_start_index, const int _count, const bool _keep_old_data = false) { int size = MathMin(_count, (int)_rates.Size()); if(size > 0) { // Учтем направление расположения данных во входном массиве: bool as_series = ArrayGetAsSeries(_rates); if(as_series == true) { ArraySetAsSeries(_rates, false); } int start_index = _src_start_index; if(_src_start_index <= 0) { start_index = 0; } // Цикл перекачки данных: datetime dt ; int index; for(int i = 0; i < size; i++) { index = start_index + i; dt = _rates[index].time; this.open.Add(dt, _rates[index].open); this.high.Add(dt, _rates[index].high); this.low.Add(dt, _rates[index].low); this.close.Add(dt, _rates[index].close); this.tick_volume.Add(dt, _rates[index].tick_volume); this.real_volume.Add(dt, _rates[index].real_volume); this.spread.Add(dt, _rates[index].spread); this.bar_type.Add(dt, ENUM_BAR_LIVE); this.time.Add(dt, dt); } // Время появления последнего нового бара: this.new_bar_time = _rates[start_index + size - 1].time; // Восстановим направление расположения данных во входном массиве: ArraySetAsSeries(_rates, as_series); return(size); } return(0); } // Обновление и/или добавление (при необходимости) последних баров: int UpdateLastData(const MqlRates& _rates[], const int _start_index, const int _count) { int size = MathMin(_count, (int)_rates.Size()); if(size > 0) { // Учтем направление расположения данных во входном массиве: bool as_series = ArrayGetAsSeries(_rates); if(as_series == false) { ArraySetAsSeries(_rates, true); } datetime dt; int index; // Цикл перекачки данных: for(int i = 0; i < size; i++) { index = _start_index + i; dt = _rates[index].time; // Если бар с таким временем уже есть в списке, то просто ОБНОВИМ данные для него: if(this.time.ContainsKey(dt) == true) { this.open.TrySetValue(dt, _rates[index].open); this.high.TrySetValue(dt, _rates[index].high); this.low.TrySetValue(dt, _rates[index].low); this.close.TrySetValue(dt, _rates[index].close); this.tick_volume.TrySetValue(dt, _rates[index].tick_volume); this.real_volume.TrySetValue(dt, _rates[index].real_volume); this.spread.TrySetValue(dt, _rates[index].spread); } // Бара с таким временем нет в списке, значит добавим НОВЫЙ БАР: else { this.open.Add(dt, _rates[index].open); this.high.Add(dt, _rates[index].high); this.low.Add(dt, _rates[index].low); this.close.Add(dt, _rates[index].close); this.tick_volume.Add(dt, _rates[index].tick_volume); this.real_volume.Add(dt, _rates[index].real_volume); this.spread.Add(dt, _rates[index].spread); this.bar_type.Add(dt, ENUM_BAR_LIVE); this.time.Add(dt, dt); // Время появления последнего нового бара: this.new_bar_time = _rates[index].time; } } // Восстановим направление расположения данных во входном массиве: ArraySetAsSeries(_rates, as_series); return(size); } return(0); } };
Основные публичные методы загрузки внешних данных в контейнер:
int AddFullData(const MqlRates& rates[], const int src_start_index, const int count, const bool keep_old_data = false) int UpdateLastData(const MqlRates& rates[], const int src_start_index, const int count)
- Метод CSymbolData::AddFullData:
Предназначен для загрузки массива MqlRates-структур во внутренние массивы. При этом, горизонтальное построение массива структур (когда каждый отдельный срез данных хранится в отдельной структуре) преобразуется в вертикальное с разбивкой на отдельные массивы OHLCV-цен.
Для наших целей это более удобно, поскольку мы уже не привязаны к формату структуры MqlRates. Это позволяет легко, без изменения логики класса, добавить вычисляемые массивы данных параллельно уже существующим OHLCV-барам. Например, легко можно добавить массив одного из вариантов типичных цен, вычисляемых как сумма четырех цен бара, деленная на четыре: (O+H+L+C)/4. И т.п.
На вход метода CSymbolData::AddFullData подается:
- [in] MqlRates rates[] — массив структур, который может заполняться штатной функцией языка MQL5 - CopyRates;
- [in] int src_start_index — стартовый индекс, начиная с которого, берутся данные из источника данных - массива rates[];
- [in] int count — задает сколько баров загрузить во внутренние массивы контейнера из входного массива rates[];
- [in] bool keep_old_data — флаг необходимости очистки внутренних массивов перед загрузкой данных (если равен истине, то новые данные добавляются в конец уже существующих).
Мы учитываем направление расположения данных в массиве rates[] с помощью кода:
bool as_series = ArrayGetAsSeries(rates); if(as_series == true) { ArraySetAsSeries(rates, false); }
Для правильной работы синхронизатора требуется возрастание времени в массиве от первого элемента к последнему — самый новый элемент должен находиться в конце массива.
Логика загрузки данных следующая: проходим массив rates[] поэлементно в цикле и добавляем OHLCV-данные с помощью метода CSortedMap::Add в наши внутренние ассоциативные массивы. Ключом является значение поля MqlRates::time.
После загрузки заданного числа баров запоминаем время появления последнего нового бара — это время открытия последнего бара в массиве данных:
this.new_bar_time = rates[start_index + size - 1].time;
Метод CSymbolData::AddFullData возвращает число реально загруженных данных (баров) или ноль в случае неуспеха.
- Метод CSymbolData::UpdateLastData:
Предназначен для обновления / добавления заданного числа последних по времени баров во внутренних массивах контейнера. Как правило, вызывается из обработчика события прихода нового тика в терминале Meta Trade 5.
На вход метода CSymbolData::UpdateLastData подается:
- [in] MqlRates rates[] — массив структур с последним баром (или несколькими последними барами), который, например, может заполняться штатной функцией языка MQL5 - CopyRates;
- [in] int src_start_index — стартовый индекс, начиная с которого, берутся данные из источника данных - массива rates[];
- [in] int count — задает сколько баров загрузить во внутренние массивы контейнера из входного массива rates[].
Принципиальное отличие метода CSymbolData::UpdateLastData от предыдущего в том, что здесь мы сначала проверяем, есть ли уже бар с таким временем в наших внутренних массивах. Если бар с таким временем уже есть, то мы обновляем (перезаписываем) значения OHLCV-данных в существующем элементе (баре) на новые. Время (ключ), естественно, остается без изменения.
Если бара с таким временем нет, то мы добавляем новый бар в массивы и переписываем время прихода последнего нового бара. Проверка наличия бара и обновление/добавление выполняется с помощью кода:
if(this.time.ContainsKey(dt) == true) { this.open.TrySetValue(dt, _rates[index].open); this.high.TrySetValue(dt, _rates[index].high); this.low.TrySetValue(dt, _rates[index].low); this.close.TrySetValue(dt, _rates[index].close); this.tick_volume.TrySetValue(dt, _rates[index].tick_volume); this.real_volume.TrySetValue(dt, _rates[index].real_volume); this.spread.TrySetValue(dt, _rates[index].spread); } else { this.open.Add(dt, _rates[index].open); this.high.Add(dt, _rates[index].high); this.low.Add(dt, _rates[index].low); this.close.Add(dt, _rates[index].close); this.tick_volume.Add(dt, _rates[index].tick_volume); this.real_volume.Add(dt, _rates[index].real_volume); this.spread.Add(dt, _rates[index].spread); this.bar_type.Add(dt, ENUM_BAR_LIVE); this.time.Add(dt, dt); this.new_bar_time = _rates[index].time; }
Метод CSymbolData::UpdateLastData возвращает число реально загруженных/обновленных данных (баров) или ноль в случае неуспеха.
Основные публичные методы получения данных из контейнера:
- Группа методов получения полного массива данных CSymbolData::GetFullXXXX.
int GetFullOpen(double& dst[], const int dst_start_index) int GetFullHigh(double& dst[], const int dst_start_index) int GetFullLow(double& dst[], const int dst_start_index) int GetFullClose(double& dst[], const int dst_start_index) int GetFullTickVolume(long& dst[], const int dst_start_index) int GetFullRealVolume(long& dst[], const int dst_start_index) int GetFullSpread(int& dst[], const int dst_start_index)
Позволяет получить в заданный входной массив все содержимое внутреннего массива данных. Все эта группа методов вызывается одинаково, отличие только в типе данных. Рассмотрим на примере метода CSymbolData::GetFullClose:
На вход метода CSymbolData::GetFullClose подается:
- [out] double& dst[] — массив-приемник данных, в который записывается ценовой массив Close[]. Этим массивом может быть, в том числе, индикаторный буфер;
- [in] int dst_start_index — стартовый индекс в массиве-приемнике, начиная с которого будут записываться данные.
Код метода CSymbolData::GetFullClose представлен ниже:
// ===================================================================== // Получить данные Close[]: // ===================================================================== #define COPY_FULL_DATA(data_type, data_src, data_dst, dst_start) \ {\ if(data_src.Count() > 0) \ // если данные загружены {\ bool as_series = ArrayGetAsSeries(data_dst); \ if(as_series == true) \ // учтем направление расположения данных во входном массиве { ArraySetAsSeries(data_dst, false); } \ datetime dt[]; \ data_type val[]; \ int data_copied = data_src.CopyTo(dt, val); \ for(int i = 0; i < data_src.Count(); i++) \ // копируем заданное число данных {\ data_dst[dst_start + i] = val[i]; \ // копируем столько данных, сколько реально есть }\ ArraySetAsSeries(data_dst, as_series); \ // восстановим направление расположения данных во входном массиве return(data_copied); \ }\ return(0);\ } // ===================================================================== int GetFullClose(double& dst[], const int dst_start_index) { COPY_FULL_DATA(double, this.close, dst, dst_start_index); } // =====================================================================
Поскольку для всех типов OHLCV-данных методы, фактически, одинаковы (разница только в типах данных), мы оформили тело методов в виде макроса COPY_FULL_DATA с параметрами:
#define COPY_FULL_DATA(data_type, data_src, data_dst, dst_start) Назначение параметров макроса следуюшее:
- [in] data_type — тип массива-приемника данных. Может быть один из следующих типов: "double" (для цен Open/High/Low/Close, "long" (для объемов TickVolume и RealVolume), "int" (для спреда Spread);
- [in] data_src — название внутреннего массива для соответствующего типа ценовых данных: this.open/this.high/this.low/this.close/this.tick_volume/this.real_volume/this.spread;
- [out] data_dst — массив-приемник данных dst[];
- [in] dst_start — индекс, начиная с которого будут записываться данные в массив-приемник.
Методы CSymbolData::GetFullXXXX возвращают число реально загруженных/обновленных данных (баров) или ноль в случае неуспеха. Направление расположения данных в массиве dst[] учитывается автоматически.
- Группа методов получения заданного числа последних по времни данных из массива CSymbolData::GetLastXXXX.
void GetLastTime(const int count, datetime& dst[], const int dst_start_index) void GetLastOpen(const int count, double& dst[], const int dst_start_index) void GetLastHigh(const int count, double& dst[], const int dst_start_index) void GetLastLow(const int count, double& dst[], const int dst_start_index) void GetLastClose(const int count, double& dst[], const int dst_start_index) void GetLastTickVolume(const int count, long& dst[], const int dst_start_index) void GetLastRealVolume(const int count, long& dst[], const int dst_start_index) void GetLastSpread(const int count, int& dst[], const int dst_start_index)
Позволяет получить в переданный во входных параметрах массив-приемник заданное число баров из внутреннего массива данных. Все эта группа методов вызывается одинаково, отличие только в типе данных. Рассмотрим на примере метода CSymbolData::GetLastClose:
На вход метода CSymbolData::GetLastClose подается:
- [in] int count — число данных, которое нужно записать в массив-приемник;
- [out] double& dst[] — массив-приемник данных, в который записывается ценовой массив Close[]. Этим массивом может быть, в том числе, индикаторный буфер;
- [in] int dst_start_index — стартовый индекс в массиве-приемнике, начиная с которого будут записываться данные.
// ===================================================================== // Копирование только последних обновленных данных: // ===================================================================== #define COPY_LAST_DATA(data_type, data_src, count, data_dst, dst_start) \ {\ if(data_src.Count() > 0) \ // если данные загружены {\ bool as_series = ArrayGetAsSeries(data_dst); \ if(as_series == true) \ // учтем направление расположения данных во входном массиве { ArraySetAsSeries(data_dst, false); } \ datetime dt[]; \ data_type val[]; \ data_src.CopyTo(dt, val); \ for(int i = 0; i < count; i++) \ // копируем заданное число данных {\ data_dst[dst_start + i] = val[this.time.Count() - count + i]; \ }\ ArraySetAsSeries(data_dst, as_series); \ // восстановим направление расположения данных во входном массиве }\ } // ===================================================================== // Получить обновленные данные Close[]: // ===================================================================== void GetLastClose(const int _count, double& _dst[], const int _dst_start_index) { COPY_LAST_DATA(double, this.close, _count, _dst, _dst_start_index); } // =====================================================================
Поскольку для всех типов OHLCV-данных методы, фактически, одинаковы (разница только в типах данных) — мы оформили тело методов в виде макроса COPY_LAST_DATA с параметрами:
#define COPY_LAST_DATA(data_type, data_src, count, data_dst, dst_start) Назначение параметров макроса следуюшее:
- [in] data_type — тип массива-приемника данных. Может быть один из следующих типов: "double" (для цен Open/High/Low/Close, "long" (для объемов TickVolume и RealVolume), "int" (для спреда Spread);
- [in] data_src — название внутреннего массива для соответствующего типа ценовых данных: this.open/this.high/this.low/this.close/this.tick_volume/this.real_volume/this.spread;
- [in] count — данных (элементов массива), которое копируется в массив-приемник;
- [out] data_dst — массив-приемник данных dst[];
- [in] dst_start — индекс, начиная с которого будут записываться данные в массив-приемник.
Число данных отсчитывается от последнего элемента массива data_src[]. Направление расположения данных в массиве dst[] учитывается автоматически.
Методы CSymbolData::GetLastXXXX возвращают число реально загруженных/обновленных данных (баров) или ноль в случае неуспеха.
Проектирование класса-синхронизатора CTimeSyncManager
Назначение класса CTimeSyncManager — получать на входе исторические данные от нескольких инструментов, синхронизировать их между собой и предоставлять полный доступ к массивам синхронизированных данных. Полный код класса CTimeSyncManager находится в файле "TimeSyncManager.mqh", который прилагается к данной статье. Упрощенный код представлен ниже.
Рассмотрим особенности реализации подробней. Мы добавили перечисляемые типы данных:
enum ENUM_SYNC_BARS_TYPE { ENUM_SYNC_BARS_INTERPOLATED, // пустой бар заменяем предыдущим ENUM_SYNC_BARS_EMPTY // добавляем пустой бар };
- ENUM_SYNC_BARS_TYPE — позволяет задать тип синхронизации баров, который должен использоваться. Определяет как будут заменяться "пропущенные" бары — либо пустым баров, либо используем цену Close предыдущего реального бара (смотри пример на Рис.1).
enum ENUM_SYNC_SYMBOL_TYPE { ENUM_SYNC_SYMBOL_CUSTOM, // синхронизируем по заданному ENUM_SYNC_SYMBOL_AUTO // автоматическая синхронизация };
- ENUM_SYNC_SYMBOL_TYPE — позволяет задать способ выбора символа для синхронизации баров. Используется в настройках индикатора/эксперта - либо прямое задание имени символа-синхронизатора, либо автоматический выбор символа графика. на котором установлен индикатор/эксперт.
enum ENUM_NEW_BARS_TYPE { ENUM_NEW_BARS_WAITING_ALL, // ждем появления нового бара на всех инструментах ENUM_NEW_BARS_UPDATE_ALL // при появлении нового бара берем предыдущий бар для остальных (если нет нового) };
- ENUM_NEW_BARS_TYPE — позволяет задать тип генерации события о появлении нового бара. Используется в настройках индикатора/эксперта - либо ожидаем появления нового бара нв всех инструментах портфеля, и только после этого фиксируем факт появления интегрального нового бара, либо запускаем алгоритм обработки данных в индикаторе/эксперте при появлении нового бара на каждом инструменте портфеля.
Упрощенный код класса CTimeSyncManager приведен ниже:
class CTimeSyncManager { protected: ENUM_SYNC_BARS_TYPE sync_bars_type; // тип синхронизации баров ENUM_SYNC_SYMBOL_TYPE sync_symbol_type; // выбор символа для синхронизации баров ENUM_NEW_BARS_TYPE new_bars_type; // тип ожидания появления новых баров protected: bool is_syncronized_FLAG; // true - все данные синхронизированы protected: CSymbolData* sync_symbol_data; // инструмент синхронизации CSymbolData* symbols_data_Array[]; // инструменты портфеля public: // ===================================================================== // Конструктор: // ===================================================================== CTimeSyncManager() : sync_bars_type(ENUM_SYNC_BARS_INTERPOLATED), sync_symbol_type(ENUM_SYNC_SYMBOL_CUSTOM), new_bars_type(ENUM_NEW_BARS_WAITING_ALL), is_syncronized_FLAG(false), sync_symbol_data(NULL) { } // ===================================================================== // Добавление символа синхронизации с данными: // ===================================================================== int SyncSymbolAddFullData(const string _symbol, const ENUM_TIMEFRAMES _data_timeframe, const MqlRates& _rates[], const int _src_start, const int _count, const bool _keep_old_data = false) { this.is_syncronized_FLAG = false; if(this.sync_symbol_data == NULL) { this.sync_symbol_data = new CSymbolData(_symbol, _data_timeframe); } // Добавим сами данные в список: return(this.sync_symbol_data.AddFullData(_rates, _src_start, _count, _keep_old_data)); } // ===================================================================== // Добавление/обновление последних баров символа синхронизации: // ===================================================================== int SyncSymbolUpdateLastData(const MqlRates& _rates[], const int _src_start, const int _count) { this.is_syncronized_FLAG = false; return(this.sync_symbol_data.UpdateLastData(_rates, _src_start, _count)); } // ===================================================================== // Добавление символа с данными в список: // ===================================================================== int SymbolAddFullData(const string _symbol, const ENUM_TIMEFRAMES _data_timeframe, const MqlRates& _rates[], const int _src_start, const int _count, const bool _keep_old_data = false) { this.is_syncronized_FLAG = false; if(this.symbols_data_Array.Size() > 0) { for(int i = 0; i < (int)this.symbols_data_Array.Size(); i++) { // Ищем символ среди уже добавленных: if(this.symbols_data_Array[i].GetSymbolName() == _symbol) { return(this.symbols_data_Array[i].AddFullData(_rates, _src_start, _count, _keep_old_data)); } } } // Если попали сюда, значит символ в списке не найден или список пустой: CSymbolData* curr_symbol = new CSymbolData(_symbol, _data_timeframe); // Добавим данные: int sz = curr_symbol.AddFullData(_rates, _src_start, _count, _keep_old_data); // Добавим сам символ в список: ArrayResize(this.symbols_data_Array, this.symbols_data_Array.Size() + 1); this.symbols_data_Array[this.symbols_data_Array.Size() - 1] = curr_symbol; return(sz); } // ===================================================================== // Добавление/обновление последних баров символа: // ===================================================================== void SymbolUpdateLastData(const string _symbol, const MqlRates& _rates[], const int _src_start, const int _count) { this.is_syncronized_FLAG = false; if(this.symbols_data_Array.Size() > 0) { //CSymbolData* curr_symbol; for(int i = 0; i < (int)this.symbols_data_Array.Size(); i++) { // Ищем символ среди уже добавленных: if(this.symbols_data_Array[i].GetSymbolName() == _symbol) { this.symbols_data_Array[i].UpdateLastData(_rates, _src_start, _count); break; } } } } };
Все обрабатываемые массивы данных хранятся в контейнерах типа CSymbolData - тип, который описан выше в статье. Мы выделили отдельный объект-контейнер для хранения символа синхронизации:
CSymbolData* sync_symbol_data; // инструмент синхронизации
Это сделано для того, чтобы не искать его каждый раз при обращении к методам синхронизации данных. Остальные символы портфеля хранятся в контейнерах, объединеных в общий массив указателей на объекты. Что удобно для перебора элементов в операторах цикла:
CSymbolData* symbols_data_Array[]; // инструменты портфеля
Публичные методы загрузки данных во внутренние контейнеры:
int SyncSymbolAddFullData(const string symbol, const ENUM_TIMEFRAMES data_timeframe, const MqlRates& rates[], const int src_start, const int count, const bool keep_old_data = false) int SyncSymbolUpdateLastData(const MqlRates& rates[], const int src_start, const int count) int SymbolAddFullData(const string symbol, const ENUM_TIMEFRAMES data_timeframe, const MqlRates& rates[], const int src_start, const int count, const bool keep_old_data = false) void SymbolUpdateLastData(const string symbol, const MqlRates& rates[], const int src_start, const int count)
Должны вызываться из кода индикатора/эксперта после получения соответствующих данных в терминале. Есть две группы методов - начальная полная загрузка данных, которые предполагается использовать для отображения и/или расчетов и группа методов для обработки и обновления последнего бара (добавления нового бара) при получении очередного тика в индикаторе/эксперте.
Метод CTimeSyncManager::SyncSymbolAddFullData позволяет загрузить начальные данные символа-синхронизатора во внутренний контейнер для дальнейшей обработки. Имеет следующие параметры:
- [in] string symbol — название (имя) символа-синхронизатора. Берется из подсистемы терминала Meta trader 5. Обязательно к заданию в текстовом виде - пустое значение не допускается.
- [in] ENUM_TIMEFRAMES data_timeframe — тайм-фрейм загружаемого массива данных rates[]. Может отличаться от ТФ графика, на котором установлен индикатор/эксперт. Должен совпадать у всех загружаемых в контейнеры массивов данных.
- [in] MqlRates& rates[] — входной массив с данными (барами).
- [in] int src_start — начальный индекс, с которого будут браться данные из входного массва rates[].
- [in] int count — число баров, которые нужно загрузить из входного массива rates[].
- [in] bool keep_old_data — флаг управления очисткой предыдущих загруженных данных (если они есть в контейнерах). Если задана истина, то предыдущие данные сохраняются.
Метод CTimeSyncManager::SyncSymbolUpdateLastData позволяет обновить данные в контейнере символа-синхронизатора с приходом нового тика (тиков) для последнего по времени бара, либо добавить новый бар в случае его появления. Имеет следующие параметры:
- [in] MqlRates& rates[] — входной массив с данными (барами). Как правило, содержит один бар в своем составе. Либо два при появлении нового бара.
- [in] int src_start — начальный индекс, с которого будут браться данные из входного массва rates[]. Как правило, равен нулю.
- [in] int count — число баров, которые нужно загрузить из входного массива.
Метод CTimeSyncManager::SymbolAddFullData позволяет загрузить начальные данные остальных символов портфеля во внутренние контейнеры для дальнейше обработки. Имеет следующие параметры:
- [in] string symbol — название (имя) символа в портфеле. Берется из подсистемы терминала Meta trader 5. Обязательно к заданию в текстовом виде — пустое значение не допускается.
- [in] ENUM_TIMEFRAMES data_timeframe — тайм-фрейм загружаемого массива данных rates[]. Может отличаться от ТФ графика, на котором установлен индикатор/эксперт. Должен совпадать у всех загружаемых в контейнеры массивов данных.
- [in] MqlRates& rates[] — входной массив с данными (барами).
- [in] int src_start — начальный индекс, с которого будут браться данные из входного массва rates[].
- [in] int count — число баров, которые нужно загрузить из входного массива rates[].
- [in] bool keep_old_data — флаг управления очисткой предыдущих загруженных данных (если они есть в контейнерах). Если задана истина, то предыдущие данные сохраняются.
Метод CTimeSyncManager::SymbolUpdateLastData позволяет обновить данные в контейнере для остальных инструментов портфеля с приходом нового тика (тиков) для последнего по времени бара, либо добавить новый бар в случае его появления. Имеет следующие параметры:
- [in] string symbol — название (имя) символа в портфеле. Берется из подсистемы терминала Meta trader 5. Обязательно к заданию в текстовом виде — пустое значение не допускается.
- [in] MqlRates& rates[] — входной массив с данными (барами). Как правило, содержит один бар в своем составе, либо два при появлении нового бара.
- [in] int src_start — начальный индекс, с которого будут браться данные из входного массва rates[]. Как правило, равен нулю.
- [in] int count — число баров, которые нужно загрузить из входного массива rates[].
Следует учитывать, что тики в индикатор/эксперт приходят только по символу того графика, на котором он установлен, и не совпадают с появлением новых тиков на остальных инструментах. Дальше мы рассмотрим примеры портфельных индикаторов и обсудим этот момент.
Методы доступа к синхронизированным данным:
- Группа методов получения полного массива данных CTimeSyncManager::GetFullXXXX для заданного символа:
int GetFullOpen(const string symbol, double& dst[], const int dst_start_index) int GetFullHigh(const string symbol, double& dst[], const int dst_start_index) int GetFullLow(const string symbol, double& dst[], const int dst_start_index) int GetFullClose(const string symbol, double& dst[], const int dst_start_index) int GetFullTickVolume(const string symbol, long& dst[], const int dst_start_index) int GetFullRealVolume(const string symbol, long& dst[], const int dst_start_index) int GetFullSpread(const string symbol, int& dst[], const int dst_start_index)
Позволяют получить полный массив синхронизированных данных - данные копируются в выходной массив, в качестве которого может быть индикаторный буфер (только для получения цен Open/High/Low/Close). Все методы этой группы вызываются одинаково, различие только в типе выходного массива dst[].
Рассмотрим параметры методов на примере CTimeSyncManager::GetFullClose:
- [in] string symbol — название (имя) символа в портфеле. Берется из подсистемы терминала Meta trader 5. Обязательно к заданию в текстовом виде — пустое значение не допускается.
- [out] double& dst[] — выходной массив-приемник синхронизированных данных.
- [in] int dst_start_index — начальный индекс, начиная с которого будут копироваться данные в выходной массив dst[].
Код метода CTimeSyncManager::GetFullClose представлен ниже:
// ===================================================================== #define GET_FULL_DATA(symbol, dst, dst_start_index, Func) \ {\ if(symbol == this.sync_symbol_data.GetSymbolName()) \ // если это СИМВОЛ СИНХРОНИЗАЦИИ { return(this.sync_symbol_data.Func(dst, dst_start_index)); } \ if(this.symbols_data_Array.Size() > 0) \ // если это НЕ СИМВОЛ СИНХРОНИЗАЦИИ {\ for(int i = 0; i < (int)this.symbols_data_Array.Size(); i++) \ {\ if(this.symbols_data_Array[i].GetSymbolName() == symbol) \ // ищем символ среди уже добавленных { return(this.symbols_data_Array[i].Func(dst, dst_start_index)); } \ }\ }\ return(0); \ } // ===================================================================== // Получить данные Close для заданного символа в виде массива: // ===================================================================== int GetFullClose(const string _symbol, double& _dst[], const int _dst_start_index) { GET_FULL_DATA(_symbol, _dst, _dst_start_index, GetFullClose); }
Поскольку для всех типов OHLCV-данных методы, фактически, одинаковы (разница только в типах данных) - мы оформили тело методов в виде макроса COPY_FULL_DATA с параметрами:
#define GET_FULL_DATA(symbol, dst, dst_start_index, Func) Назначение параметров макроса следуюшее:
- [in] symbol — название (имя) символа в портфеле.
- [out] dst — массив-приемник данных dst[];
- [in] dst_start_index — индекс, начиная с которого будут записываться данные в массив-приемник;
- [in] Func — текстовое название метода в контейнере, с помощью которого должны извлекаться данные.
Методы CTimeSyncManager::GetFullXXXX возвращают число реально скопированных данных или ноль в случае неудачи.
- Группа методов получения последних данных CTimeSyncManager::GetLastXXXX для заданного символа:
void GetLastTime(const string symbol, const int count, datetime& dst[], const int dst_start_index) void GetLastOpen(const string symbol, const int count, double& dst[], const int dst_start_index) void GetLastHigh(const string symbol, const int count, double& dst[], const int dst_start_index) void GetLastLow(const string symbol, const int count, double& dst[], const int dst_start_index) void GetLastClose(const string symbol, const int count, double& dst[], const int dst_start_index) void GetLastSpread(const string symbol, const int count, int& dst[], const int dst_start_index) void GetLastTickVolume(const string symbol, const int count, long& dst[], const int dst_start_index) void GetLastRealVolume(const string symbol, const int count, long& dst[], const int dst_start_index)
Позволяют получить заданное количество последних по времени синхронизированных данных - они копируются в выходной массив, начиная с последнего его элемента. В качестве выъодного массива может быть индикаторный буфер (только для получения цен Open/High/Low/Close). Все методы этой группы вызываются одинаково, различие только в типе выходного массива dst[].
Рассмотрим параметры методов на примере CTimeSyncManager::GetLastClose:
Код метода CTimeSyncManager::GetLastClose представлен ниже:
- [in] string symbol — название (имя) символа в портфеле из подсистемы терминала Meta trader 5. Обязательно к заданию в текстовом виде — пустое значение не допускается.
- [in] int count — число баров, которые нужно скопировать в выходной массив dst[].
- [out] double& dst[] — выходной массив-приемник синхронизированных данных.
- [in] int dst_start_index — начальный индекс, начиная с которого будут копироваться данные в выходной массив dst[].
// ===================================================================== void GetLastClose(const string symbol, const int count, double& dst[], const int dst_start_index) { // Если это СИМВОЛ СИНХРОНИЗАЦИИ: if(symbol == this.sync_symbol_data.GetSymbolName()) { this.sync_symbol_data.GetLastClose(count, dst, dst_start_index); } // Если это НЕ СИМВОЛ СИНХРОНИЗАЦИИ: if(this.symbols_data_Array.Size() > 0) { for(int i = 0; i < (int)this.symbols_data_Array.Size(); i++) { // Ищем символ среди уже добавленных: if(this.symbols_data_Array[i].GetSymbolName() == symbol) { this.symbols_data_Array[i].GetLastClose(count, dst, dst_start_index); break; } } } }
Пояснения к коду: сначала проверяется совпадает ли имя заданного символа с символом синхронизации. Если совпадает, то вызывается метод из объекта класса CSymbolData символа синхронизации. Если не совпадает, то ищется заданный символ среди остальных символов портфеля. В случае успеха вызывается соответствующий метод из объекта класса CSymbolData для найденного символа.
- Группа методов синхронизации полных массивов загруженных данных:
bool SyncAllData(const ENUM_SYNC_BARS_TYPE sync_bars_type) bool CustomSyncAllData(const ENUM_SYNC_BARS_TYPE sync_bars_type)
Позволяет синхронизировать все добавленные в контейнеры данные. Должны быть обязательно загружены данные в символ синхронизации и хотя бы еще один символ портфеля - иначе синхронизация не возможна. Основной метод CTimeSyncManager::SyncAllData, который транзитом вызывает метод CTimeSyncManager::CustomSyncAllData. Это сделано для дальнейшего расширения возможностей класса.
Параметры метода CTimeSyncManager::SyncAllData следующие:
- [in] ENUM_SYNC_BARS_TYPE sync_bars_type — задает тип синхронизации баров. Заполнение пустым баром либо предыдущим значением.
Сам алгоритм синхронизации разбит на три этапа:
Первый этап — двигаясь слева направо (в порядке увеличения времени) по массиву с временем (с ключами) символа синхронизации ищем в остальных символах такое же время (ключ). Если в синхронизируемом символе НЕТ бара с нужным временем, то добавим его в синхронизируемый массив с признаком "пустой бар" — ENUM_BAR_EMPTY. Участок кода, выполняющий этот этап приведен ниже. Индекс "k" — это текущий индекс символа в массиве CTimeSyncManager::symbols_data_Array[].
for(int sync_symbol_key_index = 0; sync_symbol_key_index < sync_symbol_key_size; sync_symbol_key_index++) { sync_symbol_time = sync_symbol_key[sync_symbol_key_index]; if(this.symbols_data_Array[k].time.ContainsKey(sync_symbol_time) != true) { this.symbols_data_Array[k].open.Add(sync_symbol_time, 0); this.symbols_data_Array[k].high.Add(sync_symbol_time, 0); this.symbols_data_Array[k].low.Add(sync_symbol_time, 0); this.symbols_data_Array[k].close.Add(sync_symbol_time, 0); this.symbols_data_Array[k].tick_volume.Add(sync_symbol_time, 0); this.symbols_data_Array[k].real_volume.Add(sync_symbol_time, 0); this.symbols_data_Array[k].bar_type.Add(sync_symbol_time, ENUM_BAR_EMPTY); this.symbols_data_Array[k].spread.Add(sync_symbol_time, 0); this.symbols_data_Array[k].time.Add(sync_symbol_time, sync_symbol_time); to_delete_count++; } }
Поскольку массивы-контейнеры данных построены на ассоциативных массивах с сортировкой, наш добавляемый бар попадает в нужное место отсортированным по возрастанию времени (ключу).
Второй этап — удаляем "лишние" элементы (если они есть) из начала массива. Они образуются при добавлении пустых баров — длина массива увеличивается. Чтобы уравнять длины массивов, нам надо удалить самые "старые" бары. Участок кода, выполняющий этот этап приведен ниже. Индекс "k" — это текущий индекс символа в массиве CTimeSyncManager::symbols_data_Array[].
if(to_delete_count > 0) { datetime _symbol_key[]; datetime _symbol_val[]; this.symbols_data_Array[k].time.CopyTo(_symbol_key, _symbol_val); for(int i = to_delete_count - 1; i >= 0; i--) { this.symbols_data_Array[k].open.Remove(_symbol_key[i]); this.symbols_data_Array[k].high.Remove(_symbol_key[i]); this.symbols_data_Array[k].low.Remove(_symbol_key[i]); this.symbols_data_Array[k].close.Remove(_symbol_key[i]); this.symbols_data_Array[k].tick_volume.Remove(_symbol_key[i]); this.symbols_data_Array[k].real_volume.Remove(_symbol_key[i]); this.symbols_data_Array[k].bar_type.Remove(_symbol_key[i]); this.symbols_data_Array[k].spread.Remove(_symbol_key[i]); this.symbols_data_Array[k].time.Remove(_symbol_key[i]); } }
Третий этап — заменяем добавленные на первом этапе пустые бары значениями предыдущего реального бара. Это делается только в случае если в параметре sync_bars_type задан тип синхронизации с интерполяцией баров ENUM_SYNC_BARS_INTERPOLATED. Участок кода, выполняющий этот этап приведен ниже. Индекс "k" — это текущий индекс символа в массиве CTimeSyncManager::symbols_data_Array[].
if(_sync_bars_type == ENUM_SYNC_BARS_INTERPOLATED) { datetime _symbol_bt_key[]; ENUM_BAR_TYPE _symbol_bt_val[]; this.symbols_data_Array[k].bar_type.CopyTo(_symbol_bt_key, _symbol_bt_val); for(int i = 1; i < (int)_symbol_bt_key.Size(); i++) { if(_symbol_bt_val[i] == ENUM_BAR_EMPTY) { // Получим значения предыдущего бара: this.symbols_data_Array[k].close.TryGetValue(_symbol_bt_key[i - 1], prev_value_dbl); this.symbols_data_Array[k].spread.TryGetValue(_symbol_bt_key[i - 1], prev_value_int); this.symbols_data_Array[k].open.TrySetValue(_symbol_bt_key[i], prev_value_dbl); this.symbols_data_Array[k].high.TrySetValue(_symbol_bt_key[i], prev_value_dbl); this.symbols_data_Array[k].low.TrySetValue(_symbol_bt_key[i], prev_value_dbl); this.symbols_data_Array[k].close.TrySetValue(_symbol_bt_key[i], prev_value_dbl); this.symbols_data_Array[k].spread.TrySetValue(_symbol_bt_key[i], prev_value_int); this.symbols_data_Array[k].bar_type.TrySetValue(_symbol_bt_key[i], ENUM_BAR_INTERPOLATED); } } }
Здесь перебор элементов в цикле начинается со второго элемента (начальный индекс "i" равен единице), чтобы иметь вохможность обратиться к предыдущему элементу.
Метод возвращает истину в случае успешной синхронизации или ноль в случае неуспеха.
- Группа методов синхронизации последних загруженных данных (баров):
bool SyncLastBars(const int count, const ENUM_SYNC_BARS_TYPE sync_bars_type) bool CustomSyncLastBars(const int count, const ENUM_SYNC_BARS_TYPE sync_bars_type)
Позволяет синхронизировать заданное число последних по времени добавленных в контейнеры данных. Должны быть обязательно загружены данные в символ синхронизации и хотя бы еще один символ портфеля - иначе синхронизация бессмысленна. Основной метод CTimeSyncManager::SyncLastBars, который транзитом вызывает метод CTimeSyncManager::CustomSyncLastBars. Это сделано для дальнейшего расширения возможностей класса.
Параметры метода CTimeSyncManager::SyncLastBars следующие:
- [in] int count — задает число последних баров, которое необходимо синхронизировать. Отсчет ведется от конца массивов.
- [in] ENUM_SYNC_BARS_TYPE sync_bars_type — задает тип синхронизации баров. Заполнение пустым баром либо предыдущим значением.
Алгоритм синхронизации последних баров абсолютно идентичен описанному выше. Также проводится в три этапа и метод возвращает истину в случае успешной синхронизации. При этом, обрабатываются только заданное количество последних по времени баров.
Методы возвращают истину в случае успешной синхронизации.
Практическое использование
При практическом использовании описанных классов возможны два варианта разработки.
Первый вариант — использование в своей разработке объекта типа CTimeSyncManager для получения массивов синхронных данных. Это может быть как свой класс с включенным в него полем типа CTimeSyncManager*, так и глобальная переменная такого же типа в основном коде индикатора/эксперта. Мы разработаем индикатор для отображения синхронных графиков инструментов (см. рис. 2,3).
- Пошаговый рецепт для первого варианта:
Шаг 1. Добавляем директиву включения файла с описанием классов синхронизации в основной код индикатора:
#include "TimeSyncManager.mqh"
Шаг 2. Добавляем глобальную переменную в основной код индикатора:
CTimeSyncManager* time_sync_manager_Ptr;
Шаг 3. В функцию OnInit добавляем код для создания объекта:
time_sync_manager_Ptr = new CTimeSyncManager(28, MaxBars); Шаг 4. В функцию OnDeinit добавляем код для удаления объекта:
if(CheckPointer(time_sync_manager_Ptr) == POINTER_DYNAMIC) { delete(time_sync_manager_Ptr); }
Шаг 5. В функцию OnCalculate, в зависимости от назначения индикатора, добавляем код для загрузки/синхронизации/получения данных. Для индикатора "TestSync-MultyChart.mq5" на рисунках 2, 3. Упрощенный код выглядит следующим образом:
int OnCalculate(...) { if(rates_total <= 0) { copied = 0; headge_copied = 0; return(0); } // Первый расчет if(prev_calculated == 0) { // Получим бары для символа синхронизации (символ графика): copied = CopyRates(Symbol(), Period(), 0, MaxBars, curr_rates); if(copied > 0) { // Загружаем начальные данные в символ синхронизации: required = MathMin(copied, MaxBars); added = time_sync_manager_Ptr.SyncSymbolAddFullData(Symbol(), Period(), curr_rates, 0, required, false); // Если задан хеджевый символ: if(HeadgeSymbol != "") { headge_copied = CopyRates(HeadgeSymbol, Period(), 0, MaxBars/*iBars(HeadgeSymbol, PERIOD_CURRENT)*/, curr_rates);; if(headge_copied > 0) { // Загружаем начальные данные в символ синхронизации: required = MathMin(headge_copied, MaxBars); headge_added = time_sync_manager_Ptr.SymbolAddFullData(HeadgeSymbol, Period(), curr_rates, 0, required, false); // Синхронизируем массивы баров: time_sync_manager_Ptr.SyncAllData(SyncBarsType); // Получим заданное число баров (синхронизированных) - время в массиве 'Time[]' идет слева направо: time_sync_manager_Ptr.GetFullOpen(HeadgeSymbol, OpenBuffer, ArraySize(OpenBuffer) - headge_added); time_sync_manager_Ptr.GetFullHigh(HeadgeSymbol, HighBuffer, ArraySize(HighBuffer) - headge_added); time_sync_manager_Ptr.GetFullLow(HeadgeSymbol, LowBuffer, ArraySize(LowBuffer) - headge_added); time_sync_manager_Ptr.GetFullClose(HeadgeSymbol, CloseBuffer, ArraySize(CloseBuffer) - headge_added); ... } } else { ... } } } // Пришел новый тик по символу графика - обработаем поледние бар(-ы): else { // Получим последние бары для символа синхронизации (символ графика): if(GetLastBar(Symbol(), Period(), 0, rates_total - prev_calculated + 1, last_rates) == true) { // Обработка текущих баров: copied = time_sync_manager_Ptr.SyncSymbolUpdateLastData(last_rates, 0, rates_total - prev_calculated + 1); if(copied > 0) { // Если задан хеджевый символ: if(HeadgeSymbol != "") { // Получим последние бары для хеджевого символа: if(GetLastBar(HeadgeSymbol, Period(), 0, rates_total - prev_calculated + 1, last_rates) == true) { time_sync_manager_Ptr.SymbolUpdateLastData(HeadgeSymbol, last_rates, 0, (int)last_rates.Size()); // Синхронизируем последние бары: time_sync_manager_Ptr.SyncLastBars(rates_total - prev_calculated + 1, SyncBarsType); time_sync_manager_Ptr.GetLastOpen(HeadgeSymbol, (int)last_rates.Size(), OpenBuffer, ArraySize(OpenBuffer) - (int)last_rates.Size()); time_sync_manager_Ptr.GetLastHigh(HeadgeSymbol, (int)last_rates.Size(), HighBuffer, ArraySize(HighBuffer) - (int)last_rates.Size()); time_sync_manager_Ptr.GetLastLow(HeadgeSymbol, (int)last_rates.Size(), LowBuffer, ArraySize(LowBuffer) - (int)last_rates.Size()); time_sync_manager_Ptr.GetLastClose(HeadgeSymbol, (int)last_rates.Size(), CloseBuffer, ArraySize(CloseBuffer) - (int)last_rates.Size()); ... } } } } } return(rates_total); }
MaxBars — число отображаемых баров инжикатора.
OpenBuffer/HighBuffer/LowBuffer/CloseBuffer — это иассивы индикаторных буферов для отображения OHLC-баров. Полный код индикатора с комментариями находится в файле "TestSync-MultyChart.mq5", приложенном к данной статье.
Второй вариант — использовать в своей разработке класс-обработчик данных, унаследованный от CTimeSyncManager для расчетов и получения массивов синхронных данных. Мы разработаем индикатор для отображения стоимости порфеля инструментов (см. рис.4).
- Пошаговый рецепт для второго варианта:
Шаг 1. Разрабатываем класс, унаследовав его от класса синхронизации:
class MultiCurrencyManager : public CTimeSyncManager
Шаг 2. Добавляем директиву включения файла с описанием классов, унаследованных от класса синхронизации в основной код индикатора:
#include "MultyCurrencyManager.mqh"
Шаг 3. Добавляем глобальную переменную в основной код индикатора:
MultiCurrencyManager* time_sync_manager_Ptr;
Шаг 4. В функцию OnInit добавляем код для создания объекта:
time_sync_manager_Ptr = new MultiCurrencyManager(28, MaxBars);
Шаг 5. В функцию OnDeinit добавляем код для удаления объекта:
if(CheckPointer(time_sync_manager_Ptr) == POINTER_DYNAMIC) { delete(time_sync_manager_Ptr); }
Шаг 6. В функцию OnCalculate, в зависимости от назначения индикатора, добавляем код для загрузки/синхронизации/получения данных. Для индикатора "SyncTest-MultyChart.mq5" на рис.2, 3. Упрощенный код выглядит следующим образом:
int OnCalculate(const int rates_total, const int prev_calculated, ...) { if(rates_total <= 0) { ArrayFill(headge_copied_Array, 0, (int)headge_copied_Array.Size(), 0); return(0); } // Первый расчет if(prev_calculated == 0) { restarts_count++; restart_time = TimeCurrent(); // Получим бары для символа синхронизации (символ графика): headge_copied_Array[0] = CopyRates(symbol_name_Array[0], Period(), 0, MaxBars, curr_rates); if(headge_copied_Array[0] > 0) { // Загружаем начальные данные в символ синхронизации: required = MathMin(headge_copied_Array[0], MaxBars); added = time_sync_manager_Ptr.SyncSymbolAddFullData(symbol_name_Array[0], Period(), curr_rates, 0, required, false); // Если задан хеджевый символ: if(symbol_name_Array.Size() > 1) { // Загрузка данных: for(int i = 1; i < (int)symbol_name_Array.Size(); i++) { headge_copied_Array[i] = CopyRates(symbol_name_Array[i], Period(), 0, MaxBars, curr_rates);; if(headge_copied_Array[i] > 0) { // Загружаем начальные данные в символ синхронизации: required = MathMin(headge_copied_Array[i], MaxBars); headge_added = time_sync_manager_Ptr.SymbolAddFullData(symbol_name_Array[i], Period(), curr_rates, 0, required, false); } } // Проверка загруженности данных: if(CheckLoading(headge_copied_Array) == true) { // Синхронизируем массивы баров: time_sync_manager_Ptr.SyncAllData(ENUM_SYNC_BARS_INTERPOLATED); // Рассчитаем портфель: time_sync_manager_Ptr.CalculateBasketFull(); // Получим заданное число баров (синхронизированных) - время в массиве 'Time[]' идет слева направо: time_sync_manager_Ptr.GetFullBasketOpen(OpenBuffer, ArraySize(OpenBuffer) - headge_added); time_sync_manager_Ptr.GetFullBasketHigh(HighBuffer, ArraySize(HighBuffer) - headge_added); time_sync_manager_Ptr.GetFullBasketLow(LowBuffer, ArraySize(LowBuffer) - headge_added); time_sync_manager_Ptr.GetFullBasketClose(CloseBuffer, ArraySize(CloseBuffer) - headge_added); ... } } } } } ... return(rates_total); }
MaxBars — число отображаемых баров индикатора.
OpenBuffer/HighBuffer/LowBuffer/CloseBuffer — это иассивы индикаторных буферов для отображения OHLC-баров. Полный код индикатора с комментариями находится в файле "SyncTest-MultyCurrency.mq5", приложенном к данной статье.
Практическое использование: пример индикатора синхронных мульти-графиков
Это пример первого варианта использования класса CTimeSyncManager.
Данный индикатор позволяет вывести в отдельном индикаторном окне OHLC — график заданного в параметрах инструмента синхронно с основным графиком. Полный текст индикатора приведен в файле "TestSync-MultyChart.mq5", приложенном к данной статье.
Результат работы индикатора приведен на рис.2. Здесь наряду с основным графиком символа AUDUSD отображаются графики XAGUSD и XAUUSD. Это характерный пример для проверки синхронизации, поскольку у инструментов XAGUSD и XAUUSD отсутствуют данные с нуля часов до часа ночи.
Видно, что в этом интервале были добавлены двенадцать баров (один час состоит из двенадцати пяти-минутных баров) с ценой закрытия предыдущего реального бара (задан режим ENUM_SYNC_BARS_INTERPOLATED). У этих баров все четыре OHLC-цены равны цене Close, поэтому они выглядят как свечи-доджи.

Рис. 2. Синхронные графики нескольких инструментов FOREX
Еще один пример работы индикатора TestSync-MultyChart приведен на рисунке 3. Здесь среднеликвидный фьючерс OJH8, торгуемый на московской бирже МОЕХ синхронизирован с относительно ликвидной акцией AFKS. Для сравнения в нижней части приведен график инструмента OJH8 — как он выглядит в отдельном окне.
Левая стрелка синего цвета указывает на один и тот же бар на обоих графиках. В данном случае в индикаторе задана вставка пустых баров вместо интерполяции (задан режим ENUM_SYNC_BARS_EMPTY), и правая стрелка синего цвета указывает на участок графика, где есть пропущенные бары на инструменте OJH8.

Рис. 3. Синхронное отображение биржевых графиков MOEX с "дырками"
Практическое использование: пример индикатора стоимости корзины
Это пример второго варианта использования класса CTimeSyncManager.
Данный индикатор позволяет вывести в отдельном индикаторном окне OHLC-график стоимости заданного в параметрах порфеля инструментов синхронно с основным графиком. Полный текст индикатора приведен в файле "SyncTest-MultyCurrency.mq5", приложенном к данной статье.
Результат работы индикатора приведен на рисунке 4. Здесь наряду с основным графиком символа EURGBP отображается график стоимости портфеля инструментов: BUY 0.01 EURGBP + SELL 0.01 EURUSD + BUY 0.01 GBPUSD. Структура портфеля задается в параметрах индикатора в виде строки: "EURGBP,1;EURUSD,-1;GBPUSD,1".

Рис. 4. Синхронное отображение портфеля инструментов FOREX
Заключение
Мы создали на основе ассоциативных массивов из штатной библиотеки терминала Meta Trader 5 достаточно эффективный, гибкий и легко внедряемый в готовые программы механизм получения синхронных массивов данных временных рядов. Ключевые преимущества подхода:
- Избавление от ошибок несинхронности данных — получение адекватных результатов работы портфельных алгоритмов.
- Простота встраивания в готовые разработки — использован ООП-код.
- Достаточно быстрый доступ к данным — в основе лежат ассоциативные массивы.
В таблице представлены файлы, приложенные к статье:
| Название файла | Описание |
|---|---|
| TimeSyncManager.mqh | Файл, содержащий код классов для синхронизации массивов данных |
| SyncTest-MultyChart.mq5 | Файл, содержащий код проверочного индикатора, отображающего синхронный мульти-валютный график |
| SyncTest-MultyCurrency.mq5 | Файл, содержащий код проверочного индикатора, отображающего синхронный график стоимости портфеля инструментов |
| MultyCurrencyManager.mqh | Файл, содержащий код классов для расчета стоимости портфеля инструментов |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Подтверждение зон спроса и предложения через статистические данные
От новичка до эксперта: Сигналы с высокой вероятностью
Нейросети в трейдинге: Масштабируемые трансформеры со структурной декомпозицией признаков (FAT)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Для интереса - вот результат вайб-кодинга на тему объединения таймсерий по символам.
Вроде работает правильно, после нескольких итераций исправлений логических ошибок.
Предоставляется "как есть", в ответах ИИ могут быть ошибки! ;-)
Сразу же видно, что для min_time нет запроса баров.
Не понял, о каком запросе речь.
Ещё не дочитал до конца, но уже вопрос по поводу использования ассоциативных массивов. Поскольку все ряды изначально отсортированы, то не проще ли (эффективнее) делать слияние за один проход с продвижением интераторов внутри каждого из массивов? Для доступа на чтение - индекс - история не меняется - должно работать быстрее ассоциативного.