Евгений Шибаев
Евгений Шибаев личный блог
29 мая 2020, 19:57

КВИК-->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;


42 Комментария
  • Replikant_mih
    29 мая 2020, 20:03

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

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

      • Replikant_mih
        29 мая 2020, 22:27

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

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

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

  • YuryDok
    29 мая 2020, 20:05
    Проблема с ключом инициации ( его не записью в папку Jatotrader-а) решается как то?
  • 3Qu
    29 мая 2020, 20:09
    Пробовал как-то установить сокеты под Луа через их установщик (забыл как его кличут). Ничего не установилось, да и сам установщик как-то криво встал.
    Снес установщик, и завязал с этими экспериментами. А сокеты через Луа ->DLL делал. Но, тоже не глянулись. Перед сокетами надо данные упаковывать, а после них много парсинга. А если еще инфа разнородная, ее еще разделять и расставлять по полкам замотаешься.
    • 3Qu, да, с dll гемора много, но работает идеально.
      если параметры функций и их сами не менять, то имхо самое то…
  • в lua еще есть функционал для вызова функций из внешних .dll
    я набросал по аналогии несколько функций, в которые и сгружал все данные… и где-то за 2-3 года почти непрерывной работы этот самопальный экспорт из lua ни разу не сглючил… единственное что омрачает, так это подозрение что скоро 32-bit квик прикроют...

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

      • GoodBargains
        01 июня 2022, 13:19
        Евгений Шибаев, добрый день! А где храните на sql сервере или как?) И почему из Квика качаете тики?
          • GoodBargains
            01 июня 2022, 23:54
            Евгений Шибаев, крутейшая программа у Вас получилась!!! Даже не представляю сколько часов и лет вы посвятили ей. Что реально на ЛИСП такое можно делать? Или она на Питоне написана?
  • LogikoMen
    29 мая 2020, 20:20
    А что насчет заявок?
      • LogikoMen
        31 мая 2020, 15:07
        Евгений Шибаев, сделать заявку, проверить ее исполнение, убедиться что это именно твоя заявка исполнилась, а не другого торгового робота. Создать алгоритм, который бы не выставлял заявку по упущенной цене. Потому что бар прилетел давно. Квик имеет свойство фривольно отвечать на запросы.
        Это самое главное в системе. Очень много проблем с этим. С этого стоит начинать.
          • Андрей
            05 апреля 2021, 08:42
            Евгений Шибаев, привествую. Вопрос, нужен скрипт, который просто периодически будет выгружать состав портфеля в табличку. Т.е. по итогу должна формироваться таблица с текущими позициями, и точками входа. Если возможно, круто еще добавлять тейки и стопы. Возможно ли это, и сколько будет стоить? Связь через личку
  • _sg_
    29 мая 2020, 20:34
    Гениально.
    Вы случайно не знаете — сокеты Луа совместимы с сокетами С# или C++?
      • _sg_
        29 мая 2020, 20:40
        Евгений Шибаев,
        у меня с Питоном сразу не завелись «часики».
        Правда, долго не копал почему.
      • Gregori
        29 мая 2020, 21:06
        Евгений Шибаев, сокеты -объекты операционной системы. они не должны зависть от конкретного языка
      • _sg_
        29 мая 2020, 21:11
        Евгений Шибаев, 
        не думали,
        может быть лучше будет разделить на потоки, где каждая сущность пишется в свой сокет?
        Или хотя бы AllTrades и Стаканы отделить от всего остального.
        И еще можно в принципе там где много элементов передается (AllTrades и Стаканы) передавать не string,  а string[]
        • Александр
          29 мая 2020, 22:26
          _sg_, Зачем каждый поток пихать в отдельный сокет? Данные передаются по одному сокету нормально.
          • _sg_
            29 мая 2020, 22:53
            Александр, вот зачем
            Евгений Шибаев
            _sg_, да, абсолютно верно, я в боевой у себя так и делаю, а это ж для примера
    • Александр
      29 мая 2020, 20:52
      _sg_, Сокеты, они и в Aфрике сокеты, везде буду работать.
      • _sg_
        29 мая 2020, 21:06
        Александр, я тоже так думаю
      • _sg_
        29 мая 2020, 22:51
        Александр,
        только что обратил внимание — Протокол UDP в данном случае
        А по TCP — протоколу сокеты Lua работают ?

  • Манул Кот
    30 мая 2020, 03:47
    Гениально! Коты одобряют Python. 
  • Nikolay D
    11 июня 2020, 18:59

    Сделал по инструкции, 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'

     

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

      • Nikolay D
        11 июня 2020, 23:17

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

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

          • Monster
            12 февраля 2022, 21:21
            Евгений Шибаев, сервер может тянуть данные с Квика только по фьючерсам или и по акциям тоже? 
  • Сергей Пияшов
    04 февраля 2021, 23:11
    Евгений, доброго времени суток!
    Спасибо большое!)

    У заявок есть Флаги в зависимости от состояния. Не подскажите, где можно посмотреть какие флаги, каким статусам соответствуют?
  • sanvir
    23 июня 2022, 10:41
    доброго времени суток!.. А как сделать чтобы lua был инициатором вызова функции в pyton и pyton отправил бы ответ если его вызвали из lua(
    надеюсь понятно выразился))
  • Vladimir Diaditchev
    15 января 2023, 17:10
    Вопрос: Обязательно запускать в Jupyter Notebook? 

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

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