English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
SQLite: нативная работа с базами данных на SQL в MQL5

SQLite: нативная работа с базами данных на SQL в MQL5

MetaTrader 5Тестер | 18 февраля 2020, 13:25
9 260 95
MetaQuotes
MetaQuotes

Содержание


Современный алготрейдинг в MetaTrader 5

MQL5 является идеальным решением для алготрейдинга, так как максимально близок к языку С++ как по синтакису, так и по скорости вычислений. Платформа MetaTrader 5 предоставила трейдерам современный специализированный язык для написания торговых роботов и пользовательских индикаторов, который позволяет выйти за рамки простых торговых задач  — здесь вы можете создавать аналитические системы любого уровня сложности.

Помимо асинхронных торговых функций и математических библиотек трейдерам доступны сетевые функции, импорт данных в Python, параллельные вычисления в OpenCL, нативная поддержка .NET библиотек с "умным" импортом функций, интеграция с MS Visual Studio и визуализация данных с помощью DirectX. Сегодня это необходимые инструменты в арсенале современного алготрейдинга, которые позволяют решать разнообразные задачи, не выходя за пределы торговой платформы MetaTrader 5.


Функции для работы с базами данных

Разработка торговых стратегий связана с обработкой больших объемов данных. Сегодня уже мало написать торговый алгоритм в виде надежной и быстрой MQL5-программы. Для получения надежных результатов трейдеру нужно также провести огромное количество тестов и оптимизаций на самых разных инструментах, сохранить результаты, обработать их, проанализировать и принять решение о том, куда двигаться дальше.

Теперь прямо в MQL5 вы можете работать с базами данных на простом и популярном движке SQLite. Результаты тестов на сайте разработчиков показывают высокую скорость выполнения SQL-запросов — в большинстве задач он опередил PostgreSQL и MySQL.  Мы в свою очередь сравнили скорость выполнений этих тестов на MQL5 и LLVM 9.0.0 и занесли их в таблицу. Результаты выполнения даны в миллисекундах — чем меньше, тем лучше.

Название
Описание
 LLVM  
MQL5
Test 1
 1000 INSERTs
11572
8488
Test 2
 25000 INSERTs in a transaction
59
60
Test 3
 25000 INSERTs into an indexed table
102
105
Test 4
 100 SELECTs without an index
142
150
Test 5
 100 SELECTs on a string comparison
391
390
Test 6
 Creating an index
43
33
Test 7
 5000 SELECTs with an index
385
307
Test 8
 1000 UPDATEs without an index
58
54
Test 9
 25000 UPDATEs with an index
161
165
Test 10
 25000 text UPDATEs with an index
124
120
Test 11  INSERTs from a SELECT
84
84
Test 12
 DELETE without an index
25
74
Test 13
 DELETE with an index
70
72
Test 14  A big INSERT after a big DELETE
62
66
Test 15  A big DELETE followed by many small INSERTs
33
33
Test 16  DROP TABLE: finished
42
40

Детали тестов вы можете найти в исходниках в приложенном файле SqLiteTest.zip. Технические характеристики компьютера, на котором проводились замеры — Windows 10 x64, Intel Xeon  E5-2690 v3 @ 2.60GHz. 

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


Пример простого запроса

Базы данных хранят информацию в виде таблиц, получение/модификация и добавление новых данных делается с помощью запросов на языке SQL. Покажем на примере, как создать простую базу данных и получить из неё информацию.

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string filename="company.sqlite";
//--- создадим или откроем базу данных в общей папке терминалов
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
... работа с базой данных


//--- закрываем базу данных
   DatabaseClose(db);
  }

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

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

//--- если таблица COMPANY существует, то удалим её
   if(DatabaseTableExists(db, "COMPANY"))
     {
      //--- удаляем таблицу
      if(!DatabaseExecute(db, "DROP TABLE COMPANY"))
        {
         Print("Failed to drop table COMPANY with code ", GetLastError());
         DatabaseClose(db);
         return;
        }
     }
