1

Тема: Советы по написанию Lua-роботов от профессионалов

На форуме QUIK наткнулся на совершенно шикарный список советов про создание скриптов на Lua от пользователя _sk_
Скопирую сюда.

Что надо продумать при создании торгового робота на Lua

Порядок OnTransReply -> OnTrade -> OnOrder обычно соблюдается, но может нарушаться. Программу надо писать так, чтобы она работала всегда. Вы ведь не хотите получить внезапно ошибку и убыток из-за неё.

Многократные ответы OnTrade() появились в 7-й версии. В повторных ответах могут добавиться некоторые поля.

Функция sendTransaction() может успешно выполниться, а OnTransReply() не придёт. Такое бывает, но очень редко. Однако, программу надо писать так, чтобы она работала всегда.

С асинхронностью нужно разобраться сразу и навсегда, чтобы не словить внезапных глюков. Один из способов такой: при получении коллбэка передавать данные через очередь из потока коллбэков в поток main(). В потоке main() периодически проверять очередь и обрабатывать данные, которые из неё приходят.

Проблему можно решить без участия команды QUIK. Надо прочитать документацию, разработать модель, запрограммировать её. Форум поможет при решении более тонких вопросов.

Основные шаги реализации примерно такие.

1) Нужны статусы заявок типа WAITING (ещё не отправлена), EXECUTING (отправлена на биржу), STOPPING (снимается, но не вся информация дошла), STOPPED (снята), EXECUTED (исполнена полностью), KILL_REJECTED (kill-заявка отвергнута), ERROR (ошибка).

2) Нужны таймауты для борьбы с неопознанными "посторонними" заявками (от других скриптов), а также заявками, для которых не пришёл OnTransReply().

3) Нужно следить за UID экземпляра QUIK, чтобы фильтровать "чужие" заявки.

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

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

6) Нужна таблица актуальных kill-заявок, куда заявка попадает при попытке снять limit-заявку, а удаляется при ошибках и исполнении.

7) Нужна таблица ответов о сделках на актуальные limit-заявки.

8) Нужен фильтр событий от "чужих" OnTrade(), например, работающий по комментарию к заявке.

9) В limit-заявке нужно помнить volume, volumeTraded, volumeLeft. Обычно volumeTraded + volumeLeft == volume, но иногда при снятии частично исполненной заявки становится понятно, чему равен volumeLeft и надо дождаться событий OnTrade(), которые ещё пока не пришли. При приходе OnTrade() нужно отбрасывать дубликаты (в 7-й версии) и корректировать volumeTraded, volumeLeft. Как только volumeLeft == 0, так удалять заявку из таблицы актуальных вместе со всеми связанными с ней kill-заявками и ответами на них.

10) При попытке снять limit-заявку иногда надо ждать, чтобы понять, какой order_num у отправленной заявки, поскольку ещё мог не придти ответ OnTransReply().

11) Периодически проверять, не стали ли известны order_num для лимитных заявок, для которых есть kill-заявки.

12) Периодически проверять, не стало ли известно, каким лимитным заявкам соответствуют OnTrade(), которые пришли раньше, чем OnTransReply().

13) Периодически разбираться с тем, сработали ли отправленные kill-заявки.

14) Удалять старые "неопознанные" сделки.

15) Реагировать на OnTransReply(), связывая transId скрипта и order_num биржи для limit-заявок. Понимать, как сработали kill-заявки. Проверять ждущие kill-заявки, если они есть.

16) Реагировать на OnTrade(), игнорируя дубликаты, логгируя соответствующие сделки.

17) Реагировать на OnOrder(), связывая transId скрипта и order_num биржи для limit-заявок. Понимать, что для некоторых уже неактивных заявок не дошли OnTrade() и логгировать сообщения об ошибках.

18) Периодически обрабатывать kill-заявки, чтобы понять, какие из них остались без OnTransReply(). Посылать новые, если что.

19) Периодически обрабатывать limit-заявки, чтобы понять, для каких из них получились кросс-сделки, а какие остались без OnTransReply(). Проверять, limit-заявки, которые почему-то неактивны, а volumeLeft не равен 0, и логгировать ошибки.

Как-то так.

2

Re: Советы по написанию Lua-роботов от профессионалов

.

3 (2019-03-02 03:09:41 отредактировано kalikazandr)

Re: Советы по написанию Lua-роботов от профессионалов

как то все сложно.

1. OnTransReply обрабатываем только ошибки - не пришла и хрен с ней, значит ошибки нет.
Если ошибка не связана с состоянием сессии то желательно бота выключить, т.к. где-то у вас ошибка алгоритма, которую нужно править при выключенном боте. Если таки ошибка - "сессия не идет", то лучше не пытаться выставить заявку заново со старой ценой и объемом - обнулить сигнал и получить новый после начала сессии.
2. OnOrder - делаем таблицу Orders = {order_num = true}, - если еще такого номера заявки в Orders нет - запоминаем и return. Если № есть - передаем в main для обработки.
    2.1 active/status - состояния заявки:
         0/"O"/"K"/"F" - нет заявки ожидание/снята/исполнена;
         1/"O" - транзакция новой заявки оправлена на сервер квик;
        -1/"A" - транзакция снятия заявки отправлена на сервер квик;
         2/"A" - заявка зарегистрирована в ТС - активна.
