MTWorkers - параллельное исполнение команд

MTWorkers - параллельное исполнение команд

26 апреля 2024, 18:26
Maxim Kuznetsov
0
49

Рад представить новую штуку - сервис и библиотеки MTWorkers, MTСlient для параллельного исполнения команд. Одновременно в разных потоках могут исполняться команды терминала, системные задачи, сетевые запросы. И конечно скрипты. Можно распараллеливать свои алгоритмы и поручать длительные функции параллельным нитям, освобождая драгоценное время советников. 

Часть 1 : низкий уровень

Будет ещё и вторая часть, посвященная классу MTClient. Решил начать публикации с более низкого уровня, чтобы ознакомить с внутренним устройством. Чтобы при необходимости вы могли например сделать свой клиент, не полагаясь на мою ОО-реализацию.

Первый тестово-демонстрационный пример, спрашиваем у воркеров список чартов

Что тут произошло ?

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

тестовый скрипт максимально простой, и по нему понятно как всё устроено:

void OnStart()
{
   // берём ATcl
   ATcl_OnInit();
   ATcl *tcl=new ATcl();
   // пакет MTClient
   if (tcl.Eval("package require MTClient")!=TCL_OK) {
      Alert("package require : %s",tcl.StringResult());
      delete tcl;
      return;
   }
   // запускаем клиент
   if (tcl.Eval("set mtc [ MTClient new ]")!=TCL_OK) {
      Alert("MTClient new : %s",tcl.StringResult());
   }
   // запрашиваем список чартов
   Tcl_Obj list=tcl.ObjEval("$mtc invoke charts");
   tcl.Ref(list);
   int total=tcl.Count(list);
   PrintFormat("Всего %d чартов",total);
   for(int i=0;i<total;i++) {
      PrintFormat("#%d %s",i,tcl.String(list,i));
   }
   tcl.Unref(list);
   // удаляем клиент
   tcl.Eval("$mtc destroy");
   delete tcl;
}

по скрипту видно - мы просто накидывали команды tcl, получали и разбирали результат. То-же самое могли повторить "руками" из консоли TkCon. Внутренние механизмы реализованы на tcl. На уровне MQL просто обращения к интерпретатору. 

Как сайд-эффект такого решения - из скриптов можно обращаться к воркерам и общаться с терминалом. Можно писать алгоритмы или расширения программ на скриптах.

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

// функция charts ?-dict|-json?
// опциональный -dict : данные будут возвращены в виде словаря
//              -json : данные будут в виде json
// @arg worker   инстанс воркера
// @arg timeout  таймаут (дедлайн) в милисек на исполнение
// @arg args     список параметров переданный юзером (args[0] - имя команды)
// @arg raise [inout] - признак ошибки. Функция должна выставить значение при ошибках
// @return Tcl_Obj результат исполнения
Tcl_Obj MTWorker_charts(MTWorker &worker,ulong timeout,Tcl_Obj args,int &raise) {
   ATcl *tcl=worker.tcl;   
   tcl.Ref(args);
   int format=0;           // формат
   if ( tcl.String(args,1)=="-dict") {
      format=1;
   } else if ( tcl.String(args,1)=="-json") {
      format=2;
   }
   Tcl_Obj list=tcl.Obj();
   for(long chart=ChartFirst();chart!=-1;chart=ChartNext(chart)) {
      ChartData data(chart);
      Tcl_Obj record=data.ToObj(tcl,format);
      tcl.ListAppend(list,record);
   }
   tcl.Unref(args);
   return list;   
}
/// --- some code skipped ---
MTWorker *serv=NULL;
void OnStart()
{
   serv=new MTWorker();
   if (serv==NULL) return;
   if (serv.OnInit()!=INIT_SUCCEEDED) {
      serv.OnDeinit(REASON_INITFAILED);
      delete serv;
      return;
   }
   serv.Extend("charts",MTWorker_charts); // регестрируем команду charts и функцию которая её исполнит
   serv.Extend("orders",MTWorker_orders);
   serv.Run();
   serv.OnDeinit(REASON_PROGRAM);
   delete serv;
   serv=NULL;
}

