English 中文 Español Deutsch 日本語 Português
preview
Рецепты MQL5 — Сервисы

Рецепты MQL5 — Сервисы

MetaTrader 5Примеры | 14 декабря 2022, 14:02
1 659 13
Denis Kirichenko
Denis Kirichenko

Введение

Относительно недавно в торговом терминале MetaTrader 5 появился такой тип программы, как сервис. Как заявляет разработчик, сервисы позволяют создавать собственные источники ценовых данных для терминала — передавать цены от внешних систем в режиме реального времени так, как это делают торговые серверы брокеров. И это далеко не единственная возможность сервисов.

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



1. Демоны на службе

Наверное не секрет, что сервисы в MQL5 имеют сходство со службами Windows. Википедия даёт такое определение службы:

Слу́жба Windows (англ. Windows Service) — приложение, автоматически (если настроено) исполняемое системой при запуске операционной системы Windows и выполняющиеся вне зависимости от статуса пользователя. Имеет общие черты с концепцией демонов в Unix.

В нашем случае внешней средой для сервисов выступает не сама операционная система, а оболочка терминала 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 Запрет на использование функций в сервисах

Разработчик приводит исчерпывающий перечень функций, которые запрещены для использования:

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

 
2.4 Загрузка и выгрузка сервисов

В этом разделе Документации есть несколько важных моментов. Рассмотрим каждый из них.

Сервисы загружаются сразу после запуска терминала, если в момент остановки терминала они были запущены. Сервисы выгружаются сразу после окончания своей работы.

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

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

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

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

 Пожалуй это второе замечательное свойство сервиса. Для него не существует какого-то графика, без которого невозможна его работа.

Создание нового экземпляра сервиса производится из Навигатора с помощью команды "Добавить сервис". Для запуска, остановки и удаления экземпляра сервиса используйте его меню. Для управления всеми экземплярами, используйте меню самого сервиса.

Это третье замечательное свойство сервиса. Имея всего один файл программы, можно одновременно запустить несколько её экземпляров. Обычно так делают тогда, когда нужно использовать разные параметры (Input переменные).

Вот в целом и всё, что касается информации о сервисах в Документации.


3. Прототип сервиса

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

Создадим в редакторе кода MetaEditor шаблон сервиса и назовём его dEmpty.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
  }
//+------------------------------------------------------------------+

После компиляции увидим имя сервиса в Навигаторе (Рис.1).


Сервис "dEmpty"

Рис.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 мы будем использовать восемь функций:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. 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).

Папка Проводника "%MQL5\Logs" до удаления файлов

Рис.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

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

Папка Проводника "%MQL5\Logs" после удаления файлов

Рис.3 Папка Проводника "%MQL5\Logs" после удаления файлов

Итак, после удаления логов в указанной папке останется только 1 файл (Рис.3). Естественно, что задачу по удалению файлов можно усовершенствовать и сделать её более гибкой. Например, перед удалением файлов можно их скопировать на другой диск, чтобы совсем не потерять нужную информацию. В общем, реализация уже зависит от конкретных требований к алгоритму. В текущем примере файлы были скопированы в такую папку G:\Logs (Рис.4).

Папка Проводника "G:\Logs" после копирования файлов

Рис.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 относительно корзины из шести других валют:

  1. евро (57,6%);
  2. японская иена (13,6%);
  3. британский фунт (11,9%);
  4. канадский доллар (9,1%);
  5. шведская крона (4,2%);
  6. швейцарский франк (3,6%).

Формула, по которой рассчитывается Индекс, представляет из себя с поправочным коэффициентом среднее геометрическое взвешенное курсов доллара к этим валютам:

USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036

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

Корзину валют схематично можно отобразить так (Рис.5).



Корзина валют Индекса доллара (DXY)

Рис.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.
Так как ранее определились, что символ будет неторговым, то поэтому double-свойств немного.

//--- 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 с выходными днями

Рис.6 График пользовательского символа DXY с выходными днями 


На графике выделены красными вертикальными отрезками субботы. Оказывается, что на истории по субботам и воскресеньям сервис продолжает расчёт тиков, что наверное не совсем правильно. Нужно дополнить код сервиса ограничением по времени (дням недели). Возложим данную задачу на функцию CheckDayOfWeek().

Теперь график синтетика выглядит так (Рис.7). Кажется, что недочёт устранили.

График пользовательского символа DXY без выходных дней

Рис.7 График пользовательского символа DXY без выходных дней 

На этом завершим работу с сервисом dDxySymbol.


Заключение

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

