Пример поиска торговой стратегии средствами SQLite

Попробуем применить SQLite для решения практических задач. Импортируем в базу структуры MqlRates с историей котировок и проведем их анализ с целью выявления закономерностей и поиска потенциальных торговых стратегий. Разумеется, любая выбранная логика может быть реализована и на MQL5, однако SQL позволяет делать это иным способом, во многих случаях более эффективно и с привлечением множества интересных встроенных функций SQL. Тематика книги, нацеленной на изучение MQL5, не позволяет углубляться в эту технологию, но мы упоминаем её, как заслуживающую внимания алготрейдера.

Скрипт для перевода истории котировок в формат базы данных называется DBquotesImport.mq5. Во входных параметрах можно задать префикс названия базы данных и размер транзакции (количество записей в одной транзакции).

input string Database = "MQL5Book/DB/Quotes";
input int TransactionSize = 1000;

Для добавления структур MqlRates в базу с использованием нашей ORM-прослойки в скрипте определена вспомогательная структура MqlRatesDB, в которую "зашиты" правила привязки полей структуры в столбцы базы. Поскольку наш скрипт только записывает данные в базу и не читает их оттуда, ему не нужна привязка с помощью функции DatabaseReadBind, что накладывало бы ограничение на "простоту" структуры. Отсутствие ограничения позволяет сделать структуру MqlRatesDB производной от MqlRates (и не повторять описание полей).

struct MqlRatesDBpublic MqlRates
{
   /* для справки:
   
      datetime time;
      double   open;
      double   high;
      double   low;
      double   close;
      long     tick_volume;
      int      spread;
      long     real_volume;
   */
   