тоже всё максимально просто - пишется функция которая реализует команду и регестрируется в воркере.

Основной принцип - используем Tcl_Obj как универсальный контейнер. Он может нести практически всё что угодно - текст/список/ассоц.массив/бинарные данные. 

Аргументы команды получили в виде Tcl_Obj, разобрали его как список. И результирующие данные тоже собрали в Tcl_Obj и отдали.

Второй пример: async/await и замеряем время


Так как непосредственное исполнение команд мы теперь возложили на воркеров, то между запросом и получением ответа можем заниматься другими полезными и срочными делами.
Главное чтобы запрос уходил быстро. Вот тест и показывает что на запрос уходит 5-10 микросек. 

В положении когда советник для обработки тика должен тратить не более 200 милисек (тики приходят до 5шт./сек), советник может в начале OnTick() быстро оправить запрос,
провести торговые операции и уже под конец забрать результат и поработать с ним.  

С точки зрения прикладного программиста, исполнение разбито на 2 части : 

  • async - отправляет асинхронный/параллельный запрос воркерам, получает некий идентификатор (promise)
  • await - получает непосредственный результат по указанному promise

между двумя этими вызовами можно делать более актуальные дела.

конечно это имеет накладные расходы: потратится время на отправку запроса (несколько микросек), на доставку до воркера, на отработку в воркере и на доставку ответа.
То есть не все функции имеет смысл выносить в паралелель. Но точно просятся:

  • функции имеющие неопределённое время исполнения. Которые могут исполнится быстро, а могут и долго, и заранее непредсказуемо как. Например практически вся работа с объектами чарта. Их категорически нельзя использовать внутри OnTick() и даже OnTimer плохое место - и куда-то надо их деть. 
  • групповые и "интеллектуальные" торговые операции. Например закрытие сетки ордеров. Или "открыть позицию, рассчитав объём от риска как %маржи и за время не более T и цене не хуже P"
  • вызов внешних программ 
  • веб-запросы или запросы к внешним базам.
  • запуск и исполнение больших рассчётов. Все любимые некоторыми нейро-сети и маш.обуч плохо укладываются в торговые лимиты времени

Третий пример: запуск внешних программ



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

В тестовом примере - вызвали dir, как результат получили 12 строчек, вывели их в журнал и в комментарий.

Особенность MTWorkers - для исполнения внешних программ используется отдельный пул процессов. То есть системная команда не занимает воркер. Всего одновременно может работать 5 (сейчас константа, а в дальнейшем можно будет настраивать). Остальные будут дожидаться места в пуле исполнения.

и ещё веб-запросы конечно


Совсем кратко - и точно так-же конечно можно кидать веб-запросы. Их обработка тоже ведётся в отдельном пуле и не занимает воркер.
Воркеры вообще только для исполнения задач MQL, где требуется взаимодействие с терминалом.

В примере - обращение к главной странице сайта и выбор оттуда непрочитанных сообщений. 

!!! CRAZY тест !!!

Запускаем...Возникает какое-то чёрненькое окошко...что-то жужжит, потом что-то печатается

ЧТО ЭТО ?

а это мы запустили КЛАСТЕР. 

На полном серьёзе - мы запустили рассчётную задачу на кластере MPI, передали ей данные непосредственно из терминала и получили результат. 

просто если уж умеем запускать внешние задачи, то отчего-бы не попробовать толковую вещь:-)

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

Пример кстати показывает и "ахиллесову пяту" облачных и параллельных вычислений - передача и преобразование данных. Мы передавали данные через stdout/stdin в текстовом виде.
Чего конечно-же не делают. Для того чтобы эффективно использовать мощные вычисляторы, нужны ещё и быстрые данные.  

Резюмируя

Получили начальную мульти-задачную системы, в которой:

- можно параллельно исполнять функции терминала в воркерах. Кол-во одновременно-исполняемых = кол-во запущенных воркеров

- можем параллельно исполнять веб-запросы и внешние программы. Кол-во одновременно исполняемых, до 5 штук (можно увеличить, это просто константа с потолка)

