bstone
bstone личный блог
25 февраля 2015, 17:59

Кооперативная многозадачность в 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'ы нормально дружат с обработкой исключений, что немаловажно. Аналогично можно работать с заявками, таблицей позиций, да с чем угодно — код будет чище и проще, а депозит сохраннее.

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

36 Комментариев
  • сомневаюсь что это будет работать
      • bstone, и как? работет сий костыль?)
  • velikan
    25 февраля 2015, 20:12
    как же хорошо, что я отказался и от qplie и от lua, и спокойно пересел на mql5…
    спасибо Зевсу и Юпитеру
      • velikan
        25 февраля 2015, 20:59
        bstone, да, ооп — очень удобно
        с lua постоянно приходится придумывать костыли — не люблю я это
  • Machez_fewtasks.ru
    25 февраля 2015, 20:12
    Github для совместной разработки ru.m.wikipedia.org/wiki/GitHub Нормальное программирование в C++.
    • velikan
      25 февраля 2015, 20:13
      Machez_fewtasks.ru, лучше уж bitbucket — там можно держать приватные репы
      • Machez_fewtasks.ru
        25 февраля 2015, 20:23
        velikan, может быть. Лучше не изобретать велосипед и пользоваться тем, что себя уже зарекомендовало — так, хоть программистом можно работать, если не получится с трейдингом. Времени на изучение разного софта можно потратить достаточно много, если не пользоваться нормальными решениями.
        • velikan
          25 февраля 2015, 20:25
          Machez_fewtasks.ru, да, только многие программисты хотят и любят изобретать велосипеды :)
          не пользуясь тем, что для этого предназначено :)
          • Machez_fewtasks.ru
            25 февраля 2015, 20:30
            velikan, это известная проблемма, поэтому и написал пример нормального софта, который еще и бесплатный — как C++ так и Github.
            • velikan
              25 февраля 2015, 21:00
              Machez_fewtasks.ru, да, bitbucket поэтому и юзаю, что бесплатный и разрешает приватные репы :)
      • Machez_fewtasks.ru
        25 февраля 2015, 20:33
        bstone, C++ подключается практиче в любой софт, в т.ч. в Метатрейдер. С помощью DLL. Поэтому, без LUA и MQL как-то обходится с минимумом программирования в терминалах. Если кому-то интересен алготрейдинг с использованием C++ можно обсудить на форуме-ссылка в моем профиле.
          • Machez_fewtasks.ru
            26 февраля 2015, 01:05
            bstone, к Квику подключается аналогично МТ, т.е. с использованием Dll. Брокер-адаптор, или Fix адаптер, по сложности обычно сложнее индикатора или робота, поэтому проще с использованием Квик или Метатрейдера.
  • Roman Ivanov
    25 февраля 2015, 22:12
    Костыль по сложности не меньше чем событийные обработчики. Поди еще и неожиданные эффекты дает в количестве.
    • П М
      25 февраля 2015, 22:37
      ivanovr, вот вот. разработчики квика, насколько я знаю, не поддерживают многозадачность и корректность синхронизации данных между потоками. в смысле для потоков которые не они сами создают.
      и их можно понять.
      всё что автор описал как «проблема» решается уже реализованными колбеками
      достаточно открыть Qlua.chm и набрать в поиске «Функции обратного вызова»
      • ПBМ, квик тут не причем, дело в тс, в протоколе сервера
      • Roman Ivanov
        26 февраля 2015, 00:22
        ПBМ, при таком подходе, насколько я понимаю, нет полноценной многопоточности. coroutine создает отдельный стек и выполняет run тем же потоком, что и вызвал resume, но с переключением на другой стек. Когда внутри run вызывается yield, то делается переключение на первый стек и делается возврат из resume. И так постоянно.
        Так что код в run может быть прерван только в yield, а значит сложной синхронизации не требуется.
      • Roman Ivanov
        26 февраля 2015, 00:36
        bstone, с LUA честно говоря не знаком, но у меня есть робот на языке общего назначения. В нем все на событиях, причем состояние хранится в БД. Если прогу рестартануть, то поскольку все состояние в БД, это никак не повлияет на работу алгоритма. Такие потоки может где-то и полезны, но они содержат в себе состояние, которое сдохнет при рестарте. А значит такой поток нельзя делать долго работающим, чтобы не нарваться на потерю данных. И тогда в целом польза от подхода сомнительна.
        Возможно, что в LUA нет сохранения состояния и значит «все равно будет плохо», но концептуально в этих задачах логика на обработчиках более «православна».
        ИМХО
  • xxxyyy
    26 февраля 2015, 00:53
    Спасибо, есть над чем подумать — может в будущем пригодится. Хотя сам стараюсь не обращаться к таблицам сделок и заявок, именно потому, что таблицы обновляются позже. Да и не обойтись наверное без колбеков, т.к. они дают код ошибки в случае, если что-то пошло не так.
      • xxxyyy
        27 сентября 2015, 14:17
        bstone, ещё раз спасибо за пример. Сорри, но только сейчас понял, к чему всё это. Проводя проверку своего робота снова наткнулся на этот пост и понял, что мои костыли в проверке выполнения нескольких событий и соотнесения номера заявки и транзакции могут быть проще (а значит, надеюсь, надёжнее) если использовать Ваш способ.

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн