Евгений Шибаев
Евгений Шибаев личный блог
11 июня 2020, 18:46

КВИК-->Lua-->Python. Стакан к празднику.

Всем привет, с наступающим праздником! Который, надеюсь у большинства пройдет, как обычно, в ЖО ЗОЖе (блин, слово-то придумали).
В продолжение топика "КВИК-->Lua-->Python. Трансляция данных из КВИКа в Питон в реальном времени".
В Python-сервер добавлен парсер и визуализатор стакана. Стакан в стиле QSCALP-лайт вариант. Все как обычно в 20 строк кода.

У Тимофея гифки со сторонних сайтов не кажут. Приходится ссылку давать… Или отказываться от главной. Выбрал второе.
КВИК-->Lua-->Python. Стакан к празднику.Чтобы насладиться созерцанием стакана нам нужны следующие ингредиенты:
1. Квик версии 8.5.2 и выше.
2. Lua-скрипт QuikLuaPython.lua (собственно сокет-клиент)
3. Питон (Jupyter Notebook Anaconda 3)
4. Python_QUIK_Server.ipynb (собственно сокет-сервер)
Считаем, что Квик и Питон у вас уже установлены. Чтобы запустить трансляцию, скачайте папку PythonServer в ней вы найдете все необходимое. Файл Python_QUIK_Server.ipynb поместите в папку Питона (чтобы его видел Jupyter Notebook). Затем, содержимое папки QUIK8.5.2(а не саму папку!) скопируйте в папку Квика. В Квике, в меню «Сервисы»-«Луа-скрипты» добавьте луа-скрипт QuikLuaPython.lua.
Запустите сервер в питоне (CTRL+Enter в первой ячейке файла Python_QUIK_Server.ipynb). Затем запустите луа-скрипт в Квике (начнется выгрузка данных с истории обезличенных сделок). И, наконец, запустите визуализацию графика и стакана в Питоне - CTRL+Enter во второй ячейке файла Python_QUIK_Server. Если вы все сделали правильно, то появится (возможно со второго раза) картинка примерно как в начале топика.
Сервер в Питоне:
#1.Запускаем сервер (эту ячейку) CTRL+ENTER
#2.В КВИКе запускаем луа-скрипт QuikLuaPython.lua
import socket
import threading
import pandas as pd

ticker = 'BRN0' #В Квике должен быть открыт стакан BRN0, а в таблице обезличенных сделок транслироваться тики BRN0
ticks=[] #Для примера, список обезличенных сделок BRN0
stakan = '' # строка формата '"имя тикера" {bid_price:bid_size,bid_price1:bid_size1...ask_price:-ask_size,ask_price1:-ask_size1}'