В архиве находятся исходники, которые можно разместить в папке %MQL5\Services.
Прикрепленные файлы |
code.zip (23.39 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (13)
Aleksey Vyazmikin
Aleksey Vyazmikin | 23 апр. 2023 в 10:41
Denis Kirichenko #:

Алексей, спасибо за мнение!

Хороший вопрос. Тут нужно взвесить все за и против. С одной стороны, как заметил коллега Alexey Viktorov, сервис для такой задачи нужно запускать в бесконечном цикле. Но с другой, сервис работает в фоне и сам пишет\читает из базы данных (БД). Если же в каждый робот добавлять возможность работы с БД, то необходимо понимать, что может быть конфликт синхронизации. Тут наверное поможет что-то вроде мьютекса.

Ну и советники могут моментально обрабатывать торговые события, а сервис - нет...

А разве SQLite не умеет работать с очередью транзакций? Я с этим вопросом не разбирался, но Вы писали же статью, поэтому и спрашиваю :)

Писать можно в каждый раздел базы (отдельную таблицу), или даже каждый советник может создать свою базу, а сервис будет проверять наличие базы и подключать её для работы.

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

Aleksey Vyazmikin
Aleksey Vyazmikin | 23 апр. 2023 в 10:44
JRandomTrader #:

У меня свои виртуальные позиции каждый робот учитывает сам. О реальных, в принципе, и не знает. Единственное, тут проблема для MM с определением лота, но у меня фиксированный лот задаётся через параметры. Альтернативу фиксированному вижу только в выделении каждому роботу какого-то процента от депо.

Хочется универсальности, что бы набирать корзины советников с разной логикой. Если советник в собственном соку вариться, то да - там чуть проще - сам делал, правда экспериментально - без сохранения данных в файл/БД.

Denis Kirichenko
Denis Kirichenko | 23 апр. 2023 в 11:14
Aleksey Vyazmikin #:

А разве SQLite не умеет работать с очередью транзакций? Я с этим вопросом не разбирался, но Вы писали же статью, поэтому и спрашиваю :)

Писать можно в каждый раздел базы (отдельную таблицу), или даже каждый советник может создать свою базу, а сервис будет проверять наличие базы и подключать её для работы.

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

Умеет.  Но в SQLite ограничена многопоточность — единовременное выполнение нескольких процессов. Одновременно читать из базы могут несколько процессов, а писать в нее по умолчанию — только один...

Alexey Viktorov
Alexey Viktorov | 23 апр. 2023 в 11:51
JRandomTrader #:
А сервисам, КМК, очень не хватает обработки событий - OnTimer, OnTrade, OnTradeTransaction. OnDeinit бы тоже не помешал для штатного останова при закрытии терминала.

И тогда сервис ничем, почти, не будет отличаться от советника. Мне больше интересны события перехода на другой график, смена периода графика… Ну может ещё чего-то из того чего нет ни в советниках, ни в индикаторах…

JRandomTrader
JRandomTrader | 23 апр. 2023 в 21:34
Alexey Viktorov #:

И тогда сервис ничем, почти, не будет отличаться от советника. Мне больше интересны события перехода на другой график, смена периода графика… Ну может ещё чего-то из того чего нет ни в советниках, ни в индикаторах…

Ключевое - не требует отдельного графика.

Популяционные алгоритмы оптимизации: Поиск косяком рыб (Fish School Search — FSS) Популяционные алгоритмы оптимизации: Поиск косяком рыб (Fish School Search — FSS)
Поиск косяком рыб (FSS) — новый современный алгоритм оптимизации, вдохновленный поведением рыб в стае, большинство из которых, до 80%, плавают организовано в сообществе сородичей. Доказано, что объединения рыб играют важную роль в эффективности поиска пропитания и защиты от хищников.
DoEasy. Элементы управления (Часть 29): Вспомогательный элемент управления "ScrollBar" DoEasy. Элементы управления (Часть 29): Вспомогательный элемент управления "ScrollBar"
В статье начнём разработку элемента вспомогательного управления ScrollBar и его производных объектов — вертикальной и горизонтальной полос прокрутки. ScrollBar (полоса прокрутки) используется для прокручивания содержимого формы, если оно выходит за пределы контейнера. Полосы прокрутки обычно расположены снизу и справа формы. Горизонтальная, расположенная снизу, служит для прокрутки содержимого влево-вправо, а вертикальная — для прокрутки вверх-вниз.
Разработка торгового советника с нуля (Часть 31): Навстречу будущему (IV) Разработка торгового советника с нуля (Часть 31): Навстречу будущему (IV)
Мы продолжаем удалять разные вещи внутри советника. Это будет последняя статья в этой серии. Последнее, что будет удалено в данной серии статей - это звуковая система. Это может сбить читателя с толку, если он не следил за этими статьями.
Популяционные алгоритмы оптимизации: Алгоритм оптимизации с кукушкой (Cuckoo Optimization Algorithm — COA) Популяционные алгоритмы оптимизации: Алгоритм оптимизации с кукушкой (Cuckoo Optimization Algorithm — COA)
Следующий алгоритм, который рассмотрим — оптимизация поиском кукушки с использованием полётов Леви. Это один из новейших алгоритмов оптимизации и новый лидер в рейтинговой таблице.