
SQLite: нативная работа с базами данных на SQL в MQL5
Содержание
- Современный алготрейдинг в MetaTrader 5
- Функции для работы с базами данных
- Пример простого запроса
- Отладка SQL-запросов в MetaEditor
- Автоматическое вычитывание результатов запроса в структуру с помощью DatabaseReadBind()
- Ускорение транзакций обертыванием в DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Работа со сделками из торговой истории
- Анализ портфеля в разрезе стратегий
- Анализ сделок по символам
- Анализ сделок по часам входа
- Удобный вывод данных в журнал советников в DatabasePrint()
- Импорт и экспорт данных
- Сохранение результатов оптимизации в базу данных
- Оптимизация выполнения запросов с помощью индексов
- Интеграция работы с базами в MetaEditor
Современный алготрейдинг в 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-х простых правил:
- все хендлы запросов должны уничтожаться после использования с помощью DatabaseFinalize();
- база данных перед завершением работы должна закрываться с помощью DatabaseClose();
- результаты выполнения запросов должны проверяться;
- в случае ошибки необходимо сначала уничтожить запрос, а затем закрыть базу данных.
Самое сложное при этом — понять в чем состоит ошибка, если запрос не создался. MetaEditor позволяет открывать файлы *.sqlite и работать с ними с помощью SQL-запросов. Покажем как это делается на примере созданного файла company.sqlite:
1. Открываем в общей папке терминалов файл company.sqlite.
2. После открытия базы данных мы видим в Навигаторе таблицу COMPANY и дважды кликаем на ней.
3. В статусной строке автоматически создается запрос "SELECT * FROM COMPANY".
4. Запрос выполняется автоматически, его можно также выполнить по клавише F9 или нажатием на кнопке Execute.
5. Смотрим результат выполнения запроса.
6. Если что-то пошло не так, то ошибки будут показаны в Журнале редактора.
С помощью SQL-запросов можно получать статистику по полям таблицы, например, сумму и среднее. Составим запросы, убедимся, что они работают.
Теперь можем перенести эти запросы в 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() позволяет пройти по всем записям из результата запроса и далее получить полную информацию о каждом столбце в полученной таблице:
- DatabaseColumnName — имя,
- DatabaseColumnType — тип данных,
- DatabaseColumnSize — размер данных в байтах,
- DatabaseColumnText — вычитывает в виде текста,
- DatabaseColumnInteger — получает значение типа int,
- DatabaseColumnLong — получает значение типа long,
- DatabaseColumnDouble — получает значение типа double,
- DatabaseColumnBlob — получает массив данных.
Эти функции позволяют универсально работать с результатами любого запроса, но платой за это становится избыточный код. Если структура результатов запроса заранее известна, то лучше воспользоваться функцией 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-программе для дальнейшей работы.
Таким образом, вы можете готовить любые данные в нужном виде для дальнейшего анализа или обмена с другими трейдерами. Исходный код, 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. Просто укажите имя файла, название таблицы и добавьте все необходимые поля с указанием типа.
Далее можно заполнить таблицу данными, делать поиск и выборку, вводить SQL-запросы и т.д. Таким образом, работать с базами данных можно не только из MQL5-программ, но и вручную — для этого не требуется прибегать к сторонним браузерам.
Внедрение SQLite в MetaTrader открывает трейдерам новые возможности по обработке больших массивов данных как программно, так и вручную. Причем мы постарались сделать так, чтобы эти функции были максимально удобны в использовании и не уступали по скорости другим решениям. Изучайте и применяйте язык SQL-запросов в своей работе.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
В MetaEditor максимальное количество столбцов таблиц БД для показа всего лишь 23.
Можно снять ограничение?
В MetaEditor максимальное количество столбцов таблиц БД для показа всего лишь 23.
Можно снять ограничение?
не успешно -
Хочу обратить внимание, перевод целого в строку не выдает ошибку при записи в таблицу.
Уважаемые разработчики, подскажите пож-ста, почему не получается присоединить существующую базу из файла (ATTACH DATABASE)?
Тогда как получается присоединить базу из оперативки...
Код приложил.
Скрипт create_databases.mq5 создаёт БД. Скрипт attach_mem_db.mq5 присоединяет базу из оперативки. А в скрипте attach_other_db.mq5 не получается присоединить существующую БД.