//--- создаем таблицу COMPANY 
   if(!DatabaseExecute(db, "CREATE TABLE COMPANY("
                       "ID INT PRIMARY KEY     NOT NULL,"
                       "NAME           TEXT    NOT NULL,"
                       "AGE            INT     NOT NULL,"
                       "ADDRESS        CHAR(50),"
                       "SALARY         REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Удаление и создание таблицы совершается с помощью запросов, необходимо всегда проверять результат выполнения. В таблице COMPANY у нас всего 5 полей: ID записи, имя, возраст, адрес и зарплата. Причем поле ID является ключем, то есть уникальным индексом. Индексы позволяют однозначно определять каждую запись и могут использоваться в разных таблицах для того чтобы связывать их между собой. Это аналогично тому, как ID позиции связывает между собой все сделки и ордера,  которые относятся к конкретной позиции.

Теперь необходимо заполнить таблицу данными, делается это с помощью запроса "INSERT":

//--- вставляем данные в таблицу 
   if(!DatabaseExecute(db, "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
     {
      Print("DB: ", filename, " insert failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Видим, что в таблицу COMPANY  добавляются 4 записи, для каждой записи указывается порядок полей и значения, которые будут записаны в эти поля. Каждая запись вставляется отдельным запросом "INSERT....", которые объединены в один запрос. То есть мы могли бы каждую запись вставлять в таблицу отдельным вызовом DatabaseExecute().

Так как по окончании работы скрипта база будет сохранена в файл company.sqlite, то при последующем его запуске мы попытались бы записать те же самые данные в таблицу COMPANY с таким же ID. Это привело бы к ошибке — именно поэтому мы сначала удалили таблицу, чтобы при каждом запуске скрипта начинать работу с нуля.

Теперь получим все записи из таблицы COMPANY, где поле SALARY>15000.  Делается это с помощью функции DatabasePrepare(),  которая компилирует текст запроса и возвращает хендл на него для последующего использования в DatabaseRead() или DatabaseReadBind().

//--- создадим запрос и получим хендл на него
   int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

После того как запрос успешно создан, нам необходимо получить результаты его выполнения. Сделаем это с помощью DatabaseRead(), которая при первом вызове выполнит запрос и перейдет на первую запись в результатах. При каждом последующем вызове она будет просто считывать следующую запись, пока не дойдет до конца. В этом случае она вернет false, что означает "записей больше нет".

//--- распечатаем все записи с зарплатой больше 15000
   int    id, age;
   string name, address;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseRead(request); i++)
     {
      //--- прочитаем значения каждого поля из полученной записи
      if(DatabaseColumnInteger(request, 0, id) && DatabaseColumnText(request, 1, name) &&
         DatabaseColumnInteger(request, 2, age) && DatabaseColumnText(request, 3, address) && DatabaseColumnDouble(request, 4, salary))
         Print(i, ":  ", id, " ", name, " ", age, " ", address, " ", salary);
      else
        {
         Print(i, ": DatabaseRead() failed with code ", GetLastError());
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
        }
     }
//--- удалим запрос после использования
   DatabaseFinalize(request);

Результатом выполнения будет:

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0
Полный  код примера в файле DatabaseRead.mq5.

Отладка SQL-запросов в MetaEditor

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

  1. все хендлы запросов должны уничтожаться после использования с помощью DatabaseFinalize();
  2. база данных перед завершением работы должна закрываться с помощью DatabaseClose();
  3. результаты выполнения запросов должны проверяться;
  4. в случае ошибки необходимо сначала уничтожить запрос, а затем закрыть базу данных.

Самое сложное при этом — понять в чем состоит ошибка, если запрос не создался. MetaEditor позволяет открывать файлы *.sqlite и работать с ними с помощью SQL-запросов. Покажем как это делается на примере созданного файла company.sqlite:

1. Открываем в общей папке терминалов файл company.sqlite.

2.  После открытия базы данных мы видим в Навигаторе таблицу COMPANY  и дважды кликаем на ней.

3. В статусной строке автоматически создается запрос "SELECT * FROM COMPANY".

4. Запрос выполняется автоматически, его можно также выполнить по клавише F9 или нажатием на кнопке Execute.

5. Смотрим результат выполнения запроса.

6. Если что-то пошло не так, то ошибки будут показаны в Журнале редактора.


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

Работа с запросами в MetaEditor

Теперь можем перенести эти запросы в MQL5 код:

   Print("Some statistics:");
//--- подготовим новый запрос о сумме зарплат
   request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double total_salary;
      DatabaseColumnDouble(request, 0, total_salary);
      Print("Total salary=", total_salary);
     }
//--- удалим запрос после использования
   DatabaseFinalize(request);
 
//--- подготовим новый запрос о средней зарплате
   request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      ResetLastError();
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double aver_salary;
      DatabaseColumnDouble(request, 0, aver_salary);
      Print("Average salary=", aver_salary);
     }
//--- удалим запрос после использования
   DatabaseFinalize(request);

И сравним результаты выполнения:

Some statistics:
Total salary=125000.0
Average salary=31250.0


Автоматическое вычитывание результатов запроса в структуру с помощью DatabaseReadBind()

Функция DatabaseRead() позволяет пройти по всем записям из результата запроса и далее получить полную информацию о каждом столбце в полученной таблице:

Эти функции позволяют универсально работать с результатами любого запроса, но платой за это становится избыточный код. Если структура результатов запроса заранее известна, то лучше воспользоваться функцией DatabaseReadBind(), которая позволяет сразу считать всю запись целиком в структуру. Мы можем переделать предыдущий пример таким образом — сначала объявим структуру Person:

struct Person
  {
   int               id;
   string            name;
   int               age;
   string            address;
   double            salary;
  };

Затем сделаем вычитку каждой записи из результатов запроса с помощью DatabaseReadBind(request, person):

//--- выведем полученные результаты запроса
   Person person;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseReadBind(request, person); i++)
      Print(i, ":  ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary);
//--- удалим запрос после использования
   DatabaseFinalize(request);

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


Ускорение транзакций обертыванием в DatabaseTransactionBegin()/DatabaseTransactionCommit() 

Если при работе с таблицей требуется провести массовые команды INSERT, UPDATE или DELETE, то лучше это делать с помощью транзакций. При выполнении транзакции база данных сначала блокируется (DatabaseTransactionBegin), затем выполняются массовые команды изменений, и затем эти изменения сохраняются (DatabaseTransactionCommit) либо отменяются в случае ошибки (DatabaseTransactionRollback).

В описании функции DatabasePrepare показан пример использования транзакций:

//--- вспомогательные переменные
   ulong    deal_ticket;         // тикет сделки
   long     order_ticket;        // тикет ордера,по которому была совершена сделка
   long     position_ticket;     // ID позиции, к которой относится сделка
   datetime time;                // время совершения сделки
   long     type ;               // тип сделки
   long     entry ;              // направление сделки
   string   symbol;              // по какому символу была сделка
   double   volume;              // объем операции
   double   price;               // цена
   double   profit;              // финансовый результат
   double   swap;                // своп
   double   commission;          // комиссия
   long     magic;               // Magic number (ID советника)
   long     reason;              // причина или источник проведения сделки
//--- пройдем по всем сделкам и внесем их в базу данных
   bool failed=false;
   int deals=HistoryDealsTotal();
//--- заблокируем базу данных перед выполнением транзакций
   DatabaseTransactionBegin(database);
   for(int i=0; i<deals; i++)
     {
      deal_ticket=    HistoryDealGetTicket(i);
      order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
      position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
      time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
      type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
      entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
      symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
      volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
      price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
      profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
      swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
      commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
      magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
      reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
      //--- внесем в таблицу каждую сделку через запрос
      string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
      if(!DatabaseExecute(database, request_text))
        {
         PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
         PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
         failed=true;
         break;
        }
     }
//--- проверим на наличие ошибок при выполнении транзакций
   if(failed)
     {
      //--- откатим все транзакции и разблокируем базу данных
      DatabaseTransactionRollback(database);
      PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
      return(false);
     }
//--- все транзакции прошли успешно - зафиксируем изменения и разблокируем базу данных
   DatabaseTransactionCommit(database);

За счет применения транзакций вы можете ускорить массовые операции по изменению таблиц в сотни раз, как это показано в примере DatabaseTransactionBegin:

Результат:
   Торговая история насчитывает сделок: 2737 
   Transations WITH    DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds
   Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds
   Использование DatabaseTransactionBegin/DatabaseTransactionCommit дало ускорение в 532.8 раз


Работа со сделками из торговой истории

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

//--- заполним через SQL-запрос таблицу TRADES на основе данных из DEALS
   ulong start=GetMicrosecondCount();
   if(DatabaseTableExists(db, "DEALS"))
     {
      //--- заполним таблицу TRADES
      if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "   d1.time as time_in,"
                          "   d1.position_id as ticket,"
                          "   d1.type as type,"
                          "   d1.volume as volume,"
                          "   d1.symbol as symbol,"
                          "   d1.price as price_in,"
                          "   d2.time as time_out,"
                          "   d2.price as price_out,"
                          "   d1.commission+d2.commission as commission,"
                          "   d2.swap as swap,"
                          "   d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=0 AND d2.entry=1"))
        {
         Print("DB: fillng the TRADES table failed with code ", GetLastError());
         return;
        }
     }
   ulong transaction_time=GetMicrosecondCount()-start;

Здесь берется существующая таблица DEALS и внутренним объединением через INNER JOIN создаются записи из сделок с одинаковым DEAL_POSITION_ID. Результат работы примера  из DatabasePrepare на торговом счете:

Результат:
   Торговая история насчитывает сделок: 2741 
   Первые 10 сделок:
       [ticket] [order_ticket] [position_ticket]              [time] [type] [entry] [symbol] [volume]   [price]   [profit] [swap] [commission] [magic] [reason]
   [0] 34429573              0                 0 2019.09.05 22:39:59      2       0 ""        0.00000   0.00000 2000.00000 0.0000      0.00000       0        0
   [1] 34432127       51447238          51447238 2019.09.06 06:00:03      0       0 "USDCAD"  0.10000   1.32320    0.00000 0.0000     -0.16000     500        3
   [2] 34432128       51447239          51447239 2019.09.06 06:00:03      1       0 "USDCHF"  0.10000   0.98697    0.00000 0.0000     -0.16000     500        3
   [3] 34432450       51447565          51447565 2019.09.06 07:00:00      0       0 "EURUSD"  0.10000   1.10348    0.00000 0.0000     -0.18000     400        3
   [4] 34432456       51447571          51447571 2019.09.06 07:00:00      1       0 "AUDUSD"  0.10000   0.68203    0.00000 0.0000     -0.11000     400        3
   [5] 34432879       51448053          51448053 2019.09.06 08:00:00      1       0 "USDCHF"  0.10000   0.98701    0.00000 0.0000     -0.16000     600        3
   [6] 34432888       51448064          51448064 2019.09.06 08:00:00      0       0 "USDJPY"  0.10000 106.96200    0.00000 0.0000     -0.16000     600        3
   [7] 34435147       51450470          51450470 2019.09.06 10:30:00      1       0 "EURUSD"  0.10000   1.10399    0.00000 0.0000     -0.18000     100        3
   [8] 34435152       51450476          51450476 2019.09.06 10:30:00      0       0 "GBPUSD"  0.10000   1.23038    0.00000 0.0000     -0.20000     100        3
   [9] 34435154       51450479          51450479 2019.09.06 10:30:00      1       0 "EURJPY"  0.10000 118.12000    0.00000 0.0000     -0.18000     200        3
 
   Первые 10 трейдов:
                 [time_in] [ticket] [type] [volume] [symbol] [price_in]          [time_out] [price_out] [commission]   [swap]  [profit]
   [0] 2019.09.06 06:00:03 51447238      0  0.10000 "USDCAD"    1.32320 2019.09.06 18:00:00     1.31761     -0.32000  0.00000 -42.43000
   [1] 2019.09.06 06:00:03 51447239      1  0.10000 "USDCHF"    0.98697 2019.09.06 18:00:00     0.98641     -0.32000  0.00000   5.68000
   [2] 2019.09.06 07:00:00 51447565      0  0.10000 "EURUSD"    1.10348 2019.09.09 03:30:00     1.10217     -0.36000 -1.31000 -13.10000
   [3] 2019.09.06 07:00:00 51447571      1  0.10000 "AUDUSD"    0.68203 2019.09.09 03:30:00     0.68419     -0.22000  0.03000 -21.60000
   [4] 2019.09.06 08:00:00 51448053      1  0.10000 "USDCHF"    0.98701 2019.09.06 18:00:01     0.98640     -0.32000  0.00000   6.18000
   [5] 2019.09.06 08:00:00 51448064      0  0.10000 "USDJPY"  106.96200 2019.09.06 18:00:01   106.77000     -0.32000  0.00000 -17.98000
   [6] 2019.09.06 10:30:00 51450470      1  0.10000 "EURUSD"    1.10399 2019.09.06 14:30:00     1.10242     -0.36000  0.00000  15.70000
   [7] 2019.09.06 10:30:00 51450476      0  0.10000 "GBPUSD"    1.23038 2019.09.06 14:30:00     1.23040     -0.40000  0.00000   0.20000
   [8] 2019.09.06 10:30:00 51450479      1  0.10000 "EURJPY"  118.12000 2019.09.06 14:30:00   117.94100     -0.36000  0.00000  16.73000
   [9] 2019.09.06 10:30:00 51450480      0  0.10000 "GBPJPY"  131.65300 2019.09.06 14:30:01   131.62500     -0.40000  0.00000  -2.62000
   Заполнение таблицы TRADES заняло 12.51 milliseconds

Запустите этот скрипт на своем торговом счете с хеджинговым учетом и сравните результаты с позициями на истории. Если раньше у вас не хватало знаний или времени на то, чтобы закодировать циклы для получения такого результата, то теперь вы можете сделать это одним SQL-запросом. Результат работы скрипта можно посмотреть в MetaEditor — откройте приложенный файл trades.sqlite.


Анализ портфеля в разрезе стратегий

В вышеприведенных результатах работы скрипта из DatabasePrepare видно, что торговля ведется на нескольких валютных парах. Но кроме того, там же мы видим в столбце [magic]  значения от 100 до  600. Это значит, что на торговом счете ведут торговлю несколько стратегий, каждая из которых имеет свой собственный Magic Number для идентификации своих сделок.

С помощью SQL-запроса мы можем сделать анализ торговли в разрезе значений magic:

//--- получим торговую статистику в разрезе советников по Magic Number
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT MAGIC,"
                           "   sum(case when entry =1 then 1 else 0 end) as trades,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(swap) as total_swap,"
                           "   sum(commission) as total_commission,"
                           "   sum(profit) as total_profit,"
                           "   sum(profit+swap+commission) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM DEALS "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY MAGIC"
                           "   ) as r");