def parser (res):
    parse = res.split(" ", 2) #первый элемент - идентификатор события, второй имя тикера
    if parse[0] == '2': # парсинг стакана (событие '2')
        if parse[1] == ticker:
            global stakan
            stakan = parse[2]
    if parse[0] == '1': # парсинг обезличенной сделки (событие '1')
        tail = res.split(" ")
        if tail[1] == ticker: #записываем цену текущего тика BRN0 в список ticks
            ticks.append(float(tail[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) #Здесь вызываете свой парсер. Для примера функция: parser (parse)
    sock.close()

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

Аниматор в Питоне:
#3.Запускаем отображение стакана и графика тиков (эту ячейку) CTRL+ENTER
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation
%matplotlib notebook

hi_lo = [0.0, 0.45] #здесь достаточно задать верхнюю и нижнюю границу цены стакана, чтобы сохранить заданный масштаб
max_vol = 3000 #максимальное значение шкалы объема на стакане
max_ticks = 1000 #Количество отображаемых тиков на графике
fsize = 10 #размер шрифта в стакане

fig = plt.figure(figsize=(12, 9), dpi= 80) #Определяем размер изображения
ax_stakan = fig.add_subplot(1, 4, 3) #Расположение стакана
ax_tick = fig.add_subplot(1, 2, 1) #Расположение графика сделок
ax_stakan.tick_params(left = False, labelleft = False, labelright = True, labelsize=8) #Расположение осей

#Функция для автосмещения шкал при приближении текущей цены к верхней или нижней границам стакана (20 процентов)
#point - количество знаков после запятой, например для BR - 2, а для RI, SI, SR, GZ - 0, или можно не задавать
def masht(price, point = 0):
    percent = 0.2
    hi, lo = hi_lo[1], hi_lo[0]
    rn = hi - lo
    if price < lo + rn*percent or price > hi - rn*percent:
        hi_lo[0]=round(price - 0.5*rn, point)
        hi_lo[1]=round(price + 0.5*rn, point)

#Функия обновления кадра графика
def update(i):
    if ticks:
        price = ticks[-1] #Цена последней сделки
        ob_dict = eval(stakan) #читаем из строки стакан-словарь в ob_dict
        masht(price, 2) #проверяем необходимость смещения High,Low диаграммы при достижении ценой соответствующей границы
        ax_tick.clear()
        ax_stakan.clear()
        ax_tick.grid(which='major', color = 'gray', linestyle = ':')
        ax_stakan.grid(which='major', color = 'gray', linestyle = ':')

        ax_stakan.set_ylim(hi_lo)
        ax_stakan.set_xlim([0,max_vol])
        ax_tick.set_ylim(hi_lo)
        ax_tick.set_xlim([0,max_ticks])
        
        bid_price, bid_size, ask_price, ask_size = [],[],[],[]
        #Распределяем стакан-словарь на 4 списка: bid_price, bid_size, ask_price, ask_size
        for (pr, sz) in ob_dict.items():
            if pr >= hi_lo[0] and pr <= hi_lo[1]: #отображаем только объемы в видимой области стакана
                if sz > 0:
                    bid_price.append(pr)
                    bid_size.append(sz)
                else:
                    ask_price.append(pr)
                    ask_size.append(-sz)
        x, y = np.array(bid_price), np.array(bid_size)
        x1, y1 = np.array(ask_price), np.array(ask_size)
        ax_stakan.autoscale(False, tight=False) #Отключаем автошкалу Питона, чтобы график не "прыгал" постоянно
        ax_stakan.barh(x, y, height = 0.009, color = 'seagreen', alpha=0.4) #tick_label = bid_size
        ax_stakan.barh(x1, y1, height = 0.009, color = 'lightcoral', alpha=0.4)
        for (pr, sz) in zip(bid_price[1:], bid_size[1:]): #Печать объемов
            ax_stakan.text(0, pr, sz, fontsize=fsize, va='center', color='green') #darkgreen
        for (pr, sz) in zip(ask_price[:-1], ask_size[:-1]):
            ax_stakan.text(0, pr, sz, fontsize=fsize, va='center', color='red') #darkred
        #Маркер текущей цены        
        ax_tick.text(max_ticks*1.01, price, price, fontsize=10, va='center',ha='left')
        ax_tick.plot(ticks[-max_ticks:]) #Отображаем последние 1000(max_ticks) тиков

# Включаем анимацию на 1000 кадров, раз в 400 мсек
ani = matplotlib.animation.FuncAnimation(fig, update, frames=1000, repeat=False, interval=400, blit=True)
plt.show()

Lua-скрипт для Квика:
stopped = false            -- Остановка файла
socket = require("socket") -- Указатель для работы с sockets
json = require( "json" ) -- Указатель для работы с json
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 = " {"
  if type(level2["bid"]) == "table" then
  	for index, bid in ipairs(level2["bid"]) do
  	  bidstr = bidstr..bid["price"]..":"..bid["quantity"]..","
  	end
  end;
  local askstr = ""
  if type(level2["offer"]) == "table" then 
    for index, ask in ipairs(level2["offer"]) do
	  askstr = askstr..ask["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"]["day"].."."..order["datetime"]["month"].."."..order["datetime"]["year"].." "..order["datetime"]["hour"]..":"..order["datetime"]["min"]..":"..order["datetime"]["sec"].."\n"
--  return status.." "..json.encode(order).."\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;

ВНИМАНИЕ! ИЗМЕНЕНИЯ К ПРЕДЫДУЩЕЙ ВЕРСИИ.
В луа-скрипте:
1. Стакан из Квика формируется в виде строки:
'2 «BRN0» {40.47:23,40.48:2,40.49:36,40.50:628,....40.96:536,40.98:-465,40.99:-758,41.00:-823,....41.45:-21,41.46:-25,41.47:-12}'
где — 2 — изменение «стакана», «BRN0» — имя тикера, далее словарь из пар Price:Size упорядочен по возрастанию цены. Если  Size положительный — это бид, отрицательный — аск.
2. Добавлена библиотека json (она пригодится нам в процессе управления заявками, подробно опишу в следующем топике)
Поэтому, если вы пользовались старой версией, обновите луа-скрипт и добавьте модуль json.lua

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

Все коды можно, также, найти в конце страницы загрузки Jatotrader.
Кое что интересное для извлечения денег с рынка можно посмотреть на моем канале в ютьюбе.
Всем удачи! И будьте здоровы!
41 Комментарий
  • GOLD
    11 июня 2020, 19:08
    Купил фьюч на звезды к этому посту с 6-м плечом.

    Продам на 50 звездах ★★★★★....

    Потому, что пост про LUA))
  • 3Qu
    11 июня 2020, 19:09
    Может когда и пригодится. Всегда рад видеть единомышленников.
    Но у меня другая концепция:
    — Питон для объемных вычислений,
    — для всяких стаканов/сделок есть С++.
    — для связи — Луа.
  • Rymys
    11 июня 2020, 20:01
    У Тимофея гифки со сторонних сайтов не кажут. Приходится ссылку давать...
    Пока не было комментариев, на картинке была прекрасная анимация.
    Сейчас вот это уточнение и статическое изображение.
    Странно, может совпадение ?
    А так, разумеется, спасибо!

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

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