Блог им. jatotrade_com

КВИК-->Lua-->Python. Трансляция данных из КВИКа в Питон в реальном времени

Всех с пятницей — самоизолятницей!
Представляю общественности Python-сервер (в 9 строк кода) для получения данных из КВИКа в Питон через луа-скрипт в режиме реального времени.
Для примера приведу получение тиковых данных по SIM0.
Нам понадобятся следующие ингредиенты.
1. Понятное дело КВИК, версии ниже 8 или 8.5.2 и выше.
2. Питон Jupyter Notebook (Anaconda 3)
3. Луа-скрипт, взятый из Jatotrader (в нем буквально изменено пару строк)
Как работает сервер можно посмотреть в этом видео (1 мин. 38 сек.) Ну и по правилам хорошего тона, естественно сам текст ниже.

Для установки сервера, скачайте папку PythonServer. Если у вас был ранее установлен Jatotrader 2.9.4, то в папку КВИКа достаточно скопировать
только луа-скрипт QuikLuaPython.lua Если вы не пользуетесь Джато, то скопируйте в папку КВИКа содержимое папки QUIK8.5.2 (или QUIK7).
А файл Python_QUIK_Server.ipynb загрузите в Jupyter Notebook. Получите две ячейки. Первая — сервер с парсером. Вторая — аниматор.
Порядок запуска следующий.
1. Запускаем сервер (CTRL+Enter в первой ячейке Jupyter Notebook)
############################################ ©2020 by Evgeny Shibaev ###################################################
#1.Запускаем сервер (эту ячейку) CTRL+ENTER
#2.В КВИКе запускаем луа-скрипт QuikLuaPython.lua
import socket
import threading

ticks=[]
def parser (parse):
    if parse[0] == '1' and parse[1] == 'RIM0': #записываем цену текущего тика SiM0 в список ticks
        ticks.append(float(parse[4]))

def service():
    sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    sock.bind(('127.0.0.1',3587)) #Хост-этот компьютер, порт - 3587
    while True:
        res = sock.recv(2048).decode('utf-8')
        if res == '<qstp>\n':  #строка приходит от клиента при остановке луа-скрипта в КВИКе
            break
        else:
            parser(res.split(' ')) #Здесь вызываете свой парсер. Для примера функция: parser (parse)
    sock.close()

#Запускаем сервер в своем потоке
t = threading.Thread(name='service', target = service)
t.start()

2. Запускаем луа-скрипт QuikLuaPython.lua в КВИКе. Первой после запуска скрипта выгружается история обезличенных сделок.В этот период могут приходить различные события, например «биржевые стаканы» или свежие обезличенные сделки.Учитывайте это при парсинге данных.
Кстати, сервер в Питоне останавливается при остановки луа-скрипта в КВИКе.

3. Запускаем аниматор (CTRL+Enter во второй ячейке Jupyter Notebook)
#3.Запускаем анимацию (эту ячейку) для проверки работы сервера
import matplotlib.pyplot as plt
import pandas as pd
import matplotlib.animation
%matplotlib notebook

fig = plt.figure()
ax = fig.add_subplot()
def update(i):
    ax.clear()
    plt.plot(ticks[-1000:]) #Отображаем последние 1000 тиков

ani = matplotlib.animation.FuncAnimation(fig, update, frames=1000, repeat=False)
plt.show()
У меня аниматор иногда запускается со второго раза. Но это не страшно...

В сокет Питона из луа-скрипта мы получаем строки, в которых элементы разделены пробелами.
Первый элемент строки — это номер события в КВИКЕ (далее описание событий с реальными примерами)

1 — Текущая обезличенная сделка (тик), Тикер, № в потоке, ID сделки, цена, 1026(1025) покупка(продажа), количество, дата, время.мс

«1 RIM0 777837 25971668199048 121660.0 1026 1.0 27.5.2020 15:49:48.122»

3 — Обезличенная сделка из истории (тик), Тикер, № в потоке, ID сделки, цена, 1026(1025) покупка(продажа), количество, дата, время.мс

«3 RIM0 762482 25971668178568 121320.0 1026 2.0 27.5.2020 15:34:54.35»

2 — Биржевой стакан, Тикер, количество бидов, цена последнего бида, количество последнего бида, цена следующего бида, количество следующего бида,... цена лучшего бида, количество лучшего бида, количество асков, цена лучшего аска, количество лучшего аска, цена следующего аска, количество следующего аска,… цена последнего аска, количество последнего аска. Т.е. стакан упорядочен по возрастанию цены.

