
Рецепты MQL5 — Сервисы
Введение
Относительно недавно в торговом терминале MetaTrader 5 появился такой тип программы, как сервис. Как заявляет разработчик, сервисы позволяют создавать собственные источники ценовых данных для терминала — передавать цены от внешних систем в режиме реального времени так, как это делают торговые серверы брокеров. И это далеко не единственная возможность сервисов.
В данной статье рассмотрим нюансы работы с сервисами и познакомимся с их замечательными свойствами. Материал статьи скорее ориентирован на новичка. Исходя из этого, я старался написать код таким образом, чтобы он был полностью воспроизводим и усложнялся от примера к примеру.
1. Демоны на службе
Наверное не секрет, что сервисы в MQL5 имеют сходство со службами Windows. Википедия даёт такое определение службы:
В нашем случае внешней средой для сервисов выступает не сама операционная система, а оболочка терминала MetaTrader5.
И несколько слов о демонах.
Де́мон (daemon, dæmon, др.-греч. δαίμων дэймон) — компьютерная программа в UNIX-подобных системах, запускаемая самой системой и работающая в фоновом режиме без прямого взаимодействия с пользователем.
На мой взгляд очень точно отражает суть определение термина:
Термин был придуман программистами проекта MAC (англ.)рус. Массачусетского технологического института, он отсылает к персонажу мысленного эксперимента, демону Максвелла, занимающегося сортировкой молекул в фоновом режиме.[1] UNIX и UNIX-подобные системы унаследовали данную терминологию.
Демон также является персонажем греческой мифологии, выполняющим задачи, за которые не хотят браться боги. Как утверждается в «Справочнике системного администратора UNIX», в Древней Греции понятие «личный демон» было, отчасти, сопоставимо с современным понятием «ангел-хранитель».[2]
Интересно, что хотя у древних греков и не было компьютеров, но отношения сущностей для них были понятны. Таким образом, в контексте статьи будем заниматься тем, за что не берутся боги ))
2. Сервисы – что есть в Документации?
Прежде чем углубляться в тему, предлагаю бегло пройтись по материалам Документации и посмотреть, как разработчик описывает возможности сервисов.
2.1 Виды приложений
На первой странице Документации в разделе «Виды приложений в MQL5» сервис определён как тип MQL5-программы:
- Сервис — программа, которая в отличие от индикаторов, советиников и скриптов для своей работы не требует привязки к графику. Как и скрипты, сервисы не обрабатывают никаких событий, кроме события запуска. Для запуска сервиса в его коде обязательно должна быть функция-обработчик OnStart. Сервисы не принимают никаких других событий кроме Start, но могут сами отправлять графикам пользовательские события с помощью EventChartCustom. Сервисы хранятся в директории <каталог_терминала>\MQL5\Services.
Отметим тут, что сервисы очень похожи на скрипты. Принципиальное отличие состоит в том, что они не привязаны ни к одному из графиков.
2.2 Выполнение программ
В разделе «Выполнение программ» представлена сводка по программам на MQL5:
Программа | Выполнение | Примечание |
---|---|---|
Сервис | В собственном потоке, сколько сервисов - столько потоков выполнения для них | Зацикленный сервис не может нарушить работу других программ |
Скрипт | В собственном потоке, сколько скриптов - столько потоков выполнения для них | Зацикленный скрипт не может нарушить работу других программ |
Эксперт | В собственном потоке, сколько экспертов - столько потоков выполнения для них | Зацикленный эксперт не может нарушить работу других программ |
Индикатор | Один поток выполнения для всех индикаторов на одном символе. Сколько символов с индикаторами - столько потоков выполнения для них | Бесконечный цикл в одном индикаторе остановит работу всех остальных индикаторов на этом символе |
Т.е. по способу задействования потока выполнения сервисы не отличаются от скриптов и экспертов. Также сходство сервисов со скриптами и экспертами состоит в том, что наличие зацикленных блоков кода не влияет на работу других mql5-программ.
2.3 Запрет на использование функций в сервисах
Разработчик приводит исчерпывающий перечень функций, которые запрещены для использования:
- ExpertRemove(),
- EventSetMillisecondTimer(),
- EventSetTimer(),
- EventKillTimer(),
- SetIndexBuffer(),
- IndicatorSetDouble(),
- IndicatorSetInteger(),
- IndicatorSetString(),
- PlotIndexSetDouble(),
- PlotIndexSetInteger(),
- PlotIndexSetString(),
- PlotIndexGetInteger().
Вполне логично, что сервисы не могут прекращать работу эксперта и работать с таймером, т.к. обрабатывают только одно событие Start. Также не могут они работать и с функциями пользовательских индикаторов.
2.4 Загрузка и выгрузка сервисов
В этом разделе Документации есть несколько важных моментов. Рассмотрим каждый из них.
Сервисы загружаются сразу после запуска терминала, если в момент остановки терминала они были запущены. Сервисы выгружаются сразу после окончания своей работы.
В этом и состоит одно из замечательных свойств такого вида программы, как сервис. За сервисом не нужно следить – один раз запустил, и он будет автоматически выполнять свои действия.
Сервисы имеют единственный обработчик OnStart(), в котором вы можете организовать бесконечный цикл получения и обработки данных, например - создание и обновление пользовательских символов при помощи сетевых функций.
Можно сделать простой вывод. Если сервис должен выполнять набор разовых действий, то зацикливать какой-то блок кода не нужно. Если же стоит задача по постоянной или регулярной работе сервиса, то необходимо обернуть блок кода в цикл. Позже на примерах рассмотрим подобные задачи.
В отличие от советников, индикаторов и скриптов, сервисы не привязаны к конкретному графику - поэтому для запуска сервиса предусмотрен отдельный механизм.
Пожалуй это второе замечательное свойство сервиса. Для него не существует какого-то графика, без которого невозможна его работа.
Создание нового экземпляра сервиса производится из Навигатора с помощью команды "Добавить сервис". Для запуска, остановки и удаления экземпляра сервиса используйте его меню. Для управления всеми экземплярами, используйте меню самого сервиса.
Это третье замечательное свойство сервиса. Имея всего один файл программы, можно одновременно запустить несколько её экземпляров. Обычно так делают тогда, когда нужно использовать разные параметры (Input переменные).
Вот в целом и всё, что касается информации о сервисах в Документации.
3. Прототип сервиса
В Справке, которая вызывается в терминале при нажатии клавиши F1, описан механизм запуска и управления сервисами. Поэтому на этом сейчас останавливаться не будем.
Создадим в редакторе кода MetaEditor шаблон сервиса и назовём его dEmpty.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { //--- } //+------------------------------------------------------------------+
После компиляции увидим имя сервиса в Навигаторе (Рис.1).
Рис.1 Сервис "dEmpty" в подокне Навигатора
После добавления и запуска сервиса dEmpty в подокне Навигатора получим такие записи в Журнале:
CS 0 19:54:18.590 Services service 'dEmpty' started CS 0 19:54:18.592 Services service 'dEmpty' stopped
Логи показывают, что сервис был запущен и остановлен. Т.к. в его коде нет никаких команд, то никаких изменений в терминале не будет. И поэтому мы ничего не заметим после запуска этого сервиса.
Попробуем заполнить шаблон сервиса какими-нибудь командами. Создадим сервис dStart.mq5 и запишем такие строки:
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
После запуска сервиса на вкладке “Experts” увидим такую запись:
CS 0 20:04:28.347 dStart Service "dStart" starts at: 2022.11.30 20:04:28.
Таким образом, сервис dStart сообщил нам о своём запуске, после чего прекратил свою работу.
Расширим возможности предыдущего сервиса и назовём новый dStartStop.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); ::Sleep(1000); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
Текущий сервис уже сообщает не только о своём старте, но и о своём завершении активности.
После запуска сервиса в журнале увидим такие записи:
2022.12.01 22:49:10.324 dStartStop Service "dStartStop" starts at: 2022.12.01 22:49:10 2022.12.01 22:49:11.336 dStartStop Service "dStartStop" stops at: 2022.12.01 22:49:11
Нетрудно заметить, что первое и второе время отличаются секундой. Просто между первой и последней командами сработала нативная функция Sleep().
Теперь расширим возможности текущего сервиса таким образом, чтобы он работал пока не будет принудительно остановлен. Назовём новый сервис dStandBy.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); do { ::Sleep(1); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- final goodbye for(ushort cnt=0; cnt<5; cnt++) { ::PrintFormat("Count: %hu", cnt+1); ::Sleep(10000); } } //+------------------------------------------------------------------+
После выхода из цикла do while вследствие остановки программы сервис ещё запишет в журнал несколько значений счётчика. После каждой такой записи будет вызываться Sleep() с интервалом задержки в 10 сек.
В журнале появятся такие записи:
CS 0 23:20:44.478 dStandBy Service "dStandBy" starts at: 2022.12.01 23:20:44 CS 0 23:20:51.144 dStandBy Service "dStandBy" stops at: 2022.12.01 23:20:51 CS 0 23:20:51.144 dStandBy Count: 1 CS 0 23:20:51.159 dStandBy Count: 2 CS 0 23:20:51.175 dStandBy Count: 3 CS 0 23:20:51.191 dStandBy Count: 4 CS 0 23:20:51.207 dStandBy Count: 5
Сервис был запущен в 23:20:44 и принудительно остановлен в 23:20:51. Также несложно заметить, что между значениями счётчика интервалы не превышают 0,02 сек. Хотя для таких интервалов ранее задали задержку в 10 сек.
Как следует из Документации к функции Sleep():
Примечание
Функцию Sleep() нельзя вызывать из пользовательских индикаторов, так как индикаторы выполняются в интерфейсном потоке и не должны его тормозить. В функцию встроена проверка состояния флага остановки эксперта каждую 0.1 секунды.
Т.е. в нашем случае функция Sleep() быстро обнаруживала, что сервис принудительно остановлен, и прекращала задерживать выполнение mql5-программы.
Для полноты картины ещё посмотрим, что Документация говорит о возвращаемом значении функции проверки состояния IsStopped():
Возвращаемое значение
Возвращает true, если в системной переменной _StopFlag содержится значение, отличное от 0. Ненулевое значение записывается в переменную _StopFlag, если поступила команда завершить выполнение mql5-программы. В этом случае необходимо как можно быстрее завершить работу программы, в противном случае программа будет завершена принудительно извне через 3 секунды.
Таким образом, после принудительной остановки у сервиса есть 3 секунды на какие-то другие действия прежде чем он будет полностью деактивирован. Проверим этот момент на практике. Добавим в код предыдущего сервиса после цикла матричное вычисление, на которое уходит около минуты. И посмотрим, успеет ли сервис всё подсчитать, после того как он был принудительно остановлен. Новый сервис назовём srvcStandByMatrixMult.mq5.
После цикла подсчёта значений счётчика нужно в предыдущий код дописать такой блок:
//--- Matrix mult //--- matrix A 1000x2000 int rows_a=1000; int cols_a=2000; //--- matrix B 2000x1000 int rows_b=cols_a; int cols_b=1000; //--- matrix C 1000x1000 int rows_c=rows_a; int cols_c=cols_b; //--- matrix A: size=rows_a*cols_a int size_a=rows_a*cols_a; int size_b=rows_b*cols_b; int size_c=rows_c*cols_c; //--- prepare matrix A double matrix_a[]; ::ArrayResize(matrix_a, rows_a*cols_a); for(int i=0; i<rows_a; i++) for(int j=0; j<cols_a; j++) matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767); //--- prepare matrix B double matrix_b[]; ::ArrayResize(matrix_b, rows_b*cols_b); for(int i=0; i<rows_b; i++) for(int j=0; j<cols_b; j++) matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767); //--- CPU: calculate matrix product matrix_a*matrix_b double matrix_c_cpu[]; ulong time_cpu=0; if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu)) { ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError()); return; } ::PrintFormat("time CPU=%d ms", time_cpu);
Запускаем сервис dStandByMatrixMult и принудительно останавливаем через несколько секунд. В журнале появятся такие строки:
CS 0 15:17:23.493 dStandByMatrixMult Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23 CS 0 15:18:17.282 dStandByMatrixMult Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17 CS 0 15:18:17.282 dStandByMatrixMult Count: 1 CS 0 15:18:17.297 dStandByMatrixMult Count: 2 CS 0 15:18:17.313 dStandByMatrixMult Count: 3 CS 0 15:18:17.328 dStandByMatrixMult Count: 4 CS 0 15:18:17.344 dStandByMatrixMult Count: 5 CS 2 15:18:19.771 dStandByMatrixMult Abnormal termination
Видим, что команда на завершение выполнения mql5-программы поступила в 15:18:17.282. А сам сервис был принудительно завершён в 15:18:19.771. И действительно, с момента завершения до принудительного останова сервиса прошло 2,489 сек. О том, что сервис был остановлен принудительно и, причём с аварийным завершением, свидетельствует запись «Abnormal termination».
В связи с тем, что при завершении работы сервиса (_StopFlag == true) до его принудительного останова остаётся не более 3 сек, не рекомендуется выносить какие-то серьёзные расчёты или торговые действия за цикл, который был прерван.
Простой пример. Допустим, что в терминале работает какой-то сервис, в задачу которого входит закрытие всех позиций при закрытии самого терминала. И вот терминал закрывается, сервис пытается ликвидировать все активные позиции. В итоге терминал закрыт, а некоторые позиции остаются открытыми, о чём мы не знаем.
4. Примеры использования
Прежде чем приступить к практическим примерам, предлагаю порассуждать о том, чем могут заниматься сервисы в торговом терминале. С одной стороны, мы можем внедрить в сервис почти любой код (кроме того, который запрещён), а с другой стороны, наверное стоит, что называется, разграничить полномочия и отвести сервисам свою нишу в среде торгового терминала.
Во-первых, сервисы не должны дублировать работу других активных MQL5-программ: советников, индикаторов, скриптов. Допустим, есть советник, который по сигналу выставляет лимитные ордера в конце торговой сессии. И есть сервис, который также выставляет эти лимитные ордера. В итоге может нарушиться система учёта лимитных ордеров в самом советнике. Или, если разные магики, то советник может упустить из виду ордера, выставленные сервисом.
Во-вторых, нужно избегать обратной ситуации – конфликта сервисов c другими MQL5 -программами. Допустим, есть советник, который по сигналу выставляет лимитные ордера в конце торговой сессии. И есть сервис, который контролирует, чтобы в конце торгового дня все позиции были закрыты, а отложенные ордера удалены. Налицо конфликт интересов: советник будет выставлять ордера, а сервис — тут же их удалять. Всё это может вылиться в своего рода DDoS-атаку торгового сервера.
В общем, сервисы должны быть гармонично встроены в работу торгового терминала, не противодействуя mql5-программам, а взаимодействуя с ними для более эффективного использования торговых алгоритмов.
4.1 Очистка логов
Пусть перед сервисом поставлена задача в начале нового торгового дня очищать папку логов (журналов), которые были сформированы одним или несколькими советниками в прошлом (вчера, позавчера и т.д.).
Какие инструменты тут понадобятся? А понадобятся нам файловые операции и определение нового бара. Про класс обнаружения нового бара можно почитать в статье Обработчик события «новый бар».
Теперь давайте разбираться с файловыми операциями. Нативные файловые операции тут не подойдут, т.к. мы столкнёмся с ограничениями файловой "песочницы". Согласно Документации:
Из соображений безопасности в языке MQL5 строго контролируется работа с файлами. Файлы, с которыми проводятся файловые операции средствами языка MQL5, не могут находиться за пределами файловой "песочницы".
Файлы журналов, которые пишут на диск MQL5 -программы, расположены в папке %MQL5\Logs. К счастью мы можем воспользоваться WinAPI, в котором непосредственно есть файловые операции.
WinAPI подключается с помощью такой директивы:
#include <WinAPI\winapi.mqh>
В файловом WinAPI мы будем использовать восемь функций:
- FindFirstFileW(),
- FindNextFileW(),
- CopyFileW(),
- GetFileAttributesW(),
- SetFileAttributesW(),
- DeleteFileW(),
- FindClose(),
- GetLastError().
Первая функция ищет в указанной папке первый файл с заданным именем. Причём в качестве имени можно подставить маску. Так, чтобы найти файлы логов в папке достаточно указать в виде имени такую строку ".log".
Вторая функция продолжает поиск, который был инициирован первой функцией.
Третья функция копирует существующий файл в новый файл.
Четвёртая функция получает атрибуты файловой системы для указанного файла или каталога.
Пятая функция задаёт такие атрибуты.
Шестая функция удаляет файл с заданным именем.
Седьмая функция закрывает дескриптор поиска файлов.
Восьмая функция извлекает значение кода последней ошибки.
Посмотрим на код сервиса dClearTradeLogs.mq5.
//--- include #include <WinAPI\winapi.mqh> #include "Include\CisNewBar.mqh" //--- defines #define ERROR_FILE_NOT_FOUND 0x2 #define ERROR_NO_MORE_FILES 0x12 #define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF #define FILE_ATTRIBUTE_READONLY 0x1 #define FILE_ATTRIBUTE_DIRECTORY 0x10 #define FILE_ATTRIBUTE_ARCHIVE 0x20 //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpDstPath="G:" ; // Destination drive //+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- new bar CisNewBar daily_new_bar; daily_new_bar.SetPeriod(PERIOD_D1); daily_new_bar.SetLastBarTime(1); //--- logs path string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\"; string mask_path=logs_path+"*.log"; //--- destination folder (if to copy files) string new_folder_name=NULL; uint file_attributes=0; if(::StringLen(InpDstPath)>0) { new_folder_name=InpDstPath+"\\Logs"; //--- check whether a folder exists file_attributes=kernel32::GetFileAttributesW(new_folder_name); bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) && ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0); if(!does_folder_exist) { //--- create a folder int create_res=kernel32::CreateDirectoryW(new_folder_name, 0); if(create_res<1) { ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError()); return; } } } //--- main processing loop do { MqlDateTime sToday; ::TimeTradeServer(sToday); sToday.hour=sToday.min=sToday.sec=0; datetime dtToday=::StructToTime(sToday); if(daily_new_bar.isNewBar(dtToday)) { ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE)); string todays_log_file_name=::TimeToString(dtToday, TIME_DATE); int replaced=::StringReplace(todays_log_file_name, ".", ""); if(replaced>0) { todays_log_file_name+=".log"; //--- log files FIND_DATAW find_file_data; ::ZeroMemory(find_file_data); HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data); if(hFind==INVALID_HANDLE) { ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError()); continue; } // List all the files in the directory with some info about them int result=0; uint files_cnt=0; do { string name=""; for(int i=0; i<MAX_PATH; i++) name+=::ShortToString(find_file_data.cFileName[i]); //--- delete any file except today's if(::StringCompare(name, todays_log_file_name)) { string file_name=logs_path+name; //--- if to copy a file before deletion if(::StringLen(new_folder_name)>0) { string new_file_name=new_folder_name+"\\"+name; if(kernel32::CopyFileW(file_name, new_file_name, 0)==0) { ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError()); } //--- set READONLY attribute file_attributes=kernel32::GetFileAttributesW(new_file_name); if(file_attributes!=INVALID_FILE_ATTRIBUTES) if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) { file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY); if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError()); } } int del_ret=kernel32::DeleteFileW(file_name); if(del_ret>0) files_cnt++; } //--- next file ::ZeroMemory(find_file_data); result= kernel32::FindNextFileW(hFind, find_file_data); } while(result!=0); uint kernel32_last_error=kernel32::GetLastError(); if(kernel32_last_error>0) if(kernel32_last_error!=ERROR_NO_MORE_FILES) ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error); ::PrintFormat("Deleted log files: %I32u", files_cnt); int file_close=kernel32::FindClose(hFind); } } ::Sleep(15000); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
Если в input-переменной задан диск, куда будут копироваться файлы, то создадим папку для хранения файлов логов, предварительно проверив существование этой папки.
В главном обрабатывающем цикле сначала проверяем, не появился ли новый день. Затем также в цикле находим и удаляем файлы логов, причём пропуская файл сегодняшнего дня. Если нужно будет скопировать файл, то проверим эту возможность и после копирования установим атрибут "Только чтение" для нового файла.
Задаём в цикле паузу продолжительностью в 15 сек. Наверное это относительно оптимальная частота для определения нового дня.
Итак, до запуска сервиса папка %MQL5\Logs имела такой вид в Проводнике (Рис.2).
Рис.2 Папка Проводника "%MQL5\Logs" до удаления файлов
После запуска сервиса в журнале появятся такие сообщения:
2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59 2022.12.05 23:26:59.960 dClearTradeLogs 2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05 2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6
Нетрудно заметить, что сервис ничего не записал в журнал относительно окончании своей работы. Дело в том, что работа сервиса и не закончилась. Просто он зациклен и исполняется до своего прерывания.
Рис.3 Папка Проводника "%MQL5\Logs" после удаления файлов
Итак, после удаления логов в указанной папке останется только 1 файл (Рис.3). Естественно, что задачу по удалению файлов можно усовершенствовать и сделать её более гибкой. Например, перед удалением файлов можно их скопировать на другой диск, чтобы совсем не потерять нужную информацию. В общем, реализация уже зависит от конкретных требований к алгоритму. В текущем примере файлы были скопированы в такую папку G:\Logs (Рис.4).
Рис.4 Папка Проводника "G:\Logs" после копирования файлов
На этом завершим работу с логами. В следующем примере поручим сервису задачу отображения графиков (чартов).
4.2 Управление графиками
Давайте представим себе, что перед нами стоит следующая задача. Нужно, чтобы в терминале были открыты графики тех символов, по которым ведётся торговля, т.е. где есть позиции.
Правила для открытых графиков очень простые. Если есть открытая позиция по какому-то из символов, тогда открываем график этого символа. Если позиции нет — не будет и графика. Если по одному символу есть несколько позиций, то открыт будет только один график.
И добавим ещё немного красок. Если позиция в прибыли, то цвет фона графика будет светло-голубым, а если в минусе — светло-розовым. Нулевая прибыль использует цвет лаванды.
Итак, для выполнения этой задачи прежде в коде сервиса всего понадобится цикл, в котором будем следить за состоянием позиций и графиков. Цикл получился достаточно большим. Поэтому проанализируем его код поблочно.
Цикл разбит на два блока.
Первый блок — это обработка ситуации, когда позиций нет:
int positions_num=::PositionsTotal(); //--- if there are no positions if(positions_num<1) { // close all the charts CChart temp_chart_obj; temp_chart_obj.FirstChart(); long temp_ch_id=temp_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++) { long ch_id_to_close=temp_ch_id; temp_chart_obj.NextChart(); temp_ch_id=temp_chart_obj.ChartId(); ::ChartClose(ch_id_to_close); } }
В блоке нужно пройтись по открытым графикам, если таковые есть, и просто их закрыть. Здесь и далее для работы со свойствами ценового графика используется класс CChart.
Второй блок более сложный:
//--- if there are some positions else { //--- collect unique position symbols CHashSet<string> pos_symbols_set; for(int pos_idx=0; pos_idx<positions_num; pos_idx++) { string curr_pos_symbol=::PositionGetSymbol(pos_idx); if(!pos_symbols_set.Contains(curr_pos_symbol)) { if(!pos_symbols_set.Add(curr_pos_symbol)) ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol); } } string pos_symbols_arr[]; int unique_pos_symbols_num=pos_symbols_set.Count(); if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num) continue; //--- collect unique chart symbols and close duplicates CHashMap<string, long> ch_symbols_map; CChart map_chart_obj; map_chart_obj.FirstChart(); long map_ch_id=map_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++) { string curr_ch_symbol=map_chart_obj.Symbol(); long ch_id_to_close=0; if(!ch_symbols_map.ContainsKey(curr_ch_symbol)) { if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol); } else { //--- if there's a duplicate ch_id_to_close=map_chart_obj.ChartId(); } //--- move to the next chart map_chart_obj.NextChart(); map_ch_id=map_chart_obj.ChartId(); if(ch_id_to_close>0) { ::ChartClose(ch_id_to_close); } } map_chart_obj.Detach(); //--- looking for a chart if there's a position for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++) { string curr_pos_symbol=pos_symbols_arr[s_pos_idx]; //--- if there's no chart of the symbol if(!ch_symbols_map.ContainsKey(curr_pos_symbol)) if(::SymbolSelect(curr_pos_symbol, true)) { //--- open a chart of the symbol CChart temp_chart_obj; long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1); if(temp_ch_id<1) ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol); else { if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol); temp_chart_obj.Detach(); } } } string ch_symbols_arr[]; long ch_ids_arr[]; int unique_ch_symbols_num=ch_symbols_map.Count(); if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num) continue; //--- looking for a position if there's a chart for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++) { string curr_ch_symbol=ch_symbols_arr[s_ch_idx]; long ch_id_to_close=ch_ids_arr[s_ch_idx]; CChart temp_chart_obj; temp_chart_obj.Attach(ch_id_to_close); //--- if there's no position of the symbol if(!pos_symbols_set.Contains(curr_ch_symbol)) { temp_chart_obj.Close(); } else { CPositionInfo curr_pos_info; //--- calculate a position profit double curr_pos_profit=0.; int pos_num=::PositionsTotal(); for(int pos_idx=0; pos_idx<pos_num; pos_idx++) if(curr_pos_info.SelectByIndex(pos_idx)) { string curr_pos_symbol=curr_pos_info.Symbol(); if(!::StringCompare(curr_ch_symbol, curr_pos_symbol)) curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap(); } //--- apply a color color profit_clr=clrLavender; if(curr_pos_profit>0.) { profit_clr=clrLightSkyBlue; } else if(curr_pos_profit<0.) { profit_clr=clrLightPink; } if(!temp_chart_obj.ColorBackground(profit_clr)) ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol); temp_chart_obj.Redraw(); } temp_chart_obj.Detach(); } //--- tile windows (Alt+R) uchar vk=VK_MENU; uchar scan=0; uint flags[]= {0, KEYEVENTF_KEYUP}; ulong extra_info=0; uchar Key='R'; for(int r_idx=0; r_idx<2; r_idx++) { user32::keybd_event(vk, scan, flags[r_idx], extra_info); ::Sleep(10); user32::keybd_event(Key, scan, flags[r_idx], extra_info); } }
Сначала собираем уникальные значения символов, позиции по которым открыты. Отмечу здесь, что для этой задачи подходят возможности класса CHashSet<T>, который является реализацией неупорядоченного динамического множества данных типа T, с соблюдением требования уникальности каждого значения. Полученные уникальные значения копируем в строковой массив, чтобы затем иметь к ним упрощённый доступ.
На следующем этапе собираем уникальные значения символов, чарты по которым открыты. И ещё попутно закроем дубликаты графиков, если они есть. Допустим, открыто 2 чарта по EURUSD. Тогда оставим только 1 чарт, а другой закроем. Тут уже задействован экземпляр класса CHashMap<TKey,TValue>, который является реализацией динамической хэш-таблицы, данные которой хранятся в виде неупорядоченных пар "ключ — значение" с соблюдением требования уникальности ключа.
Осталось теперь отработать 2 цикла. В первом пройдёмся по массиву символов открытых позиций и проверим, существует ли для неё свой график. Если не существует, то откроем. Во втором цикле пройдёмся уже по массиву символов открытых чартов и проверим, соответствует ли ему каждому символу открытая позиция. Допустим, что открыт чарт по символу USDJPY, а позиции по нему нет. Тогда график по USDJPY закрывается. B этом же цикле будем подсчитывать прибыль позиции для того, чтобы задать цвет фона, как было определено в начале задачи. Для обращения к свойствам позиции и получения их значений использовался класс Стандартной библиотеки CPositionInfo.
Ну и в конце блока наведём красоту - расположим окна чартов мозаикой. Для этого обратимся к WinAPI, а именно к функции keybd_event(), которая имитирует нажатие клавиши.
Вот и всё. Осталось только запустить сервис dActivePositionsCharts.
4.3 Пользовательский символ, котировки
Одним из преимуществ сервиса является возможность его работы в фоне, без использования ценового графика. В качестве примера в этом разделе покажем, как с помощью сервиса можно создавать пользовательский символ и его тиковую историю, генерировать новые тики.
В роли пользовательского символа будет выступать Индекс доллара США.
4.3.1 Индекс доллара, состав
Индекс доллара США — это синтетик, отражающий стоимость USD относительно корзины из шести других валют:
- евро (57,6%);
- японская иена (13,6%);
- британский фунт (11,9%);
- канадский доллар (9,1%);
- шведская крона (4,2%);
- швейцарский франк (3,6%).
Формула, по которой рассчитывается Индекс, представляет из себя с поправочным коэффициентом среднее геометрическое взвешенное курсов доллара к этим валютам:
USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036
Исходя из формулы, скажем, что котировка пары возводится в отрицательную степень тогда, когда доллар в котировке — это котируемая валюта, и что котировка пары возводится в положительную степень тогда, когда доллар в котировке - это базовая валюта.
Корзину валют схематично можно отобразить так (Рис.5).
Рис.5 Корзина валют Индекса доллара (DXY)
Индекс доллара США - базовый актив для фьючерсов, которые торгуются на бирже Intercontinental Exchange (ICE). Фьючерс на индекс рассчитывается примерно каждые 15 сек. Цены для расчёта берутся по самой высокой цене bid и самой низкой цене ask в стакане валютной пары, входящей в состав индекса.
4.3.2 Индекс доллара, сервис
Всё, что нужно для расчётов у нас есть, можно приступить к написанию кода сервиса. Но прежде отмечу, что сервис будет работать поэтапно. На первом этапе он сформирует историю тиков и баров для синтетика, а на втором - обрабатывать новые тики. Очевидно, что первый этап связан с прошлым, а второй — с настоящим.
Создадим шаблон MQL5-программы (сервиса) под именем dDxySymbol.mq5.
В качестве input-переменных определим следующие:
input datetime InpStartDay=D'01.10.2022'; // Start date input int InpDxyDigits=3; // Index digits
Первая определяет начало истории котировок, которую мы будем пытаться получить для создания своего символа. Т.е. подкачивать историю котировок будем с 1 октября 2022 г.
Вторая задаёт точность котировки символа.
Итак, чтобы начать работу с индексом, нужно создать пользовательский символ — основа для отображения синтетика. DXY — это имя для символа индекса. На ресурсе есть много материала, посвящённого пользовательским символам. Я обращусь к классу CiCustomSymbol, который был определён в статье Рецепты MQL5 – Стресс-тестирование торговой стратегии с помощью пользовательских символов.
Вот тот блок кода, где идёт работа по созданию синтетика DXY:
//--- create a custom symbol string custom_symbol="DXY", custom_group="Dollar Index"; CiCustomSymbol custom_symbol_obj; const uint batch_size = 1e6; const bool is_selected = true; int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected); ::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code); if(code < 0) return;
Отмечу, что если символ DXY ранее не был создан и отсутствует в списке пользовательских символов терминала, то метод CiCustomSymbol::Create() вернёт код 1. Если символ DXY уже есть среди символов, то получим код 0. Если же не удастся создать символ, то получим ошибку — код -1. В случае ошибки при создании пользовательского символа сервис прекратит работу.
После создания синтетика зададим для него несколько свойств.
//--- Integer properties //--- sector ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY; if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector)) { ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol); return; } //--- background color color symbol_background_clr = clrKhaki; if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr)) { ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol); return; } //--- chart mode ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID; if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode)) { ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol); return; } //--- digits if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits)) { ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol); return; } //--- trade mode ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode)) { ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol); return; }
К свойствам типа ENUM_SYMBOL_INFO_INTEGER относятся такие:
- SYMBOL_SECTOR,
- SYMBOL_BACKGROUND_COLOR,
- SYMBOL_CHART_MODE,
- SYMBOL_DIGITS,
- SYMBOL_TRADE_MODE.
Последнее свойство отвечает за торговый режим. Синтетик будет отключен от торговли, поэтому свойство получит значение SYMBOL_TRADE_MODE_DISABLED. Если же понадобится проверить какую-нибудь стратегию по символу в Тестере, то свойство нужно будет включить (SYMBOL_TRADE_MODE_FULL).
//--- Double properties //--- point double symbol_point = 1./::MathPow(10, InpDxyDigits); if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point)) { ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol); return; } //--- tick size double symbol_tick_size = symbol_point; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size)) { ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol); return; }
К свойствам типа ENUM_SYMBOL_INFO_DOUBLE относятся следующие:
- SYMBOL_POINT,
- SYMBOL_TRADE_TICK_SIZE.
//--- String properties //--- category string symbol_category="Currency indices"; if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category)) { ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol); return; } //--- country string symbol_country= "US"; if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country)) { ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol); return; } //--- description string symbol_description= "Synthetic US Dollar Index"; if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description)) { ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol); return; } //--- exchange string symbol_exchange= "ICE"; if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange)) { ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol); return; } //--- page string symbol_page = "https://www.ice.com/forex/usdx"; if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page)) { ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol); return; } //--- path string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol; if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path)) { ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol); return; }
К свойствам типа ENUM_SYMBOL_INFO_STRING относятся такие:
- SYMBOL_CATEGORY,
- SYMBOL_COUNTRY,
- SYMBOL_DESCRIPTION,
- SYMBOL_EXCHANGE,
- SYMBOL_PAGE,
- SYMBOL_PATH.
Последнее свойство отвечает за путь в дереве символов. Ещё при создании синтетика была указана группа символов и имя символа. Поэтому данное свойство можно и не задавать — оно будет идентичным.
Конечно, ещё можно было задать формулу для синтетика напрямую и не мучаться со сбором тиков. Но тогда утерян был бы смысл примера с одной стороны, а с другой - цена на индекс рассчитывается периодически. В текущем примере период подсчёта равен 10 сек.
Теперь переходим к следующему блоку – это проверка наличия торговой истории. Здесь будем решать 2 задачи: проверять историю баров и подгружать тики. Бары проверим следующим образом:
//--- check quotes history CBaseSymbol base_symbols[BASE_SYMBOLS_NUM]; const string symbol_names[]= { "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF" }; ENUM_TIMEFRAMES curr_tf=PERIOD_M1; ::Print("\nChecking of quotes history is running..."); for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay)) { ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); int check_load_code=ptr_base_symbol.CheckLoadHistory(); ::PrintFormat(" Checking code: %I32d", check_load_code); ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); if(check_load_code<0) { ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name); return; } } }
У нас будет 6 символов, по которым нужно пройтись в цикле и обработать их котировки. Для удобства при такой работе был создан класс CBaseSymbol.
//+------------------------------------------------------------------+ //| Class CBaseSymbol | //+------------------------------------------------------------------+ class CBaseSymbol : public CObject { //--- === Data members === --- private: CSymbolInfo m_symbol; ENUM_TIMEFRAMES m_tf; matrix m_ticks_mx; datetime m_start_date; ulong m_last_idx; //--- === Methods === --- public: //--- constructor/destructor void CBaseSymbol(void); void ~CBaseSymbol(void) {}; //--- bool Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date); int CheckLoadHistory(void); bool LoadTicks(const datetime _stop_date, const uint _flags); matrix GetTicks(void) const { return m_ticks_mx; }; bool SearchTickLessOrEqual(const double _dbl_time, vector &_res_row); bool CopyLastTick(vector &_res_row); };
Класс занимается историей баров и тиков, что является крайне важной задачей, иначе не будет материала для создания синтетика.
Затем подгрузим тики:
//--- try to load ticks ::Print("\nLoading of ticks is running..."); now=::TimeCurrent(); uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK; double first_tick_dbl_time=0.; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); ::ResetLastError(); if(!ptr_base_symbol.LoadTicks(now, flags)) { ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError()); return; } ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); //--- looking for the 1st tick matrix ticks_mx=ptr_base_symbol.GetTicks(); double tick_dbl_time=ticks_mx[0][0]; if(tick_dbl_time>first_tick_dbl_time) first_tick_dbl_time=tick_dbl_time; }
Для загрузки тиков использовалась нативная функция matrix::CopyTicksRange(). Удобство состоит в том, что можно загружать только те столбцы в структуре тика, которые определены флагами. Ну и вопрос экономии ресурсов крайне актуален, когда запрашиваем миллионы тиков.
COPY_TICKS_INFO = 1, // тики, вызванные изменениями Bid и/или Ask COPY_TICKS_TRADE = 2, // тики, вызванные изменениями Last и Volume COPY_TICKS_ALL = 3, // все тики, в которых есть изменения COPY_TICKS_TIME_MS = 1<<8, // время в миллисекундах COPY_TICKS_BID = 1<<9, // цена Bid COPY_TICKS_ASK = 1<<10, // цена Ask COPY_TICKS_LAST = 1<<11, // цена Last COPY_TICKS_VOLUME = 1<<12, // объем COPY_TICKS_FLAGS = 1<<13, // флаги тика
Этапы проверки истории и загрузки тиков в журнале будут описаны относительно временных затрат.
CS 0 12:01:11.802 dDxySymbol Checking of quotes history is running... CS 0 12:01:11.802 dDxySymbol CS 0 12:01:11.802 dDxySymbol Symbol #1: "EURUSD" CS 0 12:01:14.476 dDxySymbol Checking code: 1 CS 0 12:01:14.476 dDxySymbol Time elapsed: 2.688 sec CS 0 12:01:14.476 dDxySymbol CS 0 12:01:14.476 dDxySymbol Symbol #2: "USDJPY" CS 0 12:01:17.148 dDxySymbol Checking code: 1 CS 0 12:01:17.148 dDxySymbol Time elapsed: 2.672 sec CS 0 12:01:17.148 dDxySymbol CS 0 12:01:17.148 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:01:19.068 dDxySymbol Checking code: 1 CS 0 12:01:19.068 dDxySymbol Time elapsed: 1.922 sec CS 0 12:01:19.068 dDxySymbol CS 0 12:01:19.068 dDxySymbol Symbol #4: "USDCAD" CS 0 12:01:21.209 dDxySymbol Checking code: 1 CS 0 12:01:21.209 dDxySymbol Time elapsed: 2.140 sec CS 0 12:01:21.209 dDxySymbol CS 0 12:01:21.209 dDxySymbol Symbol #5: "USDSEK" CS 0 12:01:22.631 dDxySymbol Checking code: 1 CS 0 12:01:22.631 dDxySymbol Time elapsed: 1.422 sec CS 0 12:01:22.631 dDxySymbol CS 0 12:01:22.631 dDxySymbol Symbol #6: "USDCHF" CS 0 12:01:24.162 dDxySymbol Checking code: 1 CS 0 12:01:24.162 dDxySymbol Time elapsed: 1.531 sec CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Loading of ticks is running... CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Symbol #1: "EURUSD" CS 0 12:02:27.204 dDxySymbol Time elapsed: 63.032 sec CS 0 12:02:27.492 dDxySymbol CS 0 12:02:27.492 dDxySymbol Symbol #2: "USDJPY" CS 0 12:02:32.587 dDxySymbol Time elapsed: 5.094 sec CS 0 12:02:32.938 dDxySymbol CS 0 12:02:32.938 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:02:37.675 dDxySymbol Time elapsed: 4.734 sec CS 0 12:02:38.285 dDxySymbol CS 0 12:02:38.285 dDxySymbol Symbol #4: "USDCAD" CS 0 12:02:43.223 dDxySymbol Time elapsed: 4.937 sec CS 0 12:02:43.624 dDxySymbol CS 0 12:02:43.624 dDxySymbol Symbol #5: "USDSEK" CS 0 12:03:18.484 dDxySymbol Time elapsed: 34.860 sec CS 0 12:03:19.596 dDxySymbol CS 0 12:03:19.596 dDxySymbol Symbol #6: "USDCHF" CS 0 12:03:24.317 dDxySymbol Time elapsed: 4.719 sec
После того, как тики получены, сформируем тиковую историю для синтетика DXY. Происходить этот процесс будет в таком блоке:
//--- create a custom symbol ticks history ::Print("\nCustom symbol ticks history is being formed..."); long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC); long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC; ::PrintFormat(" First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec, TIME_DATE|TIME_SECONDS), first_tick_time_ms); double active_tick_dbl_time=first_tick_dbl_time; double now_dbl_time=MS_IN_SEC*now; uint ticks_cnt=0; uint arr_size=0.5e8; MqlTick ticks_arr[]; ::ArrayResize(ticks_arr, arr_size); ::ZeroMemory(ticks_arr); matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2); do { //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick; CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick); arr_size=ticks_arr.Size(); if(ticks_cnt>=arr_size) { uint new_size=(uint)(arr_size+0.1*arr_size); if(::ArrayResize(ticks_arr, new_size)!=new_size) continue; } ticks_arr[ticks_cnt]=last_ind_tick; ticks_cnt++; } active_tick_dbl_time+=TICK_PAUSE; } while(active_tick_dbl_time<now_dbl_time); ::ArrayResize(ticks_arr, ticks_cnt); int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);
Задаём временную точку (active_tick_dbl_time), к которой в конце цикла будем прибавлять 10 сек. Это своего рода timestamp (отметка времени) для получения тиков по всем символам, составляющим Индекс.
Итак, поиск нужного тика на каждом символе идёт исходя из конкретного момента времени в прошлом. Метод CBaseSymbol::SearchTickLessOrEqual() выдаёт тик, которые пришёл не позднее значения active_tick_dbl_time.
Когда тики с каждого компонента Индекса получены, то цены тиков уже находятся в матрице base_prices_mx.
Функция CalcIndexPrices() возвращает уже готовое значение индексного тика в момент времени.
Когда тики созданы, то тиковая база данных обновляется с помощью метода CiCustomSymbol::TicksReplace().
На этом работа с прошлым закончена. Затем сервис занимается только настоящим в следующем блоке:
//--- main processing loop ::Print("\nA new tick processing is active..."); do { ::ZeroMemory(base_prices_mx); //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.CopyLastTick(tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick, ticks_to_add[1]; now=::TimeCurrent(); now_dbl_time=MS_IN_SEC*now; CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick); ticks_to_add[0]=last_ind_tick; int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true); } ::Sleep(TICK_PAUSE); } while(!::IsStopped());
Задача блока аналогична той, что была у предыдущего блока. Только немного попроще. Каждые 10 сек нужно получить тиковые данные по символам и подсчитать цены индекса. Замечу, что в Индексе цена bid считается по бидам всех символов, ask - соответственно по всем аскам.
После того, как сервис dDxySymbol запущен, спустя некоторое времени можно открыть график пользовательского символа DXY (Рис.6).
Рис.6 График пользовательского символа DXY с выходными днями
На графике выделены красными вертикальными отрезками субботы. Оказывается, что на истории по субботам и воскресеньям сервис продолжает расчёт тиков, что наверное не совсем правильно. Нужно дополнить код сервиса ограничением по времени (дням недели). Возложим данную задачу на функцию CheckDayOfWeek().
Теперь график синтетика выглядит так (Рис.7). Кажется, что недочёт устранили.
Рис.7 График пользовательского символа DXY без выходных дней
На этом завершим работу с сервисом dDxySymbol.
Заключение
В статье были представлены некоторые возможности такой mql5-программы, как сервис. Этот тип mql5-программ отличается тем, что не имеет чарта привязки, а работает независимо. Подчеркну, что природа сервисов такова, что они могут вступить в конфликт с другими советниками, скриптами и наверное в меньшей степени с индикаторами. Поэтому на плечи разработчика ложится задача по разграничению прав и обязанностей сервисных программ в среде MetaTrader5.
В архиве находятся исходники, которые можно разместить в папке %MQL5\Services.




- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Алексей, спасибо за мнение!
Хороший вопрос. Тут нужно взвесить все за и против. С одной стороны, как заметил коллега Alexey Viktorov, сервис для такой задачи нужно запускать в бесконечном цикле. Но с другой, сервис работает в фоне и сам пишет\читает из базы данных (БД). Если же в каждый робот добавлять возможность работы с БД, то необходимо понимать, что может быть конфликт синхронизации. Тут наверное поможет что-то вроде мьютекса.
Ну и советники могут моментально обрабатывать торговые события, а сервис - нет...
А разве SQLite не умеет работать с очередью транзакций? Я с этим вопросом не разбирался, но Вы писали же статью, поэтому и спрашиваю :)
Писать можно в каждый раздел базы (отдельную таблицу), или даже каждый советник может создать свою базу, а сервис будет проверять наличие базы и подключать её для работы.
Вопрос конечно в задержках, но с другой стороны компенсируется сэкономленным спредом.
У меня свои виртуальные позиции каждый робот учитывает сам. О реальных, в принципе, и не знает. Единственное, тут проблема для MM с определением лота, но у меня фиксированный лот задаётся через параметры. Альтернативу фиксированному вижу только в выделении каждому роботу какого-то процента от депо.
Хочется универсальности, что бы набирать корзины советников с разной логикой. Если советник в собственном соку вариться, то да - там чуть проще - сам делал, правда экспериментально - без сохранения данных в файл/БД.
А разве SQLite не умеет работать с очередью транзакций? Я с этим вопросом не разбирался, но Вы писали же статью, поэтому и спрашиваю :)
Писать можно в каждый раздел базы (отдельную таблицу), или даже каждый советник может создать свою базу, а сервис будет проверять наличие базы и подключать её для работы.
Вопрос конечно в задержках, но с другой стороны компенсируется сэкономленным спредом.
Умеет. Но в SQLite ограничена многопоточность — единовременное выполнение нескольких процессов. Одновременно читать из базы могут несколько процессов, а писать в нее по умолчанию — только один...
А сервисам, КМК, очень не хватает обработки событий - OnTimer, OnTrade, OnTradeTransaction. OnDeinit бы тоже не помешал для штатного останова при закрытии терминала.
И тогда сервис ничем, почти, не будет отличаться от советника. Мне больше интересны события перехода на другой график, смена периода графика… Ну может ещё чего-то из того чего нет ни в советниках, ни в индикаторах…
И тогда сервис ничем, почти, не будет отличаться от советника. Мне больше интересны события перехода на другой график, смена периода графика… Ну может ещё чего-то из того чего нет ни в советниках, ни в индикаторах…
Ключевое - не требует отдельного графика.