Блог им. bstone

Кооперативная многозадачность в LUA как неплохое подспорье для ваших роботов

 

Вступление

Материала по LUA для новичков, мне кажется, более чем достаточно. Вот с более продвинутыми идеями какой-то напряг. Добавлю одну в общую копилку.

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

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

Проблема

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

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

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

Решать эти проблемы можно разными способами, но большинство из них ведут к чрезмерному усложнению кода, что неизбежно увеличивает количество ошибок. Последние же иногда больно бьют по депозиту.

Решение

Элегантным решением такого рода проблем как раз и является реализация кооперативной многозадачности в LUA. Она позволяет красиво и просто перенести сложности асинхронной обработки в плоскость синхронной последовательности выполнения тех или иных операций.

Простейшим примером хорошо послужит функция создания стоп-приказа, после выполнения которой нам будет известен номер ордера, а сам он гарантированно должен отражаться Quik'ом в его таблице ордеров, назовем ее placeStopOrder(). Детали буду опускать, концентрируясь на теме поста:

function placeStopOrder( ticker, stopOrder )

    local transId = _sendStopOrder( ticker, stopOrder );

    local newStopOrder = nil;

    repeat

        coroutine.yield();

        newStopOrder = _findStopOrderByTransId( transId );

    until( newStopOrder )

    return newStopOrder.order_num;
end

Функция использует две вспомогательные: _sendStopOrder() — для отправки соответствующей транзакции на сервер и _findStopOrderByTransId() — для поиска нового стоп-ордера в таблице ордеров по номеру транзакции.

Это простые функции и они не представляют особого интереса. Гораздо важнее здесь вызов coroutine.yield(). coroutine — это и есть компонент, обеспечивающий в LUA кооперативную многозадачность.

При обычном вызове placeStopOrder() вызов coroutine.yield() ничего не даст кроме ошибки. Однако если placeStopOrder() вызывается внутри исполняющейся coroutine, вызов coroutine.yield() прервет ее выполнение и передаст его в точку, где эта coroutine была запущена. Это позволяет очень эффективно и без всяких костылей обеспечивать работу кода, который требует выполнения нескольких асинхронных условий перед последующей обработкой их результатов.

Для использования этой возможности необходимо всего лишь обеспечить работу такого кода внутри coroutine, например так:

------------------------------------------------------------------------------------------
function run()

    while( true ) do

        — ...
        — код рабочей итерации
        — ...

        if( shouldPlaceStopOrder() ) then

            local stopOrder = {… начинка… };

            local stopOrderNum = placeStopOrder( TICKER, stopOrder );

            — дальше можно работать с уже известным номером
            — стоп-ордера, если нужно
            handleNewStopOrder( stopOrderNum );
        end

        — ...
        — код рабочей итерации
        — ...

        coroutine.yield();
    end
end

------------------------------------------------------------------------------------------
function main()

    local coRun = coroutine.create( run );

    while( not shouldStop() ) do

        coroutine.resume( coRun );
        sleep( 100 );
    end
end

В приведенном коде функция main() обеспечивает работу функции run() внутри coroutine. В момент вызова placeStopOrder() эта функция может вернуться сразу, если транзакция отработана моментально, а может и не вернуться, если стоп-приказ еще не появился в таблице ордеров. В этом случае управление вернется в функцию main(), которая выполнит sleep( 100 ) и после этой задержки снова отдаст управление в функцию placeStopOrder() — это важно, именно в placeStopOrder() (туда, где был вызов coroutine.yield()), а не вызовет run() по новой.

Таким образом вызов placeStopOrder() остановит выполнение основной логики до тех пор, пока стоп-приказ не будет выставлен на 100%. Основной код при этом не страдает из-за лишних проверок, отслеживания событий и т.п. Ну и очевидно, что логика продолжения может быть намного более продвинутой, чем sleep( 100 ) — «параллельно» могут выполняться несколько задач. Пока кто-то ждет результатов от сервера, другой код может выполнить свои задачи в текущей итерации.

