Вступление
Материала по 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'ы нормально дружат с обработкой исключений, что немаловажно. Аналогично можно работать с заявками, таблицей позиций, да с чем угодно — код будет чище и проще, а депозит сохраннее.
Такая вот петрушка. Возможно кому-то это будет полезно. Успехов в автоматизации и работорговле!
спасибо Зевсу и Юпитеру
с lua постоянно приходится придумывать костыли — не люблю я это
не пользуясь тем, что для этого предназначено :)
и их можно понять.
всё что автор описал как «проблема» решается уже реализованными колбеками
достаточно открыть Qlua.chm и набрать в поиске «Функции обратного вызова»
Так что код в run может быть прерван только в yield, а значит сложной синхронизации не требуется.
Возможно, что в LUA нет сохранения состояния и значит «все равно будет плохо», но концептуально в этих задачах логика на обработчиках более «православна».
ИМХО