GUI которое умеет работать в тестере

GUI которое умеет работать в тестере

27 января 2023, 19:11
Maxim Kuznetsov
0
51

Конечно не только в тестере, но и в нём тоже. При помощи ATcl сделаем небольшой  GUI , который в тестере/отладчике не теряет отзывчивости даже при паузе.

скорее всего вы уже догадались, что «чтобы  GUI  работал параллельно с тестером его надо делать в отдельном треде» Конечно-же ! имея такое вот богатство возможностей : https://www.tcl-lang.org/man/tcl8.6/ThreadCmd/thread.htm просто грех им не воспользоваться.

Сценарий

будем делать панель отображающую таблицу с ордерами. Подобную той которая имеется в терминале, но с блек-джеком и девами.

запускаем советник, видим таблицу ордеров группированных по символам. И часы - системные (которые непосредственно от компьютера) и терминальные (те которые TimeCurrent())

Разделение на MQL/Tcl

Всё что относится непосредственно к  GUI , расположение виджетов, их оформление, сортировку, группировку и прочее-прочее будет исполняться на уровне tcl/tk. Эту часть можно будет отдельно тестировать, отлаживать и совершенствовать.

MQL при старте создаст новую нуть, и запустит в ней требуемые скрипты. А в OnDeinit завершит нить.

Советник будет обращаться к командам (методам) скрипта :

  • Insert - добавить новый ордер в таблицу
  • Update - обновить имеющийся ордер в таблице
  • Delete - удалить ордер
  • ServerTime - будет сообщать текущее время TimeCurrent()
  • Show - показать окошко если пользователь его закрыл

Ещё раз - советник занимается торговыми делами, работает с платформой, получает тики, а взаимодействие с пользователем делает уже tcl/tk в отдельном потоке. Максимально разгружаем советник, чтобы время реакции бdло лучше.

работа с тредами

новая нить создаётся командой  thread::create  которой передаётся скрипт для исполнения в отдельном потоке. Как правило в этом скрипте присутсвует команда  thread::wait  - «ожидать команд». И ещё один опциональный аргумент  -preserved  если указан, то нитка создаётся с счётчиком ссылок=1 и когда следующий вызов  thread::release  уменьшит этот счётчик, нить завершится (когда счётчик станет 0)

то есть из советника вы исполняем  thread::create -preserved { package require Thread; thread::wait }  и создаём паралелльную нить которая почти сразу-же перейдёт в состояние ожидания команд.

а дальше мы будем посылать эти команды.  thread::send $tread_id { команды }  отправит команды в очередь заданной нити. Ключик  -async  скажет что не надо дожидаться результата, а продолжить работу сразху. Команды Insert/Update/Delete будем отсылать асинхронно с -async, а ServerTime без ключика. То есть отправка ServerTime будет синхронизовать.

Инициализация советника выглядит вот так (фрагмент про треды):

   // будем исполнять почти всё в отдельном треде
   // берём пакет Thread  
   if (tcl.Eval("package require Thread")!=TCL_OK) {
      ExpertRemove();
      return INIT_FAILED;
   }
   // литералы и часто-используемые слова
   // cделаем однократно
   cmd_orders=tcl.Obj("orders");tcl.Ref(cmd_orders);
   cmd_thread_send=tcl.Obj("thread::send");tcl.Ref(cmd_thread_send);
   opt_async=tcl.Obj("-async");tcl.Ref(opt_async);
   method_update=tcl.Obj("Update");tcl.Ref(method_update);
   method_insert=tcl.Obj("Insert");tcl.Ref(method_insert);
   method_delete=tcl.Obj("Delete");tcl.Ref(method_delete);
   method_servertime=tcl.Obj("ServerTime");tcl.Ref(method_servertime);
   // получаем id текущей нити
   Tcl_Obj curr=tcl.ObjEval("thread::id");
   tcl.Ref(curr);
   PrintFormat("EA thread %s",tcl.String(curr));
 
   // создаём нить со своим event-loop
   if (tcl.Eval("set thr [ thread::create -preserved { package require Thread ; thread::wait } ]")!=TCL_OK)  {
      Alert("thread::create failed: "+tcl.StringResult());
      ExpertRemove();
      return INIT_FAILED;
   } else {
      thr=tcl.Result();
      tcl.Ref(thr);
      PrintFormat("GUI thread %s",tcl.String(thr));
   }
   // сохраним id текущей (родительской) нити в дочерней
   // создаём в нитке переменную parent с id текущей нити
   // thread::send $thr { set ::parent $curr }
   Tcl_Obj cmd=tcl.Obj();
   tcl.Ref(cmd);
   tcl.ListAppend(cmd,tcl.Obj("set"));
   tcl.ListAppend(cmd,tcl.Obj("::parent"));
   tcl.ListAppend(cmd,curr);
   tcl.Call(cmd_thread_send,thr,cmd);
   tcl.Unref(cmd);
   tcl.Unref(curr);
   // исполняем в ней скрипт из ресурса
   // как tcl.Call вместо Eval чтобы гарантировать что ресурс передасться как отдельный объект
   if (tcl.Call(tcl.Obj("thread::send"),thr,tcl.Obj(myOrders_tcl))!=TCL_OK) {
      Alert("thread::send failed: "+tcl.StringResult());
      ExpertRemove();
      return INIT_FAILED;
   }
   // теперь интерпретатор нитки содержит:
   // класс MyOrderView
   // переменную $orders - инстанс класса MyOrderView
   // будем обращаться через thread::send -async {$orders method args..}