Пример существенно упрощенный, но в него легко добавить много чего еще, например выход из placeStopOrder() по тайм-ауту, если сервер вообще забил на выставление вашего стоп-ордера или связь с ним была потеряна надолго. Отдельно отмечу, что coroutine'ы нормально дружат с обработкой исключений, что немаловажно. Аналогично можно работать с заявками, таблицей позиций, да с чем угодно — код будет чище и проще, а депозит сохраннее.

Такая вот петрушка. Возможно кому-то это будет полезно. Успехов в автоматизации и работорговле!

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

bstone

bstone, и как? работет сий костыль?)
Профессор Преображенский, еще как. Да и вообще, по сравнению с другими вариантами — это не костыль, а ультра-современный механизированный протез :)
avatar

bstone

Евгений, ну продвинутая тема, возможно без пол-литра сразу и не разберешься, что поделать )
avatar

bstone

Евгений, я вас понял, не осуждаю — все с чего-то начинали. Пока пользы от этой идеи для вас будет мало конечно.
avatar

bstone

Евгений, не за что. Озадачил вас — значит уже не зря его потратил.
avatar

bstone

как же хорошо, что я отказался и от qplie и от lua, и спокойно пересел на mql5…
спасибо Зевсу и Юпитеру
avatar

velikan

velikan, из плюсов у MQL5 то, что он компилируемый и похож на С++. В плане гибкости LUA ему фору даст, но тошнит от обоих )
avatar

bstone

bstone, да, ооп — очень удобно
с lua постоянно приходится придумывать костыли — не люблю я это
avatar

velikan

velikan, в lua тоже удобное ооп, причем можно даже множественное наследование изобразить. Более того, я спокойно изображал ооп на mql4. Но не только в ооп дело. Например в mql обработка ошибок — гиблое дело, т.к. нет механизма исключений.
avatar

bstone

Github для совместной разработки ru.m.wikipedia.org/wiki/GitHub Нормальное программирование в C++.
avatar

Machez_fewtasks.ru

Machez_fewtasks.ru, лучше уж bitbucket — там можно держать приватные репы
avatar

velikan

velikan, может быть. Лучше не изобретать велосипед и пользоваться тем, что себя уже зарекомендовало — так, хоть программистом можно работать, если не получится с трейдингом. Времени на изучение разного софта можно потратить достаточно много, если не пользоваться нормальными решениями.
avatar

Machez_fewtasks.ru

Machez_fewtasks.ru, да, только многие программисты хотят и любят изобретать велосипеды :)
не пользуясь тем, что для этого предназначено :)
avatar

velikan

velikan, это известная проблемма, поэтому и написал пример нормального софта, который еще и бесплатный — как C++ так и Github.
avatar

Machez_fewtasks.ru

Machez_fewtasks.ru, да, bitbucket поэтому и юзаю, что бесплатный и разрешает приватные репы :)
avatar

velikan

Machez_fewtasks.ru, на данный момент оптимальная связка Quik'а c C++ — это LUA. Так что никуда не денетесь :) Если сразу с FIX/Plaza не начинать.
avatar

bstone

bstone, C++ подключается практиче в любой софт, в т.ч. в Метатрейдер. С помощью DLL. Поэтому, без LUA и MQL как-то обходится с минимумом программирования в терминалах. Если кому-то интересен алготрейдинг с использованием C++ можно обсудить на форуме-ссылка в моем профиле.
avatar

Machez_fewtasks.ru

Machez_fewtasks.ru, я вас понял, но на Метатрейдере свет клином не сошелся. До недавнего времени он был исключительно кухонным инструментом. Попробуйте подключить С++ к самому распространенному терминалу для доступа к нашей бирже — Квику. Хотя как я уже и сказал, раз уж вы о серьезном подходе, то надо смотреть в сторону FIX или Плазы. FIX конечно мутный и разные брокеры поддерживают его в рамках своих ограниченных способностей (IB, это я на тебя сейчас смотрю!), но все-таки стандарт индустрии.
avatar

bstone

bstone, к Квику подключается аналогично МТ, т.е. с использованием Dll. Брокер-адаптор, или Fix адаптер, по сложности обычно сложнее индикатора или робота, поэтому проще с использованием Квик или Метатрейдера.
avatar

Machez_fewtasks.ru

Костыль по сложности не меньше чем событийные обработчики. Поди еще и неожиданные эффекты дает в количестве.
avatar

ivanovr

ivanovr, вот вот. разработчики квика, насколько я знаю, не поддерживают многозадачность и корректность синхронизации данных между потоками. в смысле для потоков которые не они сами создают.
и их можно понять.
всё что автор описал как «проблема» решается уже реализованными колбеками
достаточно открыть Qlua.chm и набрать в поиске «Функции обратного вызова»
avatar

ПBМ

ПBМ, квик тут не причем, дело в тс, в протоколе сервера
Профессор Преображенский, совершенно верно.
avatar

bstone

ПBМ, ошибочное суждение. Одна из проблем, которые я отметил как раз в том, что в момент срабатывания колбэка у квика самого еще старые данные, т.к. события с сервера идут отдельно от обновления данных (таких как таблицы ордеров и позиций) квика. Т.е. пришел колбек о создании ордера, а информации об ордере у Квика еще нет. Или пришел колбек о сделке, а позиция по инструменту еще не обновилась. И т.п.
avatar

bstone

ПBМ, при таком подходе, насколько я понимаю, нет полноценной многопоточности. coroutine создает отдельный стек и выполняет run тем же потоком, что и вызвал resume, но с переключением на другой стек. Когда внутри run вызывается yield, то делается переключение на первый стек и делается возврат из resume. И так постоянно.
Так что код в run может быть прерван только в yield, а значит сложной синхронизации не требуется.
avatar

ivanovr

ivanovr, не то сравниваете. Я этот «костыль» использую с событийными обработчиками в том числе. Здесь просто пример простейший. Неожиданных эффектов в линейном коде с гарантированными результатами (т.е. меньшим количеством исключительных ситуаций таких как отсутствие результата) всегда меньше.
avatar

bstone

bstone, с LUA честно говоря не знаком, но у меня есть робот на языке общего назначения. В нем все на событиях, причем состояние хранится в БД. Если прогу рестартануть, то поскольку все состояние в БД, это никак не повлияет на работу алгоритма. Такие потоки может где-то и полезны, но они содержат в себе состояние, которое сдохнет при рестарте. А значит такой поток нельзя делать долго работающим, чтобы не нарваться на потерю данных. И тогда в целом польза от подхода сомнительна.
Возможно, что в LUA нет сохранения состояния и значит «все равно будет плохо», но концептуально в этих задачах логика на обработчиках более «православна».
ИМХО
avatar

ivanovr

Спасибо, есть над чем подумать — может в будущем пригодится. Хотя сам стараюсь не обращаться к таблицам сделок и заявок, именно потому, что таблицы обновляются позже. Да и не обойтись наверное без колбеков, т.к. они дают код ошибки в случае, если что-то пошло не так.
avatar

Yuri

Yuri, это универсальный подход — вы можете аналогично прерывать свой код до тех пор, пока колбек не просигналит о том или ином событии. Главное здесь — возможность скрыть сложность ассинхронных колбеков/событий от основного кода.
avatar

bstone

bstone, ещё раз спасибо за пример. Сорри, но только сейчас понял, к чему всё это. Проводя проверку своего робота снова наткнулся на этот пост и понял, что мои костыли в проверке выполнения нескольких событий и соотнесения номера заявки и транзакции могут быть проще (а значит, надеюсь, надёжнее) если использовать Ваш способ.
avatar

Yuri

Yuri, именно так.
avatar

bstone


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

Залогиниться

Зарегистрироваться
....все тэги
UPDONW