«2 RIM0 50.000000 121190 5 121200 30 121210 10 121220 9 121230 9 121240 14 121250 28 121260 18 121270 10 121280 11 121290 11 121300 28 121310 19 121320 6 121330 42 121340 64 121350 28 121360 11 121370 34 121380 44 121390 33 121400 36 121410 106 121420 37 121430 38 121440 30 121450 74 121460 38 121470 46 121480 70 121490 83 121500 123 121510 53 121520 57 121530 109 121540 41 121550 62 121560 39 121570 43 121580 20 121590 29 121600 46 121610 30 121620 24 121630 40 121640 15 121650 33 121660 27 121670 19 121680 4 50.000000 121690 5 121700 22 121710 13 121720 16 121730 19 121740 28 121750 23 121760 34 121770 37 121780 23 121790 34 121800 116 121810 158 121820 41 121830 23 121840 27 121850 168 121860 63 121870 26 121880 24 121890 75 121900 130 121910 40 121920 52 121930 58 121940 34 121950 61 121960 78 121970 85 121980 66 121990 58 122000 177 122010 34 122020 18 122030 10 122040 22 122050 50 122060 51 122070 14 122080 4 122090 37 122100 21 122110 8 122120 16 122130 8 122140 216 122150 5 122160 29 122170 5 122180 10»

"<qsrt>" — Старт соединения

«4 888888» — количество тиков в истории на текущий момент

«5»- загрузка истории завершена

6 — заявка, Тикер, № транзакции, ID заявки, Флаг, Счет, цена, количество, объем, комментарий, время заявки
Флаги: 26 — «Снята», «Купля»; 30 — «Снята», «Продажа»; 28 — «Исполнена», «Продажа»; 24 — «Исполнена», «Купля»; и т.д.

«6 RIM0 120 25971670302591 30 „764088i“ 121190.0 1.0 1.0 171665.64 „764088i“ 23:3:51»

7 — стоп-заявка

8 — сделка, Тикер, ID сделки, ID заявки, Флаг, Счет, цена, количество, объем, комментарий, время заявки
Флаги: 64 — «Исполнена», «Купля»; 68 — «Исполнена», «Продажа»; и т.д.

«8 RIM0 25971667540438 25971671513360 68 „764096i“ 121470.0 1.0 172062.26 „764096i“ 27.5.2020 10:16:25 4»

На самом деле вы можете передавать из КВИКа любые данные, например о состоянии счета, портфеля и пр. Просто добавьте в луа-скрипт
свою функцию запаковки соответствующих данных.

Желаю всем удачи и творческих успехов, выраженных в материальном благополучии!

ЗЫ. Сам луа-скрипт: (код написан Александром(ziv) и вашим покорным слугою)
stopped = false            -- Остановка файла
socket = require("socket") -- Указатель для работы с sockets
IPAddr = "127.0.0.1"       --IP Адрес
IPPort = 3587		   --IP Port	 
sender = nil		   --Укзатель на коннектор
send = nil		   --Указатель на процедуру отправки сообщения
connect = nil		   --Указатель на процедуру подключения к серверу
all_trd_indx = 0	   --Текущий индекс для переданных записей таблицы всех сделок
all_ord_indx = 0
all_stop_ord_indx = 0
all_my_trades_indx = 0
inited = false		   --Для проверки вызова OnAllTrade перед main

-- Функция вызывается перед вызовом main
function OnInit(path)
--  sender = assert(socket.connect(IPAddr, IPPort));
  sender = socket.udp()
  sender:setpeername(IPAddr, IPPort)
  --Выводим сообщение в квике, что есть подключение
  message(string.format("Connection success. IP: %s; Port: %d\n", IPAddr, IPPort), 1);
  sender:send("<qsrt>\n");
end;

-- Функция вызывается перед остановкой скрипта
function OnStop(signal)
  sender:send("<qstp>\n");
  stopped = true; -- Остановили исполнение кода 
end;

-- Функция вызывается перед закрытием квика
function OnClose()
  sender:send("<qcls>\n");
  stopped = true; -- закрыли квик, надо остановить исполнение кода
end;

-- Функция вызвается при изменении стакана