В алгоритме проверяем if math.abs(active) == 1 then return end - транзакция в пути.
3. OnTrade - не нужна, есть функция быстрого поиска (сделки ищем по № заявки), если исполненное кол-во лотов в заявке не равняется кол-ву лотов в сделках - курить, пока не будет равенства, иначе квик не пересчитает лимиты, особенно критично для заявок на закрытие позиции. Заодно можно посчитать avg_price.
4. с генерацией Trans_id и UID терминала не стоит заморачиваться вообще. Есть поле brokerref(комментарий), которое делаем так:
clientcode.."//"..botname..id, нужно понимать, что коментарий составное 20-ти символьное поле и для фондовой секции арка удосужилась разделить его "//" а не "/", т.е. украла 1 символ, а для срочки вообще пофик, можно любой разделяющий символ.
botname - имя стратегии; id - что душе угодно, 1,2,3... вполне себе подойдет.
Если бот видит, что brokerref не его - вот и фильтр, на все случаи.
5. Сделать таблицу CloseOrders = {order_num = true,} На старте бота делать обход таблицы заявок на предмет поиска не активных заявок, их запоминать и не принимать по ним OnOrder, все не активные заявки в ходе торговли помещать туда. Чистить CloseOrders по мере необходимости.
6. Сделать таблицу ActiveOrders = {[brokerref] = order}, где order содержит первоначальное состояние отправленной заявки в ТС и потом его сравнивать с изменениями этой заявки.
7. Если нет ответа на транзакцию выставления/снятия заявки, то ни в коем случае нельзя пытаться ее перевыставить. Лучше ожидать ответ до посинения и в итоге его получить и обработать должным образом. Если у бота чешется и очень срочно хочется/нужно купить/продать - то создать другую заявку, а старую не забывать, она не обязательно потерялась в сети, просто подлый брокер занимается махинацией и держит события от ТС на прокси, а в нужный момент отправит события, связанные с этой заявкой клиенту. Только не факт, что и по этой заявке ответ придет сразу, что чревато, т.к. депо может внезапно кончится, а цена приобретения инструмента неожиданно будет на десяток - другой процентов хуже - здравствуйте Дядя Коля. Краями, можно попытаться выставить заявку по минимально/максимально возможной цене 1-м лотом, и если по ней ответ придет, тогда можно считать, что прошлая транзакция сдохла в сети, но и это не 100% гарантия. Надежнее всего звонить брокеру, и спрашивать, а где же где же заявка с вот таким trans_id, по инструменту и время отправки и если ее нет, то перезапустить бота.

Все.

4

Re: Советы по написанию Lua-роботов от профессионалов

Кстати, хочу отметить, что OnOrder не вызывается терминалом по событию получения данных о заявке терминалом. У них там sleep и очередь. Т.е. если в main делать опрос на предмет новой записи в таблице заявок/сделок и др., то в main вы увидите событие в 99% случаев раньше на 15 мс, чем будет вызван соответствующий callback в вашем алгоритме.
Единственное не удобство - это отжим времени у торгового алгоритма, за счет ожидания полной загрузки данных в таблицу иначе можно поймать nil или пол цены(без дробной части, например).

А вот отсутствие TransReply - тут недавно совсем арке писал про это - пяткой в грудь себя били - "у нас всегда все TransReply приходят - это ты м*дак".

5 (2019-02-26 10:19:43 отредактировано swerg)

Re: Советы по написанию Lua-роботов от профессионалов

kalikazandr, спасибо вам!!
Это в самом деле очень ценные знания, за которыми явно куча практического опыта.

6

Re: Советы по написанию Lua-роботов от профессионалов

swerg пишет:

kalikazandr, спасибо вам!!
Это в самом деле очень ценные знания, за которыми явно куча практического опыта.

Н.з.ч) практика, да.
Алгоритмы сложные пишу по 10-20 "умных" заявок на открытие/закрытие/реверс, боты одной и разных стратегий общаются с собой через StaticVar. Один раз написал блок контроля заявок и не думаю - а что же делать с конкретной заявкой - они все для ботов обезличены и имеют встроенные алгоритмы поведения: поиск сигнала - сопровождение позиции.

Например бот(Si66000), который работает на страйке 66000, транслирует в общее пространство имен волатильность этого страйка, т.к. это его страйк и это центральный страйк, а бот(Si65500) ничего не знает о коде инструмента на 66000 страйке, но знает текущую волатильность на центральном страйке, а также открытую позицию пут/кол/фьюч на центральном страйке, ну и вообще всю инфу об остальных ботах-страйках, а так же инфу ботов других базовых активов.

Так же половину алгоритма я скидываю в таймеры qvcl - это закрытие позиций по времени, которые не требуют скорости.
Связка потоков квик-main-timer позволяет одновременно работать 500 роботам без сколько-то видимой нагрузки на ЦП

запускаемый бот выглядит вот так:

# steps.lua
strike = 66000
base = "Si"

package.path=package.path..";C:/Creator/lib/?.lua;C:/Creator/Strategy/steps/?.lua"
package.cpath=package.cpath..";C:/Creator/lib/?.dll"
--package.loaded.steps = nil -- раскоментировать если была ошибка загрузки торгового модуля
dofile("C:/Creator/smart_forts.lua") -- блок контроля "умной заявки", содержит main

local steps = require'mysteps'
function Robot()
    while working do
        steps:init()
        Trade()
        while start do
            steps:process()
            Trade()
        end
        Trade()
    end
end

Клонируем, меняем страйк и базовый актив - бот готов)