Обсуждение статьи "Как получить синхронизированные массивы для использования в алгоритмах портфельной торговли"

 

Опубликована статья Как получить синхронизированные массивы для использования в алгоритмах портфельной торговли:

Описан практический подход к синхронизации баров между инструментами портфеля в MQL5. Предложены классы для загрузки, хранения и выравнивания OHLCV с опциями: пустой бар или перенос значений предыдущего бара, выбор символа синхронизации и обработка асинхронных новых баров. Показаны примеры использования в индикаторах мультиграфиков и корзины. Читатель получает готовый API для стабильных портфельных расчетов.

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

  • nорговля спредом: спот — фьючерс на этот спот;
  • анализ корреляций инструментов;
  • различные хэджирующие стратегии торговли;
  • анализ взаимного движения инструментов (например, так называемая "сила" валют).

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

Автор: Dmitriy Skub

 
Ещё не дочитал до конца, но уже вопрос по поводу использования ассоциативных массивов. Поскольку все ряды изначально отсортированы, то не проще ли (эффективнее) делать слияние за один проход с продвижением интераторов внутри каждого из массивов? Для доступа на чтение - индекс - история не меняется - должно работать быстрее ассоциативного.
 

Обращение по начальной и конечной датам требуемого интервала времени

int  CopyRates(
   string           symbol_name,       // имя символа
   ENUM_TIMEFRAMES  timeframe,         // период
   datetime         start_time,        // с какой даты
   datetime         stop_time,         // по какую дату
   MqlRates         rates_array[]      // массив, куда будут скопированы данные
   );

MQ не стали делать вариант этой перегрузки, когда всегда в сутках 1440 M1-баров.

При таком функционале, вроде, ничего больше придумывать не нужно.

 

Для интереса - вот результат вайб-кодинга на тему объединения таймсерий по символам.

matrix CopyMultipleSymbolRates(string &symbols[], 
                               ENUM_TIMEFRAMES period, 
                               datetime start_time, 
                               datetime stop_time,
                               ulong flags = COPY_RATES_OHLC)
{
   int sym_count = ArraySize(symbols);
   
   // Принудительно добавляем флаг вертикальной ориентации (строки = время)
   ulong vertical_flags = flags | COPY_RATES_VERTICAL;
   
   struct SymbolData 
   {
      matrix m;
      datetime times[];
      int cursor;
      int rows;
      int cols;
   };
   
   SymbolData data[];
   ArrayResize(data, sym_count);
   
   int total_cols = 0;
   int max_possible_rows = 0;

   // 1. Предварительная загрузка данных
   for(int i = 0; i < sym_count; i++) 
   {
      // Используем вертикальный флаг: rows = время, cols = цены
      if(data[i].m.CopyRates(symbols[i], period, vertical_flags, start_time, stop_time)) 
      {
         CopyTime(symbols[i], period, start_time, stop_time, data[i].times);
         data[i].rows = (int)data[i].m.Rows();
         data[i].cols = (int)data[i].m.Cols();
         data[i].cursor = 0;
         
         total_cols += data[i].cols;
         max_possible_rows += data[i].rows;
      }
      else
      {
         // Если данных нет, инициализируем пустую структуру для безопасности
         data[i].rows = 0;
         data[i].cols = 0;
         data[i].cursor = 0;
      }
   }

   matrix res_matrix;
   if(total_cols == 0) 
   {
      return res_matrix;
   }

   res_matrix.Init(max_possible_rows, total_cols);
   
   int current_row = 0;
   double nan_value = (double)"NaN";

   // 2. Слияние по временной сетке
   while(true) 
   {
      datetime min_time = D'2099.12.31';
      bool any_left = false;
      
      // Ищем минимальное время среди текущих курсоров всех символов
      for(int i = 0; i < sym_count; i++) 
      {
         if(data[i].cursor < data[i].rows) 
         {
            if(data[i].times[data[i].cursor] < min_time) 
               min_time = data[i].times[data[i].cursor];
            any_left = true;
         }
      }
      
      if(!any_left) 
      {
         break;
      }

      int col_offset = 0;
      for(int i = 0; i < sym_count; i++) 
      {
         int target_row_idx = -1;
         
         // Если у символа есть данные на это время
         if(data[i].cursor < data[i].rows && data[i].times[data[i].cursor] == min_time) 
         {
            target_row_idx = data[i].cursor;
            data[i].cursor++;
         } 
         // Если данных нет, берем предыдущий известный бар (Forward Fill)
         else if(data[i].cursor > 0) 
         {
            target_row_idx = data[i].cursor - 1; 
         }

         // Если мы нашли индекс (текущий или предыдущий), копируем всю строку цен
         if(target_row_idx != -1)
         {
            for(int c = 0; c < data[i].cols; c++) 
            {
               res_matrix[current_row][col_offset + c] = data[i].m[target_row_idx][c];
            }
         }
         else 
         {
            // Если данных по символу еще не было в начале истории
            for(int c = 0; c < data[i].cols; c++) 
            {
               res_matrix[current_row][col_offset + c] = nan_value;
            }
         }
         
         col_offset += data[i].cols;
      }
      current_row++;
   }

   // Приводим матрицу к фактическому количеству строк
   res_matrix.Resize(current_row, total_cols);
   
   return res_matrix;
}

#define DAYLONG (60 * 60 * 24)

void OnStart()
{
   string symbols[] = {"EURUSD.c", "UK100", "XAUUSD.c"};
   matrix x = CopyMultipleSymbolRates(symbols, _Period, TimeCurrent() / DAYLONG * DAYLONG - 1 * DAYLONG, TimeCurrent(), COPY_RATES_OHLC | COPY_RATES_VOLUME_TICK);
   Print(x);
}

Вроде работает правильно, после нескольких итераций исправлений логических ошибок.

Предоставляется "как есть", в ответах ИИ могут быть ошибки! ;-)

Файлы:
 
Stanislav Korotky #:

Для интереса - вот результат вайб-кодинга на тему объединения таймсерий по символам.

Вроде работает правильно, после нескольких итераций исправлений логических ошибок.

Предоставляется "как есть", в ответах ИИ могут быть ошибки! ;-)

Сразу же видно, что для min_time нет запроса баров.
 
fxsaber #:
Сразу же видно, что для min_time нет запроса баров.
Не понял, о каком запросе речь. min_time используется только для перемещения курсора по результирующему массиву. В простых тестах у меня возвращало ожидаемую комбинированную матрицу, но вглубь я не проверял - это ж вайб-кодинг ;-).
 
Stanislav Korotky #:
Не понял, о каком запросе речь.
Делаете запрос с 18.02.2026 00:00 M1-бары. EURUSD имеет бар на начало интервала, GBPUSD - нет. Чем заполнять GBPUSD-бар начала интервала?
 
Stanislav Korotky #:
Ещё не дочитал до конца, но уже вопрос по поводу использования ассоциативных массивов. Поскольку все ряды изначально отсортированы, то не проще ли (эффективнее) делать слияние за один проход с продвижением интераторов внутри каждого из массивов? Для доступа на чтение - индекс - история не меняется - должно работать быстрее ассоциативного.
Там на самом деле синхронизация за один проход делается, описано так для лучшего понимания. С индексами я, в свое время, уже наэкпериментировался)
 
Дело в том, что в CSortedMap нет ни Insert, ни IndexOf, ни At. Зато сортирует хорошо)
 
fxsaber #:
Делаете запрос с 18.02.2026 00:00 M1-бары. EURUSD имеет бар на начало интервала, GBPUSD - нет. Чем заполнять GBPUSD-бар начала интервала?

Как видно по реализации - там будут NaN-ы - так сказать by design, намеренно, я это видел и по тестовым логам. Можно попросить ИИ в таких случаях - либо искать предыдущий отсчет, либо пропускать такое неполное начало.

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

      if(data[i].rows > 0 && data[i].times[0] > global_timeline[0])
      {
         // Запрашиваем 1 бар непосредственно ПЕРЕД start_time
         // Дозагрузка: берем 1 бар непосредственно перед start_time
         // Вместо NaN в начале цикла заполнения (п. 3) 
         matrix m_prev;
         if(m_prev.CopyRates(symbols[i], period, vertical_flags, start_time, 1))
         {
            data[i].initial_row = m_prev.Row(0); 
         }
      }

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

PPS. И поле SymbolData.cursor - осталось от прежнего варианта лишним, нужно удалить.

PPPS. Кстати, он назвал локальную переменную m_prev, а префикс очевидно используется некоторыми стилями только для членов класса ;-), так что - тоже мелкий оформительский баг. Глаз да глаз за ним.

Файлы:
 
Stanislav Korotky #:

Вот вариант, который ИИ оптимизировал по скорости и туда добавил дозапрос более раннего бара при необходимости.

Во сколько быстрее получается писать нужный код через ИИ?