Евгений Шибаев
Евгений Шибаев личный блог
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))
      • GOLD
        11 июня 2020, 19:17
        Евгений Шибаев, едем на север вместе))
  • 3Qu
    11 июня 2020, 19:09
    Может когда и пригодится. Всегда рад видеть единомышленников.
    Но у меня другая концепция:
    — Питон для объемных вычислений,
    — для всяких стаканов/сделок есть С++.
    — для связи — Луа.
    • 3Qu
      11 июня 2020, 19:22
      Евгений Шибаев, не знаю, я в Spyder работаю. Там все ОК. Окна делаются, в них графики, кнопки и пр радости.
        • 3Qu
          11 июня 2020, 19:28
          Евгений Шибаев, а просто наплевать и забить на этот Юпитер.?
    • /\../
      11 июня 2020, 22:24
      Евгений Шибаев, я бы попробовал перенести
      %matplotlib notebook в самое начало.
  • Rymys
    11 июня 2020, 20:01
    У Тимофея гифки со сторонних сайтов не кажут. Приходится ссылку давать...
    Пока не было комментариев, на картинке была прекрасная анимация.
    Сейчас вот это уточнение и статическое изображение.
    Странно, может совпадение ?
    А так, разумеется, спасибо!
  • Vadim Ch
    11 июня 2020, 21:30
    Полезно. Спасибо!
  • _sg_
    11 июня 2020, 21:42
    Да, хорошая работа.
    Вам большой респект и дальнейших творческих успехов.
  • Фима Гирин
    12 июня 2020, 00:24

    А где взять библиотеки для Lua:

    socket = require("socket") -- Указатель для работы с sockets
    json = require( "json" ) -- Указатель для работы с json
  • Weddy
    12 июня 2020, 05:13
    Интересно, а как эта визуалицация стакана помогает жить (зарабатывать) трейдеру?
    Сделали бы лучше что-то типа скринера для акций для Квика. Например, чтоб в таблицу можно было выбрать интересующие инструменты и по ним отображалось по каждому инструменту изменение текущей цены по отношению к цене с задаваемым условием: за неделю, месяц, или особенно здорово например от последнего хай/лой за последние хх дней/ определенной даты.
  • Weddy
    12 июня 2020, 14:04
    А по скринеру, ну что ж, на досуге сделаю специально для вас (если это поможет зарабатывать).
    Вот будет здорово! А сортировать полученные значения по возрастанию/убыванию можно будет?
    Не сочтите за наглость, а можно ли еще к таблице добавить столбец, в котором пользователь мог бы просто заносить свои примечания к инструменту в произвольном формате?
    Все это правильно было бы просить у разработчика. Но зайдя на форум Арки и полазив я увидел, что там годами висят предложения/просьбы к продукту от пользователей без всякого движения. Уж сколько лет народ просит добавить очевидные вещи типа автоматического выставления стопа/тэйк-профита по совершению сделки — все в пустоту.
      • Weddy
        15 июня 2020, 05:07
        Евгений Шибаев, 
        От вас только чуток знания питон-кода и все.
        Я не только с Питоном, я и с другими языками не знаком. Помню только в районе 1986 г на лабораторных через перфокарты на Фортран-IV вводил какие-то простейшие «программы» типа выполнения простейших арифметических  вычислений )))
        А средствами только скрипта Lua задачи построения такой таблицы не решаются?
          • Weddy
            15 июня 2020, 15:42
            Евгений Шибаев, 
            Т.к. в этом случае одно лишнее звено выпадает (питон).
            С точки пользователя это значимо. Т.к. для незнакомого с этим не надо будет запускать еще и неведомый ему сервер в питоне.
            Единственный момент, в этом случае вы сможете получать эти данные, только при подключенном Квике.
            Ну собственно вряд ли это проблема. Если таблица для Квика используется при запущенном Квике, то почему бы еще и не подключиться к серверу ))). Лишь бы не другой нюанс: видел несколько раз люди выкладывали луа-скрипты по рисованию горизонтальных объемов на графике в Квике. Почему-то по каждому из них автор указывал, что рисуется только по данным текущей сессии. Словно луа не имеет доступа к историческим данным. Что для моего дилетантского понимания странно слышать. Т.к. Квик же отображает исторические данные. Более того, он даже пишет (как я предполагаю) в папку \archive .dat файлы с данными по просматривавшимся инструментам.
            • Weddy
              22 июня 2020, 01:33
              Weddy, Как понимаю, идея с таблицей не получилась?
                • Weddy
                  22 июня 2020, 22:03
                  Евгений Шибаев, ОК, буду ждать
                • Weddy
                  27 июня 2020, 03:47
                  Евгений Шибаев, спасибо! посмотрел, отписался
    • Replikant_mih
      12 июня 2020, 21:03
      Weddy, Тут если ты научишься получать из квика любые данные и отправлять любые управляющие команды, то дальше можно что хочешь накручивать, любые логики какие хочешь. Конечно, немного уметь программировать не помешает, но думаю, если не умеешь — отличный повод изучить Python, тем более не сложный язык).
  • Replikant_mih
    12 июня 2020, 16:41
    Спасибо, красота, заодно посмотрю как подобные вещи пишутся.
      • Replikant_mih
        12 июня 2020, 20:59

        Евгений Шибаев, Понял, спасибо, супер!)

        Блин, мне бы просто посмотреть код которым команда из питона в квик прилетает) и как луашные функции цепляет. Я с луа никогда не работал, но думаю там много и не надо, чтоб функции на команды навесить, просто посмотреть шаблон).

         

        Но конечно, если в вашем варианте уже что-то будет реализовано из мясца, не только скелет — вообще отлично).

         

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

          • Replikant_mih
            13 июня 2020, 00:17
            Евгений Шибаев, Питон с питоном сокетами соединял, C# с питоном, а вот с lua… увидел, что кто-то это умеет — теперь вот сижу в засаде, выжидаю))
  • Ilia Z
    25 февраля 2021, 18:39

    При попытке запустить скрипт LUA (QuikLuaPython.lua) на QUIK версии 8.12.0.41 возникает ошибка:
    error loading module 'socket.core' from file 'C:\QUIK\socket\core.dll':
    Не найден указанный модуль.

    C чем может быть связано?
  • FruitNinja77
    19 октября 2021, 22:37
    В квике скрипт запускается, и память начинает меняться, Notebook не выдает ошибок, рисует два пустых белых графика и все. В чем может быть проблема?

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

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