Всем привет, с наступающим праздником! Который, надеюсь у большинства пройдет, как обычно, в
ЖО ЗОЖе (блин, слово-то придумали).
В продолжение топика "
КВИК-->Lua-->Python. Трансляция данных из КВИКа в Питон в реальном времени".
В Python-сервер добавлен парсер и визуализатор стакана. Стакан в стиле QSCALP-лайт вариант. Все как обычно в 20 строк кода.
У Тимофея гифки со сторонних сайтов не кажут. Приходится
ссылку давать… Или отказываться от главной. Выбрал второе.
Чтобы насладиться созерцанием стакана нам нужны следующие ингредиенты:
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.
Кое что интересное для извлечения денег с рынка можно посмотреть на моем канале в
ютьюбе.
Всем удачи! И будьте здоровы!
Продам на 50 звездах ★★★★★....
Потому, что пост про LUA))
Но у меня другая концепция:
— Питон для объемных вычислений,
— для всяких стаканов/сделок есть С++.
— для связи — Луа.
На самом деле, коллега, питон в этом варианте очень прожорлив, естессно его никто не собирается использовать для отображения быстрой графики.
Traceback (most recent call last):
File «C:\Users\User\Anaconda3\lib\site-packages\matplotlib\cbook\__init__.py», line 216, in process
func(*args, **kwargs)
File «C:\Users\User\Anaconda3\lib\site-packages\matplotlib\animation.py», line 1465, in _stop
self.event_source.remove_callback(self._loop_delay)
AttributeError: 'NoneType' object has no attribute 'remove_callback'
%matplotlib notebook в самое начало.
Comrade, saludos cordiales! El pueblo unido hamas sera vensido!
Сейчас вот это уточнение и статическое изображение.
Странно, может совпадение ?
А так, разумеется, спасибо!
Вам большой респект и дальнейших творческих успехов.
А где взять библиотеки для Lua:
Сделали бы лучше что-то типа скринера для акций для Квика. Например, чтоб в таблицу можно было выбрать интересующие инструменты и по ним отображалось по каждому инструменту изменение текущей цены по отношению к цене с задаваемым условием: за неделю, месяц, или особенно здорово например от последнего хай/лой за последние хх дней/ определенной даты.
Не сочтите за наглость, а можно ли еще к таблице добавить столбец, в котором пользователь мог бы просто заносить свои примечания к инструменту в произвольном формате?
Все это правильно было бы просить у разработчика. Но зайдя на форум Арки и полазив я увидел, что там годами висят предложения/просьбы к продукту от пользователей без всякого движения. Уж сколько лет народ просит добавить очевидные вещи типа автоматического выставления стопа/тэйк-профита по совершению сделки — все в пустоту.
А средствами только скрипта Lua задачи построения такой таблицы не решаются?
import requests
import csv
url='https://iss.moex.com/iss/engines/futures/markets/forts/securities/SIM0/candles/securities.json?&from=2020-03-20&start=0&interval=24&candles.columns=open,close,high,low,volume,end'
res=requests.get(url)
data=res.json()['candles']
data
Возвращает словарь типа:
Евгений Шибаев, Понял, спасибо, супер!)
Блин, мне бы просто посмотреть код которым команда из питона в квик прилетает) и как луашные функции цепляет. Я с луа никогда не работал, но думаю там много и не надо, чтоб функции на команды навесить, просто посмотреть шаблон).
Но конечно, если в вашем варианте уже что-то будет реализовано из мясца, не только скелет — вообще отлично).
Если что, я с пониманием отнесусь если будет и позже понедельника, а-то тоже, бывает, что-нить скажешь сгоряча, потом приходится делать что обещал))).
Там все просто — со стороны луа открывается потоковый сервер, который банально принимает строку от клиента и вычисляет ее. Конечно, небезопасно, если в сети кроме вас есть подключения. Для безопасности просто делается не вычислитель, а парсер на стороне сервера. Ну не будем забегать вперед.
При попытке запустить скрипт LUA (QuikLuaPython.lua) на QUIK версии 8.12.0.41 возникает ошибка:
C чем может быть связано?error loading module 'socket.core' from file 'C:\QUIK\socket\core.dll':
Не найден указанный модуль.