   bool bindAll(DBQuery &qconst
   {
      return q.bind(0time)
         && q.bind(1open)
         && q.bind(2high)
         && q.bind(3low)
         && q.bind(4close)
         && q.bind(5tick_volume)
         && q.bind(6spread)
         && q.bind(7real_volume);
   }
   
   long rowid(const long setter = 0)
   {
      // rowid устанавливается нами по времени бара
      return time;
   }
};
   
DB_FIELD_C1(MqlRatesDBdatetimetimeDB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(MqlRatesDBdoubleopen);
DB_FIELD(MqlRatesDBdoublehigh);
DB_FIELD(MqlRatesDBdoublelow);
DB_FIELD(MqlRatesDBdoubleclose);
DB_FIELD(MqlRatesDBlongtick_volume);
DB_FIELD(MqlRatesDBintspread);
DB_FIELD(MqlRatesDBlongreal_volume);

Название базы формируется из префикса Database, имени и таймфрейма текущего графика, на котором запущен скрипт. В базе создается единственная таблица "MqlRatesDB" с конфигурацией полей, заданной макросами DB_FIELD. Обратите внимание, что первичный ключ не будет генерироваться базой, а берется непосредственно из баров, из поля time (время открытия бара).

void OnStart()
{
   Print("");
   DBSQLite db(Database + _Symbol + PeriodToString());
   if(!PRTF(db.isOpen())) return;
   
   PRTF(db.deleteTable(typename(MqlRatesDB)));
   
   if(!PRTF(db.createTable<MqlRatesDB>(true))) return;
   ...

Далее пакетами по TransactionSize баров запрашиваем бары из истории и добавляем в таблицу. Этим занимается вспомогательная функция ReadChunk, вызываемая в цикле до тех пор, пока есть данные (функция при этом возвращает true) или пользователь не остановит скрипт вручную. Код функции показан чуть ниже.

   int offset = 0;
   while(ReadChunk(dboffsetTransactionSize) && !IsStopped())
   {
      offset += TransactionSize;
   }

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

   DBRow *rows[];
   if(db.prepare(StringFormat("SELECT COUNT(*) FROM %s",
      typename(MqlRatesDB))).readAll(rows))
   {
      Print("Records added: "rows[0][0].integer_value);
   }
}

Обещанная функция ReadChunk выглядит следующим образом.

bool ReadChunk(DBSQLite &dbconst int offsetconst int size)
{
   MqlRates rates[];
   MqlRatesDB ratesDB[];
   const int n = CopyRates(_SymbolPERIOD_CURRENToffsetsizerates);
   if(n > 0)
   {
      DBTransaction tr(dbtrue);
      Print(rates[0].time);
      ArrayResize(ratesDBn);
      for(int i = 0i < n; ++i)
      {
         ratesDB[i] = rates[i];
      }
      
      return db.insert(ratesDB);
   }
   else
   {
      Print("CopyRates failed: "_LastError" "E2S(_LastError));
   }
   return false;
}

В ней вызывается встроенная функция CopyRates и, тем самым, заполняется массив баров rates. Затем бары переносятся в массив ratesDB, чтобы одним оператором db.insert(ratesDB) записать информацию в базу (именно в MqlRatesDB нами формализовано, как это правильно делать).

Наличие объекта DBTransaction (с включенной опцией автоматического "коммита") внутри блока означает, что все операции с массивом "обложены" транзакцией. Для индикации прогресса, во время обработки каждого блока баров в журнал выводится метка первого бара.

Пока функция CopyRates возвращает данные и их вставка в базу происходит успешно, цикл в OnStart продолжается со сдвигом номеров копируемых баров вглубь истории. Когда достигается конец доступной истории или лимит баров, заданный в настройках терминала, CopyRates вернет ошибку 4401 (HISTORY_NOT_FOUND), и работа скрипта завершится.

Запустим скрипт на графике EURUSD,H1. В журнале должны появиться примерно такие сообщения.

   db.isOpen()=true / ok
   db.deleteTable(typename(MqlRatesDB))=true / ok
   db.createTable<MqlRatesDB>(true)=true / ok
   2022.06.29 20:00:00
   2022.05.03 04:00:00
   2022.03.04 10:00:00
   ...
   CopyRates failed: 4401 HISTORY_NOT_FOUND
   Records added: 100000

Теперь у нас имеется база QuotesEURUSDH1.sqlite, над которой можно ставить эксперименты для проверки различных торговых гипотез. Вы можете открыть её в редакторе MetaEditor, чтобы убедиться в правильности переноса данных.

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

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

Поскольку время баров хранится как количество секунд (по стандартам datetime MQL5 и, по совместительству, "эпоха Unix" SQL), их отображение для удобного чтения желательно преобразовать в строку, поэтому начнем запрос SELECT с поля datetime на основе функции DATETIME:

SELECT
   DATETIME(time, 'unixepoch') as datetime, open, ...

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

Поскольку мы сбираемся, при необходимости, выбирать некий период из всего файла, в условии потребуется поле time в "чистом виде", и его тоже следует добавить в запрос. Кроме того, согласно планируемому анализу котировок нам потребуется вычленить из метки бара его внутрисуточное время, а также день недели (их нумерация соответствует принятой в MQL5, 0 — воскресенье). Два последних столбца запроса назовем intraday и day, соответственно, а для их получения задействованы функции TIME и STRFTIME.

SELECT
   DATETIME(time, 'unixepoch') as datetime, open,
   time,
   TIME(time, 'unixepoch') AS intraday,
   STRFTIME('%w', time, 'unixepoch') AS day, ...

Для вычисления приращения цены в SQL можно применить функцию LAG — она возвращает значение указанной колонки со смещением на заданное количество строк. Например, LAG(X, 1) означает получение величины X в предыдущей записи, причем второй параметр 1, означающий смещение, по умолчанию равен 1 и потому его можно опустить, получив эквивалентную запись LAG(X). Для получения величины следующей записи следует вызвать LAG(X,-1). В любом случае, при использовании LAG требуется дополнительная синтаксическая конструкция, задающая порядок сортировки записей, в простейшем случае, вида OVER (ORDER BY столбец).

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

   ...
   (LAG(open,-1) OVER (ORDER BY time) - open) AS delta, ...

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

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

   ...

   (LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

      AS product, ...

Данный показатель выбран как наиболее простой в расчете: для реальных торговых систем можно выбрать более сложный критерий.

Чтобы оценить прибыль, генерируемую системой на бэктесте, нужно направление предыдущего бара (выступающего индикатором будущего движения) умножить на приращение цены на следующем баре. Направление рассчитывается в колонке direction (с помощью функции SIGN), только для справки. Оценка прибыли — в колонке estimate — это произведение прежнего направления движения (direction) на приращение следующего бара (delta): если направление сохранится, получим положительный результат (в пунктах).

   ...

   SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

   (LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

      AS estimate ...

В выражениях в SQL-команде нельзя использовать AS-алиасы, определенные в той же команде. Именно поэтому мы не можем определить estimate как delta * direction, и приходится повторять вычисление произведения в явном виде. Однако напомним, что колонки delta и direction для программного анализа не нужны и добавлены здесь только для визуализации таблицы перед пользователем.

В завершение SQL-команды укажем таблицу, из которой делается выборка, и условия фильтрации по диапазону дат бэктеста: два параметра "от" и "до".

...
FROM MqlRatesDB
WHERE (time >= ?1 AND time < ?2)

Опционально мы можем добавить ограничение LIMIT ?3 (и вводить какое-нибудь малое значение, например 10), чтобы визуальная проверка результатов запроса на первых порах не вынуждала просматривать десятки тысяч записей.

Проверить работу SQL-команды можно с помощью функции DatabasePrint, однако функция, к сожалению, не позволяет работать с подготовленными запросами с параметрами. Поэтому нам придется заменить подготовку параметров SQL '?n' на форматирование строки запроса с помощью StringFormat и подставлять значения параметров там. Альтернативно можно было бы полностью отказаться от DatabasePrint и выводить результаты в журнал самостоятельно, построчно (через массив DBRow).

Таким образом, завершающий фрагмент запроса превратится в:

   ...
   WHERE (time >= %ld AND time < %ld)
   ORDER BY time LIMIT %d;

Следует отметить, что в данном запросе значения datetime будут поступать из MQL5 в "машинном" формате количества секунд с начала 1970 года. Если же мы захотим отлаживать этот же SQL-запрос в редакторе MetaEditor, то условие на диапазон дат удобнее записывать с применением литералов (строк) дат, следующим образом:

   WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

Здесь опять возникает необходимость использования функции STRFTIME (модификатор '%s' в SQL задает перевод указанной строки с датой в метку "эпохи Unix"; тот факт, что '%s' напоминает форматную строку MQL5 — просто совпадение).

Спроектированный SQL-запрос сохраним в отдельном текстовом файле DBQuotesIntradayLag.sql и подключим его как ресурс в одноименный тестовый скрипт DBQuotesIntradayLag.mq5.

#resource "DBQuotesIntradayLag.sql" as string sql1

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

input string Database = "MQL5Book/DB/Quotes";
input datetime SubsetStart = D'2022.01.01';
input datetime SubsetStop = D'2023.01.01';
input int Limit = 10;

Таблица с котировками известна заранее, из предыдущего скрипта.

const string Table = "MqlRatesDB";

В функции OnStart откроем базу и убедимся в наличии таблицы котировок.

void OnStart()
{
   Print("");
   DBSQLite db(Database + _Symbol + PeriodToString());
   if(!PRTF(db.isOpen())) return;
   if(!PRTF(db.hasTable(Table))) return;
   ...

Далее подставляем параметры в строку SQL-запроса. Уделяем внимание не только подмене SQL-параметров '?n' на форматные последовательности, но и удваиваем сперва символы процента '%', потому что иначе функция StringFormat воспримет их, как свои команды, и не пропустит в SQL.

   string sqlrep = sql1;
   StringReplace(sqlrep"%""%%");
   StringReplace(sqlrep"?1""%ld");
   StringReplace(sqlrep"?2""%ld");
   StringReplace(sqlrep"?3""%d");
   
   const string sqlfmt = StringFormat(sqlrepSubsetStartSubsetStopLimit);
   Print(sqlfmt);

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

Наконец выполним SQL-запрос и выведем таблицу с результатами в журнал.

   DatabasePrint(db.getHandle(), sqlfmt0);
}

Вот что мы увидим для 10 баров EURUSD,H1 в начале 2022 года.

db.isOpen()=true / ok

db.hasTable(Table)=true / ok

      SELECT

         DATETIME(time, 'unixepoch') as datetime,

         open,

         time,

         TIME(time, 'unixepoch') AS intraday,

         STRFTIME('%w', time, 'unixepoch') AS day,

         (LAG(open,-1) OVER (ORDER BY time) - open) AS delta,

         SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

         (LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

            AS product,

         (LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

            AS estimate

      FROM MqlRatesDB

      WHERE (time >= 1640995200 AND time < 1672531200)

      ORDER BY time LIMIT 10;

 #| datetime               open       time intraday day       delta dir       product      estimate

--+------------------------------------------------------------------------------------------------

 1| 2022-01-03 00:00:00 1.13693 1641168000 00:00:00 1  0.0003200098                                

 2| 2022-01-03 01:00:00 1.13725 1641171600 01:00:00 1  2.999999e-05  1  9.5999478e-09  2.999999e-05 

 3| 2022-01-03 02:00:00 1.13728 1641175200 02:00:00 1  -0.001060006  1 -3.1799748e-08  -0.001060006 

 4| 2022-01-03 03:00:00 1.13622 1641178800 03:00:00 1 -0.0003400007 -1  3.6040028e-07  0.0003400007 

 5| 2022-01-03 04:00:00 1.13588 1641182400 04:00:00 1  -0.001579991 -1  5.3719982e-07   0.001579991 

 6| 2022-01-03 05:00:00  1.1343 1641186000 05:00:00 1  0.0005299919 -1 -8.3739827e-07 -0.0005299919 

 7| 2022-01-03 06:00:00 1.13483 1641189600 06:00:00 1 -0.0007699937  1 -4.0809905e-07 -0.0007699937 

 8| 2022-01-03 07:00:00 1.13406 1641193200 07:00:00 1 -0.0002600149 -1  2.0020098e-07  0.0002600149 

 9| 2022-01-03 08:00:00  1.1338 1641196800 08:00:00 1   0.000510001 -1 -1.3260079e-07  -0.000510001 

10| 2022-01-03 09:00:00 1.13431 1641200400 09:00:00 1  0.0004800036  1  2.4480023e-07  0.0004800036 

...

Легко убедиться, что внутридневное время бара выделено правильно, как и день недели — 1, что соответствует понедельнику. Также можно проверить и приращение delta. Значения product и estimate пусты в первой строке, потому что для их расчета требуется отсутствующая предыдущая строка.

Усложним наш SQL-запрос, сгруппировав записи с одинаковыми сочетаниями времени суток (intraday) и дня недели (day), и вычислив для каждого из этих сочетаний некий целевой показатель, характеризующий успешность торговли. Возьмем в качестве такого показателя средний размер ячейки product, деленный на стандартное отклонение этих же произведений. Чем больше среднее произведение приращений цен соседних баров, тем больше ожидаемая прибыль, а чем меньше разброс этих произведений, тем стабильнее прогноз. Название показателя в SQL-запросе — objective.

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

   SELECT

      AVG(product) / STDDEV(product) AS objective,

      SUM(estimate) AS backtest_profit,

      SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

         SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

      intraday, day

   FROM

   (

      SELECT

         time,

         TIME(time, 'unixepoch') AS intraday,

         STRFTIME('%w', time, 'unixepoch') AS day,

         (LAG(open,-1) OVER (ORDER BY time) - open) AS delta,

         SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

         (LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

            AS product,

         (LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

            AS estimate

      FROM MqlRatesDB

      WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

   )

   GROUP BY intraday, day

   ORDER BY objective DESC

Первый SQL-запрос стал вложенным, из которого мы теперь аккумулируем данные внешним SQL-запросом. Группировку по всем сочетаниям времени и дня недели обеспечивает "довесок" GROUP BY intraday, day. Кроме того, мы добавили сортировку по целевому показателю (ORDER BY objective DESC), чтобы лучшие варианты оказались наверху таблицы.

Во вложенном запросе мы убрали параметр LIMIT, потому что количество групп стало приемлемым, значительно меньше количества анализируемых баров. Так, для H1 получим 120 вариантов (24*5).

Расширенный запрос помещен в текстовый файл DBQuotesIntradayLagGroup.sql, который в свою очередь подключен в виде ресурса в одноименный тестовый скрипт DBQuotesIntradayLagGroup.mq5. Его исходный код мало отличается от предыдущего, поэтому сразу покажем результат его запуска для диапазона дат по умолчанию: с начала 2015 год по начало 2021 (исключая 2021-й и 2022-й).

db.isOpen()=true / ok

db.hasTable(Table)=true / ok

   SELECT

      AVG(product) / STDDEV(product) AS objective,

      SUM(estimate) AS backtest_profit,

      SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

         SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

      intraday, day

   FROM

   (

      SELECT

         ...

      FROM MqlRatesDB

      WHERE (time >= 1420070400 AND time < 1609459200)

   )

   GROUP BY intraday, day

   ORDER BY objective DESC

  #|             objective       backtest_profit       backtest_PF intraday day

---+---------------------------------------------------------------------------

  1|      0.16713214428916     0.073200000000001  1.46040631486258 16:00:00 5   

  2|     0.118128291843983    0.0433099999999995  1.33678071539657 20:00:00 3   

  3|     0.103701251751617   0.00929999999999853  1.14148790506616 05:00:00 2   

  4|     0.102930330078208    0.0164399999999973   1.1932071923845 08:00:00 4   

  5|     0.089531492651001    0.0064300000000006  1.10167615433271 07:00:00 2   

  6|    0.0827628326995007 -8.99999999970369e-05 0.999601152226913 17:00:00 4   

  7|    0.0823433025146974    0.0159700000000012  1.21665988332657 21:00:00 1   

  8|    0.0767938336191962   0.00522999999999874  1.04226945769012 13:00:00 1   

  9|    0.0657741522256548    0.0162299999999986  1.09699976093712 15:00:00 2   

 10|    0.0635243373432768   0.00932000000000044  1.08294766820933 22:00:00 3

...   

110|   -0.0814131025461459   -0.0189100000000015 0.820605255668329 21:00:00 5   

111|   -0.0899571263478305   -0.0321900000000028 0.721250432975386 22:00:00 4   

112|   -0.0909772560603298   -0.0226100000000016 0.851161872161138 19:00:00 4   

113|   -0.0961794181717023  -0.00846999999999931 0.936377976414036 12:00:00 5   

114|    -0.108868074018582   -0.0246099999999998 0.634920634920637 00:00:00 5   

115|    -0.109368419185336   -0.0250700000000013 0.744496534855268 08:00:00 2   

116|    -0.121893581607986   -0.0234599999999998 0.610945273631843 00:00:00 3   

117|    -0.135416609546408   -0.0898899999999971 0.343437294573087 00:00:00 1   

118|    -0.142128458003631   -0.0255200000000018 0.681835182645536 06:00:00 4   

119|    -0.142196924506816   -0.0205700000000004 0.629769618430515 00:00:00 2   

120|     -0.15200009633513   -0.0301499999999988 0.708864426419475 02:00:00 1   

Таким образом, анализ подсказывает нам, что 16-часовой бар H1 в пятницу является лучшим кандидатом для торговли в продолжение тренда на основании предыдущего бара. Следующий по предпочтительности — 20-часовой бар в среду. И так далее.

Однако желательно проверить найденные настройки на форвард-периоде.

Для этого мы можем выполнить текущий SQL-запрос не только на "прошлом" диапазоне дат (у нас в тесте до 2021-го года), но и еще раз — в "будущем" (с начала 2021-го). Результаты обоих запросов следует объединить (JOIN) по нашим группам (intraday, day). Тогда при сохранении сортировки по целевому показателю мы увидим в соседних колонках прибыли и профит-фактор для тех же сочетаний времени и дня недели, и насколько они просели.

Приведем здесь окончательный SQL-запрос (в сокращенном варианте):

SELECT * FROM

(

   SELECT

      AVG(product) / STDDEV(product) AS objective,

      SUM(estimate) AS backtest_profit,

      SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) / 

         SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

      intraday, day

   FROM

   (

      SELECT ...

      FROM MqlRatesDB

      WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

   )

   GROUP BY intraday, day

) backtest

JOIN

(

   SELECT

      SUM(estimate) AS forward_profit,

      SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

         SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS forward_PF,

      intraday, day

   FROM

   (

      SELECT ...

      FROM MqlRatesDB

      WHERE (time >= STRFTIME('%s', '2021-01-01'))

   )

   GROUP BY intraday, day

) forward

USING(intraday, day)

ORDER BY objective DESC

Полный текст с запросом находится в файле DBQuotesIntradayBackAndForward.sql. Он подключен как ресурс в скрипте DBQuotesIntradayBackAndForward.mq5.

Запустив скрипт с настройками по умолчанию, получим такие показатели (с сокращениями):

 #|          objective    backtest_profit    backtest_PF intraday day forward_profit     forward_PF

--+------------------------------------------------------------------------------------------------

 1|   0.16713214428916     0.073200000001  1.46040631486 16:00:00 5   0.004920000048  1.12852664576 

 2|  0.118128291843983    0.0433099999995  1.33678071539 20:00:00 3   0.007880000055    1.277856135 

 3|  0.103701251751617   0.00929999999853  1.14148790506 05:00:00 2   0.002210000082  1.12149532710 

 4|  0.102930330078208    0.0164399999973   1.1932071923 08:00:00 4   0.001409999969  1.07253086419 

 5|  0.089531492651001    0.0064300000006  1.10167615433 07:00:00 2  -0.009119999869 0.561749159058 

 6| 0.0827628326995007 -8.99999999970e-05 0.999601152226 17:00:00 4   0.009070000091  1.18809622563 

 7| 0.0823433025146974    0.0159700000012  1.21665988332 21:00:00 1    0.00250999999  1.12131464475 

 8| 0.0767938336191962   0.00522999999874  1.04226945769 13:00:00 1  -0.008490000055 0.753913043478 

 9| 0.0657741522256548    0.0162299999986  1.09699976093 15:00:00 2    0.01423999997  1.34979120609 

10| 0.0635243373432768   0.00932000000044  1.08294766820 22:00:00 3   -0.00456999993 0.828967065868

... 

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

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

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