и соответственно типичная функция отправляющая данные в Gui выглядит так:

// обновляем информацию тикета
// чтобы не путать, имена - такие-же как методы в скрипте
// вызываем : $obj Update { список_ключей } значения ключей в том-же порядке
void Update()
{
   if (thr==0) return;
   Tcl_Obj cmd=tcl.Obj();              // команда которую будем пересылать к нить
   tcl.Ref(cmd);
   tcl.ListAppend(cmd,cmd_orders);     // объект/команда orders
   tcl.ListAppend(cmd,method_update);  // метод Update
   if (update_args==0) {
      update_args=tcl.Obj("ticket type lots price stopLoss takeProfit profit");
      tcl.Ref(update_args);
   }
   tcl.ListAppend(cmd,update_args);   // первый параметр - список обновляемых полей
 
   // значения в указанном порядке            
   tcl.ListAppend(cmd,tcl.Obj((long)OrderTicket()));  
   tcl.ListAppend(cmd,tcl.Obj(OrderTypeString())); 
   // цены и объёмы в строках, просто потому-что скрипт ещё не умеет округлять double до нужных знаков
   tcl.ListAppend(cmd,tcl.Obj(OrderLotsString())); 
   tcl.ListAppend(cmd,tcl.Obj(OrderPriceString())); 
   tcl.ListAppend(cmd,tcl.Obj(OrderStopLossString())); 
   tcl.ListAppend(cmd,tcl.Obj(OrderTakeProfitString())); 
   tcl.ListAppend(cmd,tcl.Obj(OrderProfitString())); 
   // пересылаем
   tcl.Call(cmd_thread_send,opt_async,thr,cmd);   // отсылаем команду, и не будем ждать её результат
   tcl.Unref(cmd);   // освобождаем cmd
}

результат

и вот что получилось :


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

дальнейшее

связь gui-советник

в демонстрационном примере обратная связь, то есть от  GUI  к советнику не реализована. Можно дополнить панель кнопками/менюшками по реакции на которые что-то должно происходить в советнике. Открывать/закрываться/изменяться ордера и так далее.

С одной стороны мы можем релизовать подобное через thread::send - советник когда находит подходящим вызовет метод Receive, заберёт данные и на них среагирует. Но есть гораздо более подходящие варианты: Thread Shared Variables, tsv : https://www.tcl-lang.org/man/tcl8.6/ThreadCmd/tsv.htm . Можно сказать что это некоторое сильно расширенное подобие Global Variables терминала.

В TSV можно хранить практически произвольные данные, доступ к данным производится по двойным идентификаторам (имя, элемент); К некоторым типам (списки, коллекции ключ-значение, ассоциативные массивы) даны дополнительные удобные  API . Можете считать что это такая оперативная разделяемая база данных для обмена между нитями (а-ля REDIS). Очереди и стеки «из коробки» и многое-многое другое.

Обмен данными между нитями лучше делать через TSV. Например  GUI  будет помещать запросы в заранее оговоренную очередь, а советник оттуда читать. Тогда никто никого не ждёт и все занимаются своим делом.

развитие интерфейса

и сам по себе внешний вид «таблицы ордеров» можно сделать покрасивее - добавить сортировку, разные варианты групировок, промежуточные и итоговые суммы, показывать/скрывать/перемещать столбцы, подсвечивать выделять…. Взятый компонент treectrl очень многое позволяет. Просто полюбопытсвуйте https://tktreectrl.sourceforge.net/ все подобные красоты можно добавить.

И не затрагивая при разработке советник. Методов Insert,Update,Delete более чем достаточно, всё прочее дело View.

Скачать библиотеку ATcl отсюда https://uploadfiles.in/6q9 или https://dfiles.eu/files/3sigiedi1 или https://www.filefactory.com/file/nmqp3op9f9q/atclsetup_1.01.exe

а также со страницы загрузки эскизного сайта http://atcl.unaux.com/download/

или следуя инструкциям, из репозитария проекта https://chiselapp.com/user/nektomk/repository/atcl-lib/home

Читайте также предыдущие статьи:

Исходники (и mq5 и tcl) прикладываю


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