Результат:

Trade statistics by Magic Number
    [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0]     100      242     2584.80000  -2110.00000          -33.36000    -93.53000      474.80000    347.91000          143            99           1.96198      59.09091       40.90909         18.07552      -21.31313         1.22502
[1]     200      254     3021.92000  -2834.50000          -29.45000    -98.22000      187.42000     59.75000          140           114           0.73787      55.11811       44.88189         21.58514      -24.86404         1.06612
[2]     300      250     2489.08000  -2381.57000          -34.37000    -96.58000      107.51000    -23.44000          134           116           0.43004      53.60000       46.40000         18.57522      -20.53078         1.04514
[3]     400      224     1272.50000  -1283.00000          -24.43000    -64.80000      -10.50000    -99.73000          131            93          -0.04687      58.48214       41.51786          9.71374      -13.79570         0.99182
[4]     500      198     1141.23000  -1051.91000          -27.66000    -63.36000       89.32000     -1.70000          116            82           0.45111      58.58586       41.41414          9.83819      -12.82817         1.08491
[5]     600      214     1317.10000  -1396.03000          -34.12000    -68.48000      -78.93000   -181.53000          116            98          -0.36883      54.20561       45.79439         11.35431      -14.24520         0.94346

Мы видим, что 4 из 6 стратегий показали прибыль. И для каждой из стратегий мы получили статистические показатели:

  • trades — количество трейдов по стратегии,
  • gross_profit — общая прибыль по стратегии (сумма всех положительных значений profit),
  • gross_loss — общий убыток по стратегии (сумма всех отрицательных значений profit),
  • total_commission — сумма всех комиссий по трейдам стратегии,
  • total_swap — сумма всех свопов по трейдам стратегии,
  • total_profit — сумма gross_profit  и gross_loss,
  • net_profit — сумма (gross_profit  + gross_loss + total_commission + total_swap),
  • win_trades — количество трейдов, где profit>0,
  • loss_trades— количество трейдов, где profit<0,
  • expected_payoff — матожидание трейда без учета свопов и комисссий = net_profit/trades,
  • win_percent — процент выигрышных трейдов,
  • loss_percent — процент проигрышных трейдов,
  • average_profit — средний выигрыш = gross_profit/win_trades,
  • average_loss — средний проигрыш = gross_loss /loss_trades,
  • profit_factor — Профит фактор = gross_profit/gross_loss.

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


Анализ сделок по символам

Мы можем провести анализ торговли в разрезе символов. Для этого сделаем такой запрос:

//--- получим торговую статистику в разрезе символов
   int request=DatabasePrepare(db, "SELECT r.*,"
                               "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                               "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                               "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                               "   r.gross_profit/r.win_trades as average_profit,"
                               "   r.gross_loss/r.loss_trades as average_loss,"
                               "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                               "FROM "
                               "   ("
                               "   SELECT SYMBOL,"
                               "   sum(case when entry =1 then 1 else 0 end) as trades,"
                               "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                               "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                               "   sum(swap) as total_swap,"
                               "   sum(commission) as total_commission,"
                               "   sum(profit) as total_profit,"
                               "   sum(profit+swap+commission) as net_profit,"
                               "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                               "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                               "   FROM DEALS "
                               "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                               "   GROUP BY SYMBOL"
                               "   ) as r");

Результат:

Trade statistics by Symbol
      [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0] "AUDUSD"      112      503.20000   -568.00000           -8.83000    -24.64000      -64.80000    -98.27000           70            42          -0.57857      62.50000       37.50000          7.18857      -13.52381         0.88592
[1] "EURCHF"      125      607.71000   -956.85000          -11.77000    -45.02000     -349.14000   -405.93000           54            71          -2.79312      43.20000       56.80000         11.25389      -13.47676         0.63512
[2] "EURJPY"      127     1078.49000  -1057.83000          -10.61000    -45.76000       20.66000    -35.71000           64            63           0.16268      50.39370       49.60630         16.85141      -16.79095         1.01953
[3] "EURUSD"      233     1685.60000  -1386.80000          -41.00000    -83.76000      298.80000    174.04000          127           106           1.28240      54.50644       45.49356         13.27244      -13.08302         1.21546
[4] "GBPCHF"      125     1881.37000  -1424.72000          -22.60000    -51.56000      456.65000    382.49000           80            45           3.65320      64.00000       36.00000         23.51712      -31.66044         1.32052
[5] "GBPJPY"      127     1943.43000  -1776.67000          -18.84000    -52.46000      166.76000     95.46000           76            51           1.31307      59.84252       40.15748         25.57145      -34.83667         1.09386
[6] "GBPUSD"      121     1668.50000  -1438.20000           -7.96000    -49.93000      230.30000    172.41000           77            44           1.90331      63.63636       36.36364         21.66883      -32.68636         1.16013
[7] "USDCAD"       99      405.28000   -475.47000           -8.68000    -31.68000      -70.19000   -110.55000           51            48          -0.70899      51.51515       48.48485          7.94667       -9.90563         0.85238
[8] "USDCHF"      206     1588.32000  -1241.83000          -17.98000    -65.92000      346.49000    262.59000          131            75           1.68199      63.59223       36.40777         12.12458      -16.55773         1.27902
[9] "USDJPY"      107      464.73000   -730.64000          -35.12000    -34.24000     -265.91000   -335.27000           50            57          -2.48514      46.72897       53.27103          9.29460      -12.81825         0.63606

Статистика говорит нам, что только на 5 символах из 10 была получена чистая прибыль (net_profit>0), хотя на 6 из 10 профит фактор был положителен (profit_factor>1). Как раз тот случай, когда свопы и комиссии делают стратегию проигрышной на EURJPY.


Анализ сделок по часам входа

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

//--- получим торговую статистику в разрезе часа входа в рынок
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT HOUR_IN,"
                           "   count() as trades,"
                           "   sum(volume) as volume,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(profit) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM TRADES "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY HOUR_IN"
                           "   ) as r");

Результат:

Trade statistics by entry hour
     [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[ 0]         0       50  5.00000      336.51000   -747.47000   -410.96000           21            29          -8.21920      42.00000       58.00000         16.02429      -25.77483         0.45020
[ 1]         1       20  2.00000      102.56000    -57.20000     45.36000           12             8           2.26800      60.00000       40.00000          8.54667       -7.15000         1.79301
[ 2]         2        6  0.60000       38.55000    -14.60000     23.95000            5             1           3.99167      83.33333       16.66667          7.71000      -14.60000         2.64041
[ 3]         3       38  3.80000      173.84000   -200.15000    -26.31000           22            16          -0.69237      57.89474       42.10526          7.90182      -12.50938         0.86855
[ 4]         4       60  6.00000      361.44000   -389.40000    -27.96000           27            33          -0.46600      45.00000       55.00000         13.38667      -11.80000         0.92820
[ 5]         5       32  3.20000      157.43000   -179.89000    -22.46000           20            12          -0.70187      62.50000       37.50000          7.87150      -14.99083         0.87515
[ 6]         6       18  1.80000       95.59000   -162.33000    -66.74000           11             7          -3.70778      61.11111       38.88889          8.69000      -23.19000         0.58886
[ 7]         7       14  1.40000       38.48000   -134.30000    -95.82000            9             5          -6.84429      64.28571       35.71429          4.27556      -26.86000         0.28652
[ 8]         8       42  4.20000      368.48000   -322.30000     46.18000           24            18           1.09952      57.14286       42.85714         15.35333      -17.90556         1.14328
[ 9]         9      118 11.80000     1121.62000   -875.21000    246.41000           72            46           2.08822      61.01695       38.98305         15.57806      -19.02630         1.28154
[10]        10      206 20.60000     2280.59000  -2021.80000    258.79000          115            91           1.25626      55.82524       44.17476         19.83122      -22.21758         1.12800
[11]        11      138 13.80000     1377.02000   -994.18000    382.84000           84            54           2.77420      60.86957       39.13043         16.39310      -18.41074         1.38508
[12]        12      152 15.20000     1247.56000  -1463.80000   -216.24000           84            68          -1.42263      55.26316       44.73684         14.85190      -21.52647         0.85227
[13]        13       64  6.40000      778.27000   -516.22000    262.05000           36            28           4.09453      56.25000       43.75000         21.61861      -18.43643         1.50763
[14]        14       62  6.20000      536.93000   -427.47000    109.46000           38            24           1.76548      61.29032       38.70968         14.12974      -17.81125         1.25606
[15]        15       50  5.00000      699.92000   -413.00000    286.92000           28            22           5.73840      56.00000       44.00000         24.99714      -18.77273         1.69472
[16]        16       88  8.80000      778.55000   -514.00000    264.55000           51            37           3.00625      57.95455       42.04545         15.26569      -13.89189         1.51469
[17]        17       76  7.60000      533.92000  -1019.46000   -485.54000           44            32          -6.38868      57.89474       42.10526         12.13455      -31.85813         0.52373
[18]        18       52  5.20000      237.17000   -246.78000     -9.61000           24            28          -0.18481      46.15385       53.84615          9.88208       -8.81357         0.96106
[19]        19       52  5.20000      407.67000   -150.36000    257.31000           30            22           4.94827      57.69231       42.30769         13.58900       -6.83455         2.71129
[20]        20       18  1.80000       65.92000    -89.09000    -23.17000            9             9          -1.28722      50.00000       50.00000          7.32444       -9.89889         0.73993
[21]        21       10  1.00000       41.86000    -32.38000      9.48000            7             3           0.94800      70.00000       30.00000          5.98000      -10.79333         1.29277
[22]        22       14  1.40000       45.55000    -83.72000    -38.17000            6             8          -2.72643      42.85714       57.14286          7.59167      -10.46500         0.54408
[23]        23        2  0.20000        1.20000     -1.90000     -0.70000            1             1          -0.35000      50.00000       50.00000          1.20000       -1.90000         0.63158

Хорошо видно, что наибольшее количество трейдов совершается в интервале с 9 по 16 часов включительно. Торговля в другие часы дает меньше трейдов и в основном убыточна. Полный исходный код с этими 3 видами запросов вы можете найти в примере к функции DatabaseExecute().


Удобный вывод данных в журнал советников в DatabasePrint()

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

long  DatabasePrint(
   int     database,          // хендл базы данных, полученный в DatabaseOpen
   string  table_or_sql,      // таблица или SQL-запрос
   uint    flags              // комбинация флагов
   );

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

   DatabasePrint(db,"SELECT * from DEALS",0);

Результат (показаны первые 10 строк таблицы):

  #|       ID ORDER_ID POSITION_ID       TIME TYPE ENTRY SYMBOL VOLUME   PRICE  PROFIT  SWAP COMMISSION MAGIC REASON
---+----------------------------------------------------------------------------------------------------------------
  1| 34429573        0           0 1567723199    2     0           0.0     0.0  2000.0   0.0        0.0     0      0 
  2| 34432127 51447238    51447238 1567749603    0     0 USDCAD    0.1  1.3232     0.0   0.0      -0.16   500      3 
  3| 34432128 51447239    51447239 1567749603    1     0 USDCHF    0.1 0.98697     0.0   0.0      -0.16   500      3 
  4| 34432450 51447565    51447565 1567753200    0     0 EURUSD    0.1 1.10348     0.0   0.0      -0.18   400      3 
  5| 34432456 51447571    51447571 1567753200    1     0 AUDUSD    0.1 0.68203     0.0   0.0      -0.11   400      3 
  6| 34432879 51448053    51448053 1567756800    1     0 USDCHF    0.1 0.98701     0.0   0.0      -0.16   600      3 
  7| 34432888 51448064    51448064 1567756800    0     0 USDJPY    0.1 106.962     0.0   0.0      -0.16   600      3 
  8| 34435147 51450470    51450470 1567765800    1     0 EURUSD    0.1 1.10399     0.0   0.0      -0.18   100      3 
  9| 34435152 51450476    51450476 1567765800    0     0 GBPUSD    0.1 1.23038     0.0   0.0       -0.2   100      3 
 10| 34435154 51450479    51450479 1567765800    1     0 EURJPY    0.1  118.12     0.0   0.0      -0.18   200      3 


Импорт и экспорт данных

Также для облегчения импорта и экспорта данных добавлены функции DatabaseImport() и DatabaseExport(). Эти функции позволяют работать с CSV-файлами и данными, записанными в ZIP-архив.

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

DatabaseExport() позволяет сохранить в файл таблицу или результаты запроса. Если экспортируются результаты запроса, то SQL-запрос должен начинаться с "SELECT" или "select". Другими словами, SQL-запрос не может изменять состояние базы данных, в противном случае DatabaseExport() завершится ошибкой.

Полное описание функций смотрите в MQL5 Документации.


Сохранение результатов оптимизации в базу данных

Функции по работе с базами данных можно также использовать и для обработки результатов оптимизации. Покажем на примере советника "MACD Sample" из стандартной поставки, как получать результаты тестирования с помощью фреймов, и затем сохранить значения всех критериев оптимизации в один файл. Для этого создадим класс CDatabaseFrames, в котором определим метод OnTester() для отправки торговой статистики:

//+------------------------------------------------------------------+
//| Tester function - отправляет во фрейме торговую статистику       |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTester(const double OnTesterValue)
  {
//--- массив stats[] для отправки данных во фрейм
   double stats[16];
//--- статистику по трейдам заведем в отдельные переменные для наглядности
   int    trades=(int)TesterStatistics(STAT_TRADES);
   double win_trades_percent=0;
   if(trades>0)
      win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades;
//--- заполним массив результатами тестирования
   stats[0]=trades;                                       // количество трейдов
   stats[1]=win_trades_percent;                           // процент прибыльных трейдов
   stats[2]=TesterStatistics(STAT_PROFIT);                // чистая прибыль
   stats[3]=TesterStatistics(STAT_GROSS_PROFIT);          // общая прибыль
   stats[4]=TesterStatistics(STAT_GROSS_LOSS);            // общий убыток
   stats[5]=TesterStatistics(STAT_SHARPE_RATIO);          // коэффициент Шарпа
   stats[6]=TesterStatistics(STAT_PROFIT_FACTOR);         // профит-фактор
   stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR);       // фактор восстановления
   stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // матожидание трейда
   stats[9]=OnTesterValue;                                // пользовательский критерий оптимизации
//--- вычислим встроенные стандартные критерии оптимизации
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   double balance_plus_profitfactor=0;
   if(TesterStatistics(STAT_GROSS_LOSS)!=0)
      balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR);
   double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF);
   double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT);
   double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR);
   double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO);