- функции воркеров также доступны в скриптах tcl - можем расширять программы mql скриптами

- умеем запускать mpiexec и передавать в него данные

- рассмотрели как расширять воркер. (пишем простую функцию, и регестрируем её в воркере)

- для клиента пока рассмотрели только прямое обращение через интерпретатор, без отдельного класса. Доступные функции и внутренний код можно глянуть в исходниках tcl/lib/mtworkers/mtworker.tcl (файл небольшой и более-менее прокомментирован)

Установка и запуск.

Текущий дистрибутив ATcl 1.09с (tcl/tk для MetaTrader) можно скачать на sourceforge  https://sourceforge.net/projects/mt-atcl/  

Если у вас был установлен ATcl и не хочется скачивать,запускать инсталлятор то можно взять только апдейт библиотеки из аттача или с yandex https://disk.yandex.ru/d/GHOajyMlehx1wg и распаковать в каталог данных терминала

MTWorkers приложен к статье, но можно также кануть с яндекс https://disk.yandex.ru/d/727wDfdvcCZTpQ


в архив MTWorkers в каталог tcl/opt включены :

  tidy , https://www.html-tidy.org/ программа и библиотека для исправлений не вполне корректных html 

  mpich2 1.4.1. https://www.mpich.org/ библиотеки, клиент и сервер MPI 


Для использования ATcl (теперь, усилиями параноиков MQ, надеюсь икается) должны быть предварительно настроены переменные окружения, либо запуск терминала отдельным bat файлом.
Я использую вот такой :

@echo off
REM redefine and uncomment for alternative path
REM ATCL_HOME=c:/ATcl

REM если ATCL_HOME не задан, попробовать умолчания
IF "%ATCL_HOME%" == "" SET ATCL_HOME=C:/ATcl

REM убедиться что ATCL_HOME указывает на каталог
IF NOT EXIST %ATCL_HOME%/ (
        ECHO "ATCL_HOME is not correct"
        EXIT
)

SET MT_COMMON=%APPDATA%/MetaQuotes/Terminal/Common

REM === каталоги opt ===

REM == opt/MPICH
IF EXIST %CD%/tcl/opt/MPICH2/bin/ SET PATH=%CD%/tcl/opt/MPICH2/bin;%PATH% ELSE IF EXISTS %MT_COMMON%/Tcl/opt/MPICH2/bin SET PATH=%MT_COMMON%/Tcl/opt/MPICH2/bin;%PATH%
REM == opt/tidy
IF EXIST %CD%/tcl/opt/tidy/bin/ SET PATH=%CD%/tcl/opt/tidy/bin;%PATH% ELSE IF EXISTS %MT_COMMON%/Tcl/opt/tidy/bin SET PATH=%MT_COMMON%/Tcl/opt/tidy/bin;%PATH%

REM === Python ===
IF "%PYTHONHOME%" == "" SET PYTHONHOME=%ATCL_HOME%/python
IF EXIST %PYTHONHOME%/ SET PATH=%PYTHONHOME%;%PYTHONHOME%/DLLs;%PATH%
IF EXIST %PYTHONHOME%/ SET PYTHONPATH=%PYTHONHOME%/lib

REM === Tcl ===
SET TCL_LIBRARY=%ATCL_HOME%/lib/tcl8.6
SET TCLLIBPATH=%ATCL_HOME%/lib/tcl8.6 %ATCL_HOME%/lib 

SET PATH=%CD%/bin;%MT_COMMON%/bin;%ATCL_HOME%/bin;%PATH%

start "" /B terminal64.exe /portable

весь его смысл - правильно задать переменные окружения TCL_LIBRARY TCLLIBPATH и добавить каталоги содержащие нужные DLL в %PATH%. Раньше это авто-делалось при ините основной DLL, теперь запретили

я использую /portable при запуске терминалов. Чтобы использовать без ключика /portable в начале bat добавьте "cd полный_путь_к_каталогу_данных", а конце уберите ключ /portable


Файлы:
mtworkers.zip  8654 kb
Поделитесь с друзьями: