Заметка

Qlua для чайников. Часть 5. Работа с таблица Quik. Поиск заявок. Искусство отладки

  6  

Мы продолжаем создавать нашего биржевого робота спредера. В этом уроке будем учиться искать заявки и разбираться с процессом отладки.

Qlua для чайников. Часть 1

Qlua для чайников. Часть 2. Циклы

Qlua для чайников. Часть 3. Работа со стаканом

Qlua для чайников. Часть 4. Анализ информации из стакана и работа с заявками

На прошлом уроке мы с вами написали заготовку, которая рассчитывает цены выставления наших заявок, на основе крайних цен в стакане (программа считает заданный отступ от этих цен). Если вы не читали прошлый урок, все равно зайдите на него и скачайте приложение – заготовку робота, в этом уроке вам она понадобится.

Как я уже говорил, у нашей программы есть недочеты. Во-первых, из-за того, что события изменения стакана приходит раньше, чем событие выставления заявок, у нас иногда проскакивают неверные цены. Подробнее опять см. прошлый урок. Во-вторых, после запуска у нас робот начинает работать только после того, как произойдут первые изменения в стакане. Как исправить эти недочеты? Давайте подумаем.

Итак, начнем с первой проблемы – рассинхронизация событий. Как вариант решения – все таки сделать поиск заявок, а не читать параметры введенной заявки в событии OnOrder. А сейчас  внимание!!!! – важная информация. Поиск заявок – это одна из задач, которые можно решить при помощи функции SearchItems. Эта функция предназначена для поиска информации в различных таблицах Quik. Таблица заявок – это одна из таких таблиц.  Полный список таких таблиц можно посмотреть здесь http://help.qlua.org/ch4_5_3.htm, нас же интересует пока только таблица orders – заявки.

Для поиска заявок пишем функцию find_orders:

--Процедура поиска ордеров

function find_orders()

      local NO=getNumberOf("orders")

      t_orders = SearchItems("orders", 0, NO-1, fn, "flags, sec_code, class_code")

      if t_orders ~= nil then

            for i=1,#t_orders,1 do

                  t_orders_item=getItem("orders", t_orders[i])

                  remember_order(t_orders_item)

            end

      end

end

Что делает эта функция? Во-первых, она получает количество элементов в таблице orders (количество заявок):

local NO=getNumberOf("orders")

Для чего это нам надо? Для того, что бы вызвать функцию SearchItems, которой необходимо указать диапазон поиска. Диапазон поиска начинается с нуля. Поэтому мы указываем так:

t_orders = SearchItems("orders", 0, NO-1, fn, "flags, sec_code, class_code")

Первый параметр SearchItems – это имя таблицы, в которой мы ищем, в данном случае orders. Второй параметр – начало диапазона поиска, третий конец диапазона поиска, четвертый –поисковая функция, о ней сейчас скажу отдельно. Пятый параметр  — это список полей таблицы ордеров, которые будет анализировать поисковая функция. У каждой таблицы свой набор полей, что касается таблицы orders, то полный список полей можно посмотреть тут http://help.qlua.org/ch4_6_4.htm. В нашем же случае используются следующие поля:

  • Flags – набор битовых флагов, про них я рассказывал на уроке 4. (ссылка)
  • sec_code – код инструмента.
  • class_code – код класса.

Теперь сама поисковая функция:

--Поисковая функция

function fn(flags, sec_code, class_code)

      if sec_code==p_seccode and class_code==p_classcode and bit.band(flags,1)>0 then          

            return true

      else

            return false

      end

end

Эта функция производит анализ входных параметров. Значения входных параметров – это значения полей, перечисленных в пятом параметре функции SearchItems. В частности, мы проверяем поля sec_code и class_code – наш ли это инструмент и проверяем первый флаг битовых флагов, который сигнализирует о том, выставленная ли заявка. Работа с флагами так же была описана в уроке 4 (ссылка).

Если заявка найдена, а это значит, что она удовлетворяет заданным условиям – активна и по нашем инструменту – то  эта заявка запоминается. Для этого используется функция remember_order:

