Конечно не только в тестере, но и в нём тоже. При помощи 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) прикладываю