function OnQuote(class, seccode)
  if stopped then return; end
  local sec = getSecurityInfo(class, seccode);
  local price = 0;
  local level2 = getQuoteLevel2(class, seccode); --Получаем стакан
  --сначала загружаем бид, потом аск
  local bidstr = " "..level2["bid_count"]
  if type(level2["bid"]) == "table" then
  	for index, bid in ipairs(level2["bid"]) do
          price = bid["price"]
  	  bidstr=bidstr.." "..price.." "..bid["quantity"]
  	end
  end;
  local askstr = " "..level2["offer_count"]
  if type(level2["offer"]) == "table" then 
    for index, ask in ipairs(level2["offer"]) do
          price = ask["price"]
	  askstr=askstr.." "..price.." "..ask["quantity"]
    end
  end;
  local str = "2 "..seccode..bidstr..askstr.."\n";
  sender:send(str)
end;

--User trades functions
function formatmytrade (status, trade)
  all_my_trades_indx = all_my_trades_indx + 1
  return status.." "..trade["sec_code"].." "..trade["trade_num"].." "..trade["order_num"].." "..trade["flags"].." \""..trade["account"].."\" "..trade["price"].." "..trade["qty"].." "..trade["value"].." \""..trade["brokerref"].."\" "..trade["datetime"]["day"].."."..trade["datetime"]["month"].."."..trade["datetime"]["year"].." "..trade["datetime"]["hour"]..":"..trade["datetime"]["min"]..":"..trade["datetime"]["sec"].." "..trade["trans_id"].."\n"
end;

function OnTrade(trade)
  if (not inited) or (stopped) then return; end;
  sender:send (formatmytrade (8, trade));
end;

function sendallmytrades()
  local count = getNumberOf("trades");
  local index = 0
  for index=0,count - 1 do
  	if stopped then return false; end;
  	sender:send (formatmytrade (8, getItem("trades", index)));
  end;
  return true;
end;

--User stop orders functions
function formatstoporder (status, order)
  all_stop_ord_indx = all_stop_ord_indx + 1
  return status.." "..order["sec_code"].." "..order["trans_id"].." "..order["order_num"].." "..order["flags"].." \""..order["account"].."\" "..order["price"].." "..order["condition_price"].." "..order["qty"].." "..order["balance"].." \""..order["brokerref"].."\" "..order["ordertime"].."\n"
end;

function OnStopOrder (order)
  if (not inited) or (stopped) then return; end;
  sender:send (formatstoporder (7, order));
end;

function sendallstoporders()
  local count = getNumberOf ("stop_orders");
  local index = 0
  for index=0,count - 1 do
  	if stopped then return false; end;
  	sender:send (formatstoporder (7, getItem ("stop_orders", index)));
  end;
  return true;
end;

function formatorder (status, order)
  all_ord_indx = all_ord_indx + 1
  return status.." "..order["sec_code"].." "..order["trans_id"].." "..order["order_num"].." "..order["flags"].." \""..order["account"].."\" "..order["price"].." "..order["qty"].." "..order["balance"].." "..order["value"].." \""..order["brokerref"].."\" "..order["datetime"]["hour"]..":"..order["datetime"]["min"]..":"..order["datetime"]["sec"].."\n"
end;

function OnOrder(order)
  if (not inited) or (stopped) then return; end;
  sender:send (formatorder (6, order));
end;

function sendallorders()
  local count = getNumberOf("orders");
  local index = 0
  for index=0,count - 1 do
  	if stopped then return false; end;
  	sender:send (formatorder (6, getItem("orders", index)));
  end;
  return true;
end;

function OnAllTrade(trade)
  if (not inited) or (stopped) then return; end;
  --Отправляем сделку
  sender:send(formattrade1(1, trade));
end;

function formattrade1(status, trade)
  all_trd_indx = all_trd_indx + 1 -- Увеличиваем счетчик кол-ва 
  --Формируем запись для передачи
  return status.." "..trade["sec_code"].." "..all_trd_indx.." "..trade["trade_num"].." "..trade["price"].." "..trade["flags"].." "..trade["qty"].." "..trade["datetime"]["day"].."."..trade["datetime"]["month"].."."..trade["datetime"]["year"].." "..trade["datetime"]["hour"]..":"..trade["datetime"]["min"]..":"..trade["datetime"]["sec"].."."..trade["datetime"]["ms"].."\n"
--[[  trade_num NUMBER  Идентификатор сделки
flags NUMBER  Набор битовых флагов
price NUMBER  Цена
qty NUMBER  Количество
value NUMBER  Объем сделки
accruedint  NUMBER  Накопленный купонный доход
yield NUMBER  Доходность
settlecode  STRING  Код расчетов
reporate  NUMBER  Ставка РЕПО
repovalue NUMBER  Сумма РЕПО
repo2value  NUMBER  Объем сделки выкупа РЕПО
repoterm  NUMBER  Срок РЕПО в днях
sec_code  STRING  Код инструмента
class_code  STRING  Код класса
datetime  TABLE Дата и время
]]
end;

--Загрузка обезличенных сделок
function sendalltrades()
  local count = getNumberOf("all_trades");
  sender:send("4 "..count.."\n");
  local index = 0
  for index=0,count - 1 do
  	if stopped then return false; end;
  	sender:send(formattrade1(3, getItem("all_trades", index)));
  end; 
  sender:send("5\n");
  return true;
end;

-- Основная функция выполнения скрипта
function main()
  inited = true
  if not stopped then
        local start_time = os.clock()
        if sendalltrades() then
           message("Send all trades history success: " .. tonumber(os.clock() - start_time), 1);
           sendallorders()
           sendallstoporders()
           sendallmytrades()
 	   while not stopped do
  	          sleep(1);
  	    end; --while
  	  end; --if  
  end; --if
  sender:send('<qbye>\n')
  sender:close() --закрываем соединение
  sender=nil
end;


  • обсудить на форуме:
  • Quik Lua
★70
42 комментария

Вау, че-то прикольное! Спасибо. Я правда ещё не читал, точечно глянул — очень интересно. Чуть позже почитаю подробней.

А нету, случайно, в заначке решения чтобы так же из питона можно было заявки отправлять... 

avatar
Replikant_mih, да сделаю, не проблема. Через тот же сокет и отправим.

Евгений Шибаев, Посмотрел, первое впечатление не обмануло — очень крутая штука для меня! Запустилось).

Пойду попробую научить луа скрипт упаковывать послания в словари))). Хотя в луа вообще не шарю.

Ну и очень буду ждать пост про то, как с заявками работать.

avatar
Replikant_mih, да, кстати все можно в луа упаковывать и не только в словари но и в объекты датафрейма панды — только сначала посчитать где упаковка-парсинг выгоднее — на чьей стороне…
Проблема с ключом инициации ( его не записью в папку Jatotrader-а) решается как то?
avatar
YuryDok, а помню с вами долго разбирались, по-моему из-за Каспера. Я сделаю следующую сборку чтобы можно было по-другому ключ поставить.
Пробовал как-то установить сокеты под Луа через их установщик (забыл как его кличут). Ничего не установилось, да и сам установщик как-то криво встал.
Снес установщик, и завязал с этими экспериментами. А сокеты через Луа ->DLL делал. Но, тоже не глянулись. Перед сокетами надо данные упаковывать, а после них много парсинга. А если еще инфа разнородная, ее еще разделять и расставлять по полкам замотаешься.
avatar
3Qu, да, с dll гемора много, но работает идеально.
если параметры функций и их сами не менять, то имхо самое то…

в lua еще есть функционал для вызова функций из внешних .dll
я набросал по аналогии несколько функций, в которые и сгружал все данные… и где-то за 2-3 года почти непрерывной работы этот самопальный экспорт из lua ни разу не сглючил… единственное что омрачает, так это подозрение что скоро 32-bit квик прикроют...

А как у вас статистика по соккетам? отрубаются? глючат? память жрут?

Niktesla (бывш. Бабёр-Енот), вообще ни разу с 2015-го сокеты не глючили. Если не считать перехода АРКИ на 8-й Квик. Вот после версии 8.5.2 луа у них стал стабильно работать. При этом нагрузка у меня серьезная, одних только тиков порядка 3млн за сессию собирается, не говоря уже о стаканах
Евгений Шибаев, добрый день! А где храните на sql сервере или как?) И почему из Квика качаете тики?
avatar
GoodBargains, добрый вечер, это я про онлайн тики которые из Квика для обработки на лету. А так хранить можно где угодно например в питонской SQLite.
Евгений Шибаев, крутейшая программа у Вас получилась!!! Даже не представляю сколько часов и лет вы посвятили ей. Что реально на ЛИСП такое можно делать? Или она на Питоне написана?
avatar
GoodBargains, на Лиспе. На Питоне будет оч медленно
А что насчет заявок?
avatar
LogikoMen, «да сделаю, не проблема. Через тот же сокет и отправим» ответ на первый комент
Евгений Шибаев, сделать заявку, проверить ее исполнение, убедиться что это именно твоя заявка исполнилась, а не другого торгового робота. Создать алгоритм, который бы не выставлял заявку по упущенной цене. Потому что бар прилетел давно. Квик имеет свойство фривольно отвечать на запросы.
Это самое главное в системе. Очень много проблем с этим. С этого стоит начинать.
avatar
LogikoMen, все это реализовано в Jatotrader. В следующем топике постараюсь изложить отправку транзакций из Питона в Квик. А насчет того 
сделать заявку, проверить ее исполнение, убедиться что это именно твоя заявка исполнилась, а не другого торгового робота
здесь же существует механизм выставления заявок со своим ID транзакции. По ID транзакции все это и  отслеживается, а именно исполнилась ли данная заявка полностью или частично, снята или отвергнута торговой системой. И никакой путаницы не возникает.
Создать алгоритм, который бы не выставлял заявку по упущенной цене. Потому что бар прилетел давно.
Вот это не понял, что имели ввиду.
Евгений Шибаев, привествую. Вопрос, нужен скрипт, который просто периодически будет выгружать состав портфеля в табличку. Т.е. по итогу должна формироваться таблица с текущими позициями, и точками входа. Если возможно, круто еще добавлять тейки и стопы. Возможно ли это, и сколько будет стоить? Связь через личку
avatar
Гениально.
Вы случайно не знаете — сокеты Луа совместимы с сокетами С# или C++?
avatar
_sg_, не знаю, но предполагаю, что все писано нативно на сях, посему все должно работать «как часики» ©ВВП
Евгений Шибаев,
у меня с Питоном сразу не завелись «часики».
Правда, долго не копал почему.
avatar
Евгений Шибаев, сокеты -объекты операционной системы. они не должны зависть от конкретного языка
avatar
Евгений Шибаев, 
не думали,
может быть лучше будет разделить на потоки, где каждая сущность пишется в свой сокет?
Или хотя бы AllTrades и Стаканы отделить от всего остального.
И еще можно в принципе там где много элементов передается (AllTrades и Стаканы) передавать не string,  а string[]
avatar
_sg_, да, абсолютно верно, я в боевой у себя так и делаю, а это ж для примера
_sg_, Зачем каждый поток пихать в отдельный сокет? Данные передаются по одному сокету нормально.
Александр, вот зачем
Евгений Шибаев
_sg_, да, абсолютно верно, я в боевой у себя так и делаю, а это ж для примера
avatar
_sg_, Сокеты, они и в Aфрике сокеты, везде буду работать.
Александр, я тоже так думаю
avatar
Александр,
только что обратил внимание — Протокол UDP в данном случае
А по TCP — протоколу сокеты Lua работают ?

avatar
_sg_, да, работают, например в Jatotrader там по потоковому сокету передача идет (не по datagram)
Гениально! Коты одобряют Python. 

Сделал по инструкции, Quik 8.5.2.11 выдает:

\QuikLuaPython.lua:3: module 'json' not found:
no field package.preload['json']
no file 'mypath\lua\json.lua'
no file 'mypath\lua\json\init.lua'
no file 'mypath\json.lua'
no file 'mypath\json\init.lua'
no file 'mypath\..\share\lua\5.3\json.lua'
no file 'mypath\..\share\lua\5.3\json\init.lua'
no file '.\json.lua'
no file '.\json\init.lua'
no file 'mypath\json.dll'
no file 'mypath\..\lib\lua\5.3\json.dll'
no file 'mypath\loadall.dll'
no file '.\json.dll'

 

Где можно найти недостающие файлы?

avatar
Nikolay D, вам нужно в Квик в папку lua добавить файл json.lua. Взять его можно отсюда https://yadi.sk/d/7r_d03W0cPiw4g
Извиняюсь, забыл положить — сейчас все ок

Евгений Шибаев, 

Спасибо, работает. (плюсануть не хватает кармы)

avatar
Nikolay D, плюсану в вашу
Евгений Шибаев, сервер может тянуть данные с Квика только по фьючерсам или и по акциям тоже? 
avatar
Monster, по всему что торгуется
avatar
Евгений, доброго времени суток!
Спасибо большое!)

У заявок есть Флаги в зависимости от состояния. Не подскажите, где можно посмотреть какие флаги, каким статусам соответствуют?
avatar
доброго времени суток!.. А как сделать чтобы lua был инициатором вызова функции в pyton и pyton отправил бы ответ если его вызвали из lua(
надеюсь понятно выразился))
avatar
Вопрос: Обязательно запускать в Jupyter Notebook? 
avatar

теги блога Евгений Шибаев

....все тэги



UPDONW
Новый дизайн