function remember_order(order)

      p_file:write(os.date().."  заявка "..order["order_num"].."\n")

      --если заявка активна, то запоминаем ее

      if bit.band(order["flags"],1)>0 then

            --message("флаг:"..bit.band(order["flags"],4),1)

            if bit.band(order["flags"],4)>0 then

                  sell_order=order["order_num"]

                  sell_price=tonumber(order["price"])

                  sell_count=tonumber(order["balance"])

            else

                  buy_order=order["order_num"]

                  buy_price=tonumber(order["price"])

                  buy_count=tonumber(order["balance"])

            end

      else

            --если заявка не активна то сбрасываем информацию о заявке

            if bit.band(order["flags"],1)>0 then

                  if bit.band(order["flags"],4)>0 then

                        sell_order=""

                        sell_price=0

                        sell_count=0

                  else

                        buy_order=""

                        buy_price=0

                        buy_count=0

                  end

            end

      end

end

По сути, эта функция – кусок кода, выдранный из OnOrder и повторяющий ее. Поэтому мы можем просто вызвать remember_order из функции OnOrder, сократив ее:

function OnOrder(order)

      p_file:write(os.date().." OnOrder\n");

      --сначала проверим, по нашему ли инструменту эта заявка

      if order["sec_code"]==p_seccode and order["class_code"]==p_classcode then

            remember_order(order)

      end

end

А в функции анализа стакана OnQuote добавим вызов find_orders() :

function OnQuote(class_code, sec_code)

      if class_code==p_classcode and sec_code==p_seccode then

     

            find_orders()

Теперь, теоретически, у нас должен исправиться недочет. Но не тут-то было. Если посмотреть лог, то можно увидеть, что перед выставлением заявки крайние цены все равно считаются неправильно. Ну что ж, зато есть повод поучиться отлаживать программу. Сразу скажу, что в Квике с отладчиком очень тяжко. Точнее, его нет совсем. Так что придется довольствоваться либо логированием, либо сообщениями.

Начнем отладку. Процесс поиска ошибок можно проводить по следующему алгоритму:

  1. Выдвигаем гипотезу, где может быть ошибка.
  2. Проверяем эту гипотезу.
  3. Если ошибка найдена, то исправляем. Иначе см. п. 1.

В данном случае логично предположить, что не работает функция поиска. Так что вставляем отладочное сообщение в нее:

--Процедура поиска ордеров

function find_orders()

      local NO=getNumberOf("orders")

      t_orders = SearchItems("orders", 0, NO-1, fn, "flags, sec_code, class_code")

      message("find_orders: "..tostring(t_orders),1)

      if t_orders ~= nil then

            for i=1,#t_orders,1 do

                  t_orders_item=getItem("orders", t_orders[i])

                  remember_order(t_orders_item)

            end

      end

end

Запускаем. Видим ряд сообщений. Вводим заявку. Сначала видим сообщение о факте ввода заявки:

 

Затем видим наше сообщение:

  

Оно говорит о том, что функция SearchItems ничего не нашла. Но потом, с очередным изменением стакана приходит другое сообщение:

  

Оно уже говорит нам о том, что функция все-таки что-то нашла.

Мы выяснили, что функция SearchItems находит выставленные заявки не сразу, а лишь спустя некоторое время после их выставления.

Итак, нам опять придется искать выход. Еще можно попробовать событие OnTransReply. Оно вызывается, когда происходит транзакция. То есть, если мы выставили заявку, то у нас должно прийти OnTransReply. Теоретически, OnTransReply должно прийти раньше, чем OnOrder. Но лучше все-таки это проверить.  Для этого добавляем в нашего робота вот такую функцию:

--обработка события транзакции

function OnTransReply(trans_reply)

      nord=trans_reply["order_num"] --Номер заявки

      if nord==nil or nord==0 or nord=="0" then

            message("Заявка не выставилась",1)

            return

      end

      if trans_reply["sec_code"]==p_seccode and trans_reply["class_code"]==p_classcode then

            remember_order(trans_reply)

      end

end

Но, увы, и это ничего не дало. По-прежнему один такт выдается неверная цена:

09/25/14 14:31:31    300.68,302.2

09/25/14 14:31:31    300.68,302.2

09/25/14 14:31:32    301.01(!!!!),302.2

09/25/14 14:31:32 OnOrder

09/25/14 14:31:32  заявка 3217747

09/25/14 14:31:32 OnOrder

09/25/14 14:31:32  заявка 3217747

09/25/14 14:31:33  заявка 3217747

09/25/14 14:31:33    300.68,302.2

09/25/14 14:31:34  заявка 3217747

              09/25/14 14:31:34    300.68,302.19

Как быть? Продолжать отглючивать (отлаживать).  Воспользуемся тем же методом, что и ранее.  Возможно,  OnTransReply так же приходит после обновления стакана. А может, ошибка и в самом обработчике OnTransReply. Что бы это выяснить, добавляем туда логирование:

--обработка события транзакции

function OnTransReply(trans_reply)

      p_file:write(os.date().." OnTransReply\n")

      nord=trans_reply["order_num"] --Номер заявки

      p_file:write(os.date().."nord="..nord.."\n")

      if nord==nil or nord==0 or nord=="0" then

            message("Заявка не выставилась",1)

            return

      end

      p_file:write(os.date().."сейчас будет проверка условия"..trans_reply["sec_code"].."   "..trans_reply["class_code"].."\n")

      if trans_reply["sec_code"]==p_seccode and trans_reply["class_code"]==p_classcode then

            p_file:write("Сейчас запустим remember_order\n")

            remember_order(trans_reply)

      end

end

Логирование показало, что OnTransReply не запускается вообще. Обычно в таком случае имеет смыл задать вопрос на форуме по qlua (http://www.quik.ru/forum/lua/), хотя ответа иногда приходиться ждать несколько часов или дней. Я задал вопрос и получил ответ, что для заявок, введенных вручную, OnTransReply не работает.

Вообще, если вы не планируете вводить заявки вручную во время работы робота, то данная особенность qlua не критична, поэтому сейчас мы не будет останавливаться на решении этой проблемы, а попытаемся протестировать нашего еще не до конца написанного робота вводя заявки программно.  Для этих целей мы можем запустить еще один скрипт, который вводит заявки. Можно воспользоваться тем скрпитом для ввода единичной заявки, что мы писали на первом уроке (http://robostroy.ru/community/article.aspx?id=773), только не забудьте поставить наш инструмент и нужные цены.

  

Теперь OnTransReply запускается, но неверная цена все равно проскакивает:

09/29/14 12:01:23    304.56,304.78

09/29/14 12:01:48    304.56,304.78

09/29/14 12:01:48    304.56,304.77

09/29/14 12:01:49    304.56,304.77

09/29/14 12:01:49    304.56,304.76

09/29/14 12:01:49    304.56,304.76

09/29/14 12:01:51    304.56,304.76

09/29/14 12:01:51    304.56,304.76

09/29/14 12:01:52    304.56,304.76

09/29/14 12:01:52    304.56,304.76

09/29/14 12:02:09 OnTransReply

09/29/14 12:02:09nord=1848863

09/29/14 12:02:09сейчас будет проверка условияPOLY   TQBR

Сейчас запустим remember_order

09/29/14 12:02:09  заявка 1848863

09/29/14 12:02:09    304.61,304.76

09/29/14 12:02:09 OnOrder

09/29/14 12:02:09  заявка 1848863

09/29/14 12:02:09 OnOrder

09/29/14 12:02:09  заявка 1848863

Чтобы с этим разобраться, придется добавить логирования и в remember_order, из OnTransReply логирование можно убрать. Что мы выясняем из лога:

09/29/14 12:37:22    304.13,305.84

09/29/14 12:37:22    304.13,305.84

09/29/14 12:37:23    304.13,305.84

09/29/14 12:37:23    304.13,305.84

09/29/14 12:37:24    304.13,305.84

09/29/14 12:37:24    304.13,305.84

09/29/14 12:37:24    304.13,305.84

09/29/14 12:37:25    304.13,305.84

09/29/14 12:37:25    304.13,305.84

09/29/14 12:37:26    304.13,305.84

09/29/14 12:37:26 OnTransReply

09/29/14 12:37:26 remember_order:  заявка 2451199

09/29/14 12:37:26 remember_order:  заявка активна

09/29/14 12:37:26 remember_order:  заявка на покупку

09/29/14 12:37:26 remember_order:  buy_price=305(!!!!!)

09/29/14 12:37:26    305.01(!!!!!),305.84

09/29/14 12:37:26 OnOrder

09/29/14 12:37:26 remember_order:  заявка 2451199

09/29/14 12:37:26 remember_order:  заявка активна

09/29/14 12:37:26 remember_order:  заявка на покупку

09/29/14 12:37:26 remember_order:  buy_price=305

09/29/14 12:37:26 OnOrder

09/29/14 12:37:26 remember_order:  заявка 2451199

09/29/14 12:37:26 remember_order:  заявка активна

09/29/14 12:37:26 remember_order:  заявка на покупку

09/29/14 12:37:26 remember_order:  buy_price=305

09/29/14 12:37:27 remember_order:  заявка 2451199

09/29/14 12:37:27 remember_order:  заявка активна

09/29/14 12:37:27 remember_order:  заявка на покупку

09/29/14 12:37:27 remember_order:  buy_price=305

09/29/14 12:37:27    304.13,305.84

09/29/14 12:37:27 remember_order:  заявка 2451199

А выясняем мы то, что заявка качественно запоминается (обратите внимание на пометки в логе). Но, тем не менее, цена почему то на один тик становиться неправильной. Может, OnTransReply тоже позже, чем OnQuote приходит? Добавим в OnQuote логирование.

И тут получается интересное «кино».  Цены остаются правильные, вот, пожалуйста, смотрите лог:

09/29/14 12:43:06    303.72,303.89

09/29/14 12:43:20 OnQuote

09/29/14 12:43:20    303.72,303.89

09/29/14 12:43:22 OnTransReply

09/29/14 12:43:22 remember_order:  заявка 2553806

09/29/14 12:43:22 remember_order:  заявка активна

09/29/14 12:43:22 remember_order:  заявка на покупку

09/29/14 12:43:22 remember_order:  buy_price=303.8

09/29/14 12:43:22 OnOrder

09/29/14 12:43:22 remember_order:  заявка 2553806

09/29/14 12:43:22 remember_order:  заявка активна

09/29/14 12:43:22 remember_order:  заявка на покупку

09/29/14 12:43:22 remember_order:  buy_price=303.8

09/29/14 12:43:22 OnOrder

09/29/14 12:43:22 remember_order:  заявка 2553806

09/29/14 12:43:22 remember_order:  заявка активна

09/29/14 12:43:22 remember_order:  заявка на покупку

09/29/14 12:43:22 remember_order:  buy_price=303.8

09/29/14 12:43:22 OnQuote

09/29/14 12:43:22 remember_order:  заявка 2553806

09/29/14 12:43:22 remember_order:  заявка активна

09/29/14 12:43:22 remember_order:  заявка на покупку

09/29/14 12:43:22 remember_order:  buy_price=303.8

09/29/14 12:43:22    303.72,303.89

09/29/14 12:43:22 OnQuote

09/29/14 12:43:22 remember_order:  заявка 2553806

09/29/14 12:43:22 remember_order:  заявка активна

09/29/14 12:43:22 remember_order:  заявка на покупку

09/29/14 12:43:22 remember_order:  buy_price=303.8

09/29/14 12:43:22    303.72,303.88

Иными словами, мы ввели заявку по цене 303.8, но нижняя крайняя цена осталась 303.72, что  правильно.

Спрашивается, почему так? Видимо, логирование создает задержку. 

А идея! Давайте в OnQuote просто сделаем задержку. Посмотрим, что получится. Лишнее логирование тогда тоже уберем.

Сделать задержку можно командой sleep:

function OnQuote(class_code, sec_code)

      if class_code==p_classcode and sec_code==p_seccode then

     

            sleep(100)

Время задержки я поставил 0.1, этого хватило. Но вообще, его следует выбирать эмпирическим путем – если работает с меньшей задержкой – ставьте меньше, что бы робот не «тормозил» систему.

И, прежде чем я закончу сегодняшний большой урок, решим еще одну проблему: робот начинает работать только с первым изменением стакана. Это проблема решается очень просто: принудительным вызовом OnQuote в функции main:

function main()

      p_file:write(os.date().." main\n")

      OnQuote(p_classcode, p_seccode)

      while is_run do

            sleep(2000)

      end

end

На этом я заканчиваю урок, конечный вариант написанной на этом уроке программы см.  в приложении. 

Прикрепленные файлы

·   Приложение к уроку 5.rar

Комментарии

asas55555 — 16 октября 2014 г.

что- то приложения нет опять :-(

0 +

orekton — 17 октября 2014 г.

asas55555, приложил

0 +

gena37 — 17 октября 2014 г.

Как правило на краю спреда стоят очень мелкие заявки. Как организовать просмотр программой всего стакана и выбор строки с достаточно крупной заявкой, чтобы поставить свою перед ней и так чтобы до своей заявки было не более определенного объема (мелких может быть много и в сумме они могут оказаться и больше крупной)?

0 +

megabax — 20 октября 2014 г.

gena37, Как перебрать весь стакан вот пример http://robostroy.ru/community/article.aspx?id=778

0 +

gena37 — 21 октября 2014 г.

megabax, спасибо, получилось организовать анализ всего стакана!

0 +

thejobber — 19 октября 2014 г.

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

0 +

thejobber — 19 октября 2014 г.

не всё так грусно с отладкой ))

https://www.youtube.com/watch?v=vzlqT89Q8wY

0 +

megabax — 20 октября 2014 г.

thejobber, спасибо за ролик, только вот интересно, где взят эту программу?

0 +

asas55555 — 20 октября 2014 г.

megabax,вот тут например http://unknownworlds.com/decoda/download/

0 +

megabax — 21 октября 2014 г.

asas55555, ок, спасибо

0 +

gena37 — 22 октября 2014 г.

Возник вопрос: как преобразовать (tb.bid[i].quantity) в целое число или хотя бы вообще в число, иначе программа иногда (почему-то не всегда) считает его "не числом" и выдает ошибочный результат при проходе через блок с IF.

0 +

asas55555 — 22 октября 2014 г.

gena37, если не ошибаюсь в 4 уроке было вот это :
<Так же у вас может возникнуть вопрос по math.ceil. Дело в том, что tb.bid_count представляет собой не целое число, а дробное, хотя и округленного до целого. В общем, просто пока запомните, что в программировании 10 и 10.0 не одно и то же, хотя они и равны. В поле tb.bid_count как раз содержится число, подобное второму, и при попытке обратится вот так tb.bid[(tb.bid_count] вылазит сообщение об ошибке. Функция math.ceil как раз преобразует число к первому виду.>

0 +

gena37 — 22 октября 2014 г.

И еще не могу разобраться с использованием tostring() - когда его необходимо использовать?

0 +

asas55555 — 22 октября 2014 г.

gena37,
tostring из справки
Принимает параметр любого типа и конвертирует его в строку в подходящем формате. Если требуется специальное форматирование, используйте функцию string.format.
Если метатаблица для e имеет поле "__tostring", tostring вызывает его значение с параметром e, и возвращает результат этого вызова.

0 +

megabax — 24 октября 2014 г.

gena37, Тогда, когда нам надо из какого то другого типа преобразовать в строку. Например, при message. Вообще, иногда преобразование происходит автоматом, но не всегда, и не всегда правильно. Так что лучше все таки применять tostring(), когда нам надо произвести строковую операцию с не строковым типом или когда мы не знаем точно, что это ха тип.

0 +

asas55555 — 22 октября 2014 г.

gena37, если не ошибаюсь в 4 уроке было вот это :
<Так же у вас может возникнуть вопрос по math.ceil. Дело в том, что tb.bid_count представляет собой не целое число, а дробное, хотя и округленного до целого. В общем, просто пока запомните, что в программировании 10 и 10.0 не одно и то же, хотя они и равны. В поле tb.bid_count как раз содержится число, подобное второму, и при попытке обратится вот так tb.bid[(tb.bid_count] вылазит сообщение об ошибке. Функция math.ceil как раз преобразует число к первому виду.> или так наверно tonumber(tb.bid[math.ceil(tb.bid_count)].quantity)

0 +

gena37 — 30 октября 2014 г.

Как вызвать значение текущей теоретической цены опциона с доски опционов для последующего использования в расчетах в программе?

0 +

megabax — 4 ноября 2014 г.

gena37, Нет пока такой возможности. Разработчик Quik-а обещали сделать, так что ждем-с.
Хотя, в принципе, значение теоретической цены можно рассчитать по формулам.

0 +

Rich74 — 17 ноября 2014 г.

Такой вопрос: а не может ли задержка между OnOrder и OnQuote происходить изза Sleep(2000) в теле цикла main? Как я понимаю поток то тут один и события не перехватывают управление пока поток спит?

0 +

megabax — 19 ноября 2014 г.

Rich74, Вообще то для каждого обработчика должен быть свой поток. Когда вызывается OnOrder или OnQuote функция main прерыывается.

0 +

Rich74 — 20 ноября 2014 г.

megabax, я вот тут корошее объяснение нашел http://quik2dde.ru/viewtopic.php?id=16

0 +

megabax — 21 ноября 2014 г.

Rich74, Да, хорошее разъяснение, будет полезно как новичкам так и профессионалам.

0 +

Николай Камынин — 18 ноября 2014 г.

Rich74,
колбеки OnOrder и OnQuote вызываются в основном потоке КВИК, а функция main - это отдельный поток скрипта.
Поэтому Sleep(2000) не влияет на вызов OnOrder и OnQuote.
Даже наоборот, освобождает время потока скрипта, если main бездействует.

0 +

Николай Камынин — 18 ноября 2014 г.

asas55555,
Вот эта фраза из 4 урока :
<... что в программировании 10 и 10.0 не одно и то же, хотя они и равны. >
Она ошибочна для LUA.
В LUA они не только равны,
но и одно и тоже,
потому,
что в LUA нет формата целых чисел.

0 +

megabax — 19 ноября 2014 г.

Николай Камынин, как показывает практика, это именно так. 10 и 10.0 оказываться не равны друг другу. Возможно, это и противоречат официальной парадигме lua, но, тем не менее, в Квике скрипты ведут себя именно так.

0 +

megabax — 19 ноября 2014 г.

megabax, Иными словами, при сравнении 1 и 1.0 компьютер думает, что эти числа не равны. Хотя для человека они и равны.

0 +

Николай Камынин — 18 ноября 2014 г.

и еще...
Для чисел фраза "они равны, но не одно и тоже" - не имеет смысла (либо имеет , но тайный).
Числа либо равны либо нет, тогда либо больше либо меньше.
других свойств для сравнения у чисел нет .
Даже при существовании целого формата, сравниваются числа всегда в одном и том же формате.

0 +

megabax — 19 ноября 2014 г.

Николай Камынин, В программировании не всегда соблюдаться законы математики. Там 2+2 вполне может получиться 3.99999999999999 или 4.0000000000000000001.

0 +

Николай Камынин — 18 ноября 2014 г.

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

-1 +

megabax — 19 ноября 2014 г.

Николай Камынин, Какие суждение вы считаете ошибочными? Давайте разберемся.

0 +

Nemo_2000 — 25 декабря 2014 г.

День добрый,

Недавно я занялся написанием бота на qlua. Столкнулся с проблемой, что при использовании колбека OnQuote для получения котировок, их анализа на выполнение условия открытия позиции, и вызова функции открывающей позицию, возникает ситуация, когда квик успевает получить 5-6 котировок до момента завершения открытия позы по первой полученной. То есть , до момента получения информации в терминале об успешном открытии позы , на сервер успевает вылететь ещё несколько заявок. Пока я как то справился с этим только введя шаг получения котировок, и рассматривая лишь каждую десятую... но всё равно при высокой волатильности, иногда проскакивают две заявки. Не подскажите, как это можно исправить?
Спасибо.

0 +

Lomonosov — 9 октября 2015 г.

перестал работать скрипт с 1 части. Пишет ошибку Syntax error while compiling "путь скрипта латиницей: путь скрипта латиницей":1: unexpected symbol near 'п'. Удалял, писал заново - всё одно и то же.

0 +

Lomonosov — 9 октября 2015 г.

забавно! скопировал код в новый файл с название _1 и заработало =) прям жуть!

0 +

Написать комментарий

Чтобы написать комментарий, необходимо авторизоваться.

Написать администратору