//--- добавим значения встроенных критериев оптимизации
   stats[10]=balance;                                     // Balance
   stats[11]=balance_plus_profitfactor;                   // Balance+ProfitFactor
   stats[12]=balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   stats[13]=balance_plus_dd;                             // Balance+EquityDrawdown
   stats[14]=balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   stats[15]=balance_plus_sharpe;                         // Balance+Sharpe
//--- создадим фрейм с данными и отправим его в терминал
   if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats))
      Print("Frame add error: ", GetLastError());
   else
      Print("Frame added, Ok");
  }

Второй важный метод этого класса  OnTesterDeinit() — по окончании оптимизации он читает все полученные фреймы  и сохраняет статистику в базу данных:

//+------------------------------------------------------------------+
//| TesterDeinit function - читает данные из фреймов                 |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTesterDeinit(void)
  {
//--- возьмем имя советника и время окончания оптимизации
   string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite";
   StringReplace(filename, ":", "."); // символ ":" запрещен в названиях файлов
//--- открываем/создаем базу данных в общей папке терминалов
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
   else
      Print("DB: ", filename, " opened successful");
//--- создаем таблицу PASSES
   if(!DatabaseExecute(db, "CREATE TABLE PASSES("
                       "PASS               INT PRIMARY KEY NOT NULL,"
                       "TRADES             INT,"
                       "WIN_TRADES         INT,"
                       "PROFIT             REAL,"
                       "GROSS_PROFIT       REAL,"
                       "GROSS_LOSS         REAL,"
                       "SHARPE_RATIO       REAL,"
                       "PROFIT_FACTOR      REAL,"
                       "RECOVERY_FACTOR    REAL,"
                       "EXPECTED_PAYOFF    REAL,"
                       "ON_TESTER          REAL,"
                       "BL_BALANCE         REAL,"
                       "BL_PROFITFACTOR    REAL,"
                       "BL_EXPECTEDPAYOFF  REAL,"
                       "BL_DD              REAL,"
                       "BL_RECOVERYFACTOR  REAL,"
                       "BL_SHARPE          REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
//--- переменные для чтения фреймов
   string        name;
   ulong         pass;
   long          id;
   double        value;
   double        stats[];
//--- переведем указатель фреймов в начало
   FrameFirst();
   FrameFilter("", STATS_FRAME); // выбираем для работы фреймы с торговой статистикой
//--- переменные для получения статистики из фрейма
   int trades;
   double win_trades_percent;
   double profit, gross_profit, gross_loss;
   double sharpe_ratio, profit_factor, recovery_factor, expected_payoff;
   double ontester_value;                              // пользовательский критерий оптимизации
   double balance;                                     // Balance
   double balance_plus_profitfactor;                   // Balance+ProfitFactor
   double balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   double balance_plus_dd;                             // Balance+EquityDrawdown
   double balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   double balance_plus_sharpe;                         // Balance+Sharpe
//--- блокируем базу данных на время массовых транзакций
   DatabaseTransactionBegin(db);
//--- проходим по фреймам и читаем данные из них
   bool failed=false;
   while(FrameNext(pass, name, id, value, stats))
     {
      Print("Got pass #", pass);
      trades=(int)stats[0];
      win_trades_percent=stats[1];
      profit=stats[2];
      gross_profit=stats[3];
      gross_loss=stats[4];
      sharpe_ratio=stats[5];
      profit_factor=stats[6];
      recovery_factor=stats[7];
      expected_payoff=stats[8];
      stats[9];
      balance=stats[10];
      balance_plus_profitfactor=stats[11];
      balance_plus_expectedpayoff=stats[12];
      balance_plus_dd=stats[13];
      balance_plus_recoveryfactor=stats[14];
      balance_plus_sharpe=stats[15];
      PrintFormat("VALUES (%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%2.f,%.2f,%.2f,%.2f,%.2f)",
                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                  balance_plus_sharpe);
      //--- запишем данные в таблицу
      string request=StringFormat("INSERT INTO PASSES (PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS,"
                                  "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER,"
                                  "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) "
                                  "VALUES (%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)",
                                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                                  balance_plus_sharpe);

      //--- выполним запрос на добавление прохода в таблицу PASSES
      if(!DatabaseExecute(db, request))
        {
         PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError());
         failed=true;
         break;
        }
     }
//--- если при выполнении транзакции произошла ошибка, сообщим и завершим работу
   if(failed)
     {
      Print("Transaction failed, error code=", GetLastError());
      DatabaseTransactionRollback(db);
      DatabaseClose(db);
      return;
     }
   else
     {
      DatabaseTransactionCommit(db);
      Print("Transaction done successful");
     }
//--- закроем базу данных
   if(db!=INVALID_HANDLE)
     {
      Print("Close database with handle=", db);
      DatabaseClose(db);
     }

Далее в советнике "MACD Sample" подключаем файл DatabaseFrames.mqh и объявляем переменную класса CDatabaseFrames:

#define MACD_MAGIC 1234502
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include "DatabaseFrames.mqh"
...
CDatabaseFrames DB_Frames;

Затем добавляем в конце советника три функции, которые будут вызываться только при оптимизации:

//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
int OnTesterInit()
  {
   return(DB_Frames.OnTesterInit());
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   DB_Frames.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double ret=0;
   //--- создадим пользовательский критерий оптимизации как отношение чистой прибыли к относительной просадке по балансу
   if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0)
      ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
   DB_Frames.OnTester(ret);
   return(ret);
  }
//+------------------------------------------------------------------+

Запускаем оптимизацию и получаем в общей папке терминалов файл базы данных с торговой статистикой:

CDatabaseFrames::OnTesterInit: оптимизация запущена в 15:53:27
DB: MACD Sample Database 2020.01.20 15.53.sqlite opened successful
Transaction done successful
Close database with handle=65537
Database stored in file 'MACD Sample Database 2020.01.20 15.53.sqlite'

Созданный файл с базой данных можно открыть в MetaEditor или использовать в другой MQL5-программе для дальнейшей работы.

Работа с базой данных в MetaEditor

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


Оптимизация выполнения запросов с помощью индексов

Прелесть использования SQL (во всех его реализациях, а не только SQLite) состоит в том, что это декларативный язык, а не процедурный язык. При программировании на SQL вы сообщаете исполняющей системе, ЧТО вы хотите вычислить, а не КАК это вычислять. Задача "как это сделать" делегируется подсистеме планировщика запросов в ядре базы данных SQL.

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

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

Поиск без индексов

Пусть у нас есть таблица DEALS со сделками, которая содержит указанные 14 полей. Вот первые 10 записей этой таблицы.

rowid
ID ORDER_ID POSITION_ID TIME TYPE ENTRY SYMBOL VOLUME PRICE PROFIT SWAP COMMISSION MAGIC REASON
1 34429573 0 0 1567723199 2 0 0 0 2000 0 0 0 0
2 34432127 51447238 51447238 1567749603 0 0 USDCAD 0.1 1.3232 0 0 -0.16 500 3
3 34432128 51447239 51447239 1567749603 1 0 USDCHF 0.1 0.98697 0 0 -0.16 500 3
4 34432450 51447565 51447565 1567753200 0 0 EURUSD 0.1 1.10348 0 0 -0.18 400 3
5 34432456 51447571 51447571 1567753200 1 0 AUDUSD 0.1 0.68203 0 0 -0.11 400 3
6 34432879 51448053 51448053 1567756800 1 0 USDCHF 0.1 0.98701 0 0 -0.16 600 3
7 34432888 51448064 51448064 1567756800 0 0 USDJPY 0.1 106.962 0 0 -0.16 600 3
8 34435147 51450470 51450470 1567765800 1 0 EURUSD 0.1 1.10399 0 0 -0.18 100 3
9 34435152 51450476 51450476 1567765800 0 0 GBPUSD 0.1 1.23038 0 0 -0.2 100 3
10 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0 0 -0.18 200 3

В ней представлена информация из раздела Свойства сделок (за исключением DEAL_TIME_MSC, DEAL_COMMENT и DEAL_EXTERNAL_ID), необходимая для анализа торговой истории. В каждой таблице помимо самой хранимой информации всегда есть целочисленный ключ rowid, за которым следуют поля записи. Значения ключа rowid создаются автоматически, являются уникальными в пределах таблицы и увеличиваются при добавлении новых записей. При удалении записей могут возникать разрывы в нумерации, но строки таблицы всегда хранятся в порядке возрастания rowid .

Если нам потребуется найти сделки, которые относятся к какой-то конкретной позиции , например, ID=51447571, то мы напишем запрос вида:

SELECT * FROM deals WHERE position_id=51447571

В этом случае будет произведено полное сканирование таблицы — будут просмотрены все строки и в каждой строке будет произведена проверка поля POSITION_ID на равенство значению 51447571. Строки, удовлетворяющие данному условию будут выданы в результатах выполнения запроса. Если таблица содержит милллионы или десятки миллионов записей, то поиск может занимать существенное время. Если бы сделали поиск не по условию position_id=51447571, а по условию rowid=5, то время поиска сократилось бы в тысячи или даже миллионы раз (зависит от размера таблицы).

SELECT * FROM deals WHERE rowid=5

Результат выполнения запроса был бы таким же, так как строка с rowid=5 хранит position_id=51447571. Ускорение достигается за счет того, что значения rowid отсортированы по возрастанию, и для получения результата используется бинарный (или двоичный) поиск. Но, к сожалению, поиск по значению rowid нам не подходит, так как нас интересуют записи с нужным значением position_id.

Поиск по индексу

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

 CREATE INDEX Idx1 ON deals(position_id)

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

POSITION_ID rowid
0 1
51447238 2
51447239 3
51447565 4
51447571 5
51448053 6
51448064 7
51450470 8
51450476 9
51450479 10

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

Теперь, когда у нас есть индекс по полю POSITION_ID, наш запрос

SELECT * FROM deals WHERE position_id=51447571

будет выполняться иначе. Сначала будет произведен бинарный поиск в индексе Idx1 по столбцу POSITION_ID и найдены все соответствующие условию rowid. И затем вторым бинарным поиском в исходной таблице DEALS будет произведена выборка всех записей по известным значениям rowid. Таким образом, теперь вместо одного полного сканирования большой таблицы происходит два последовательных поиска — сначала по индексу, а затем по номерам строк таблицы. Это позволяет сократить время выполнения подобных запросов в тысячи и более раз при большом количестве строк в таблице.

Общее правило: если какие-то поля в таблице используются часто для поиска/сравнения/сортировки, то рекомендуется создавать индексы по этим полям.

В данной таблице DEALS есть еще и такие поля, как SYMBOL, MAGIC (идентификатор советника) и ENTRY (направление входа). Если вам понадобится делать выборки по данным полям, то желательно создать по ним соответствующие индексы. Например:

CREATE INDEX Idx2 ON deals(symbol)
CREATE INDEX Idx3 ON deals(magic)
CREATE INDEX Idx4 ON deals(entry)

При этом необходимо иметь в виду, что создание индексов требует дополнительной памяти и при каждом добавлении/удалении записей происходит реиндексация. Можно создавать также мультииндексы на основе нескольких полей. Например, если нам требуется выбрать все сделки, которые были совершены советником с MAGIC= 500 на символе USDCAD, то мы можем создать такой запрос:

SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'

В этом случае можно создать мультииндекс по полям MAGIC и SYMBOL

CREATE INDEX Idx5 ON deals(magic, symbol)

и тогда будет создана таблица индекса такого вида (схематично показаны первые 10 строк)

MAGIC SYMBOL rowid
100 EURUSD 4
100 EURUSD 10
100 EURUSD 20
100 GBPUSD 5
100 GBPUSD 11
200 EURJPY 6
200 EURJPY 12
200 EURJPY 22
200 GBPJPY 7
200 GBPJPY 13

В созданном мультииндексе записи сначала сортируются блоками по MAGIC, затем по полю SYMBOL. Поэтому, при AND-запросах поиск в индексе сначала будет идти по первому столбцу MAGIC, а затем уже проверяться значение второго столбца SYMBOL. Если оба условия соблюдены, то rowid добавляется в результирующую выборку, по которой уже будет сделан поиск в исходной таблице. Вообще говоря, данный мультииндекс уже не подходит для запросов, где сначала проверяется SYMBOL, а затем MAGIC

SELECT * FROM deals WHERE  symbol='USDCAD' AND magic=500 

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

OR-запросы

Мультииндексы подходят только для AND-запросов. Например, мы хотим найти все сделки, которые совершены советником c MAGIC=100 или по символу EURUSD:

SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'

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

SELECT * FROM deals WHERE magic=100 
SELECT * FROM deals WHERE symbol='EURUSD'

Но даже и в этом случае необходимо, чтобы оба поля OR-запроса имели индексы, так как в противном случае поиск приведет к полному сканированию таблицы.

Сортировка

Для ускорения сортировки также рекомендуется иметь индекс по тем полям, по которым происходит упорядочивание результатов запроса. Например, нужно выбрать все сделки по EURUSD, отсортированные по времени совершения сделки:

SELECT * FROM deals symbol='EURUSD' ORDER BY time

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

Здесь мы рассмотрели только самые основы по оптимизации запросов, для лучшего понимания рекомендуем начать изучение данной темы с раздела Query Planning на сайте разработчиков SQLite.


Интеграция работы с базами в MetaEditor

Развитие платформы  MetaTrader 5 продолжается. Мы добавили в язык MQL5 нативную поддержку SQL-запросов и встроили в MetaEditor новый функционал по работе с базами данных — создание базы, вставку и удаление данных, проведение массовых транзакций. Создание базы данных делается стандартно с помощью MQL5 Wizard. Просто укажите имя файла, название таблицы и добавьте все необходимые поля с указанием типа.

Создание базы данных в MQL Wizard

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

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

Прикрепленные файлы |
SqLiteTest.zip (2708.45 KB)
trades.sqlite (340 KB)
MACD.zip (8.62 KB)
DatabaseRead.mq5 (10.14 KB)
DatabasePrepare.mq5 (34.91 KB)
DatabaseExecute.mq5 (64.54 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (95)
DDFedor
DDFedor | 7 нояб. 2022 в 11:32
Не работает горячая клавиша F9 - выполнить запрос. Ожидание заявленного можно увидеть, если навести курсор на кнопку "Выполнить". Но, ни при нажатии F9 с курсором в командной строке, ни при перемещении курсора ( указателя мыши ) в другое(другие) положения заявленная F9 не отрабатывает. Использовать мышь для нажатия "Выполнить" - крайне не привлекательно. 
Anatoli Kazharski
Anatoli Kazharski | 2 дек. 2022 в 18:25

В MetaEditor максимальное количество столбцов таблиц БД для показа всего лишь 23.

Можно снять ограничение?

MetaQuotes
MetaQuotes | 7 дек. 2022 в 05:44
Anatoli Kazharski #:

В MetaEditor максимальное количество столбцов таблиц БД для показа всего лишь 23.

Можно снять ограничение?

Исправлено в бете 3531 с увеличением колонок до 64.
DDFedor
DDFedor | 17 дек. 2022 в 22:35
В какой-то момент где-то прозвучал ответ про одинарные и двойные кавычки. Не дословно, но близко к тексту - "работает давно с двойными кавычками". На текущий момент, возвращаясь к работе с таблицами, попытка записи в таблицу текста в двойных кавычках заканчивается неудачей. Однако, заключив текст в одинарные, а затем в двойные кавычки, запись завершается успешно. Каким образом звучит правило одинарных и двойных кавычек для записи текста в таблицу? 
Варианты и исходы : 
успешно  - 
         AddTable_TstDate(i,
                          iTime(Symbol(),PERIOD_CURRENT,i),
                          iHigh(Symbol(),PERIOD_CURRENT,i),
                          iTime(Symbol(),PERIOD_CURRENT,i),
                          iLow(Symbol(),PERIOD_CURRENT,i),
                          IntegerToString(iTime(Symbol(),PERIOD_CURRENT,i)),
                          1121,
                          "'string_no_error'");

не успешно  - 

         AddTable_TstDate(i,
                          iTime(Symbol(),PERIOD_CURRENT,i),
                          iHigh(Symbol(),PERIOD_CURRENT,i),
                          iTime(Symbol(),PERIOD_CURRENT,i),
                          iLow(Symbol(),PERIOD_CURRENT,i),
                          IntegerToString(iTime(Symbol(),PERIOD_CURRENT,i)),
                          1121,
                          "string_error");

Хочу обратить внимание, перевод целого в строку не выдает ошибку при записи в таблицу. 

Denis Kirichenko
Denis Kirichenko | 29 дек. 2022 в 21:59

Уважаемые разработчики, подскажите пож-ста, почему не получается присоединить существующую базу из файла (ATTACH DATABASE)?

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

Код приложил.

Скрипт create_databases.mq5 создаёт БД. Скрипт attach_mem_db.mq5 присоединяет базу из оперативки. А в скрипте attach_other_db.mq5 не получается присоединить существующую БД.

Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа
С этой статьи мы открываем новую серию описания создания библиотеки "DoEasy" для простого и быстрого создания программ. Сегодня начнём подготавливать функционал библиотеки для доступа и работе с данными таймсерий символов. Создадим объект "Бар", хранящий основные и расширенные данные бара таймсерии, и разместим объекты-бары в список-таймсерию для удобного поиска и сортировки этих объектов.
Работа с сетевыми функциями, или MySQL без DLL: Часть II - программа для мониторинга изменения свойств сигналов Работа с сетевыми функциями, или MySQL без DLL: Часть II - программа для мониторинга изменения свойств сигналов
В предыдущей части статьи мы ознакомились с реализацией коннектора MySQL. В этой части мы рассмотрим его применение на примере реализации сервиса сбора свойств сигналов и программы для просмотра их изменения с течением времени. Кроме того, реализованный пример может иметь практический смысл в том случае, если пользователю нужно наблюдать изменения свойств, которые не отображаются на веб-странице сигнала.
Непрерывная скользящая оптимизация (Часть 5): Обзор проекта автооптимизатора, а также создание графического интерфейса Непрерывная скользящая оптимизация (Часть 5): Обзор проекта автооптимизатора, а также создание графического интерфейса
Продолжаем описание скользящей оптимизации в терминале MetaTrader 5. Рассмотрев в прошлых статьях методы формирования отчета оптимизации и способ его фильтрации, мы перешли к описанию внутренней структуры приложения, отвечающего за сам процесс оптимизации. Автооптимизатор, выполненный как приложение на C#, имеет собственный графический интерфейс. Именно созданию данного графического интерфейса и посвящена текущая статья.
Прогнозирование временных рядов (Часть 1): метод эмпирической модовой декомпозиции (EMD) Прогнозирование временных рядов (Часть 1): метод эмпирической модовой декомпозиции (EMD)
В статье рассмотрена теория и практическое применение алгоритма прогнозирования временных рядов на основе эмпирической модовой декомпозиции, предложена его реализации на MQL, предоставлены тестовые индикаторы и эксперты.