Блог им. jatotrade_com

Python-->Lua-->Квик. Управление заявками в Квике из Питона.

Всем привет!
То о чем так долго мечтали большевики — свершилось!
Представляю QLua-сервер для управления заявками в Квике Квиком. Как обычно, в несколько строк кода.

Сервер представляет собой пассивный tcp-сокет со стороны QLua, который принимает от клиента любой корректный
lua-код, «вычисляет» его на своей стороне и возвращает результат клиенту.
Если вы хотите усилить безопасность, (особенно если клиент не на локальном хосте) замените «вычислитель» собственным парсером.
В видео показано взаимодействие с Квиком со стороны Питона(клиент).
Нам понадобятся следующие ингредиенты:
1. Квик 8-й версии от 8.5.2
2. Питон. Jupyter Notebook (Anaconda 3)
Последовательность действий:
Установка: содержимое папки Server (а не саму папку) скопируйте в папку Квика. Здесь сам луа-скрипт и библиотеки. Файл PythonClientQuikLuaServer.ipynb откройте в Питоне.
В Квике запустите QLua-сервер — луа-скрипт LuaQuikServer.lua
В Питоне в файле PythonClientQuikLuaServer.ipynb в переменную account_forts запишите ваш аккаунт на ФОРТСе (можно получить из окна выставления заявки в Квике).
Вычислите первую ячейку(CTRL+Enter) — определения переменных, функций и подключение клиента к серверу.
Можно отправлять команды на сервер.
Начиная со второй ячейки ноутбука показаны примеры, в том числе запрос параметров инструмента, выставление и снятие лимитных и стоп-заявок по номеру заявки. Сервер работает даже с не подключенным Квиком (кроме выставления заявок). Так что, его можно опробовать уже сегодня.
Код lua-сервера:
stopped = false -- Остановка файла
socket = require("socket") -- Указатель для работы с sockets
json = require( "json" ) -- Указатель для работы с json
IPAddr = "127.0.0.1" --IP Адрес
IPPort = 3585		 --IP Port	 
client = nil
   
-- Функция вызывается перед вызовом main
function OnInit(path)
  -- create a TCP socket and bind it to the local host, at port IPPort
  server = assert(socket.bind("*", IPPort))
  message(string.format("Server started. IP: %s; Port: %d\n", IPAddr, IPPort), 1);
end;

-- Функция вызывается перед остановкой скрипта
function OnStop(signal)
  if client then client:close() end
  stopped = true; -- Остановили исполнение кода 
end;

-- Функция вызывается перед закрытием квика
function OnClose()
  if client then client:close() end
  stopped = true; -- закрыли квик, надо остановить исполнение кода
end;

--Этой функцией заменен парсер. Саму ее нужно применять с осторожностью, т.к. это может нарушить безопасность системы.
--Она позволяет выполнять любой луа-код, поступивший со стороны клиента
function evalString (str)
  return assert(loadstring( "return "  .. str))()
end
 
-- Основная функция выполнения скрипта
function main()
-- wait for a connection from any client
  client = server:accept()
  sleep(1)
  while not stopped do
    local line, err = client:receive()
-- if there was no error, send it back to the client
    local result = evalString(line)
    if result == nil  then result = "{}" end
    if type(result) == "table"  then result = json.encode(result) end
    if type(result) == "boolean"  then result = (result and 1 or 0) end
--    if not err then message(string.format("Message:%s Result: %s\n", line, result), 1) end
    if not err then client:send(result .. "\n") end
  end
end;

Код Python-клиента:
# 1.В Квике запускаем скрипт LuaQuikServer.lua
# 2.В Питоне эту ячейку CTRL+Enter
import socket
import json
import sys
from datetime import datetime

host = '127.0.0.1' #локальный компьютер
port = 3585 #порт для подключения к серверу
# Буфер для получения ответа от сервера QLUA. Будьте внимательны, если ответ длиннее buff, увеличивайте размер buff заранее
buff=2048
account_forts = '888097i' # Аккаунт на ФОРТСе
account_tplus = 'L01+00000F00' # Аккаунт на Мамбе
client_tplus = '888VB/888VB' # Код клиента на Мамбе
transid = 1 # Инкрементируемый ID транзакции

#Функция - пример того, как из Python (или любого другого клиента, в т.ч. удаленного) можно вычислить любое корректное
#выражение или функцию на стороне сервера QLua и вернуть результат в Python, например res=QLua("1+2").
#Аргументом является строка, содержащая корректное выражение QLua. Т.к. в Квике присутствует кириллица формат будет 'cp1251'
#Строка посылается через сокет на QLua-сервер, вычисляется на нем, а результат возвращается в Python.
#Всегда используйте это с осторожностью, особенно если КВИК находится не на локальном хосте а в сети. Т.к. подключаемый
#к QLua-серверу клиент получает полный контроль над Квиком. Чтобы избежать проблем с безопасностью, замените в QLua-сервере
#"вычислитель" на парсер или используйте SSL-соединение.
def QLua(any_eval_string):
    s.send(f'{any_eval_string}\n'.encode('cp1251'))
    return s.recv(buff).decode('cp1251').replace("\n","")

#Функция предназначена для определения состояния подключения клиентского места к серверу. 
#Возвращает 1, если клиентское место подключено и 0, если не подключено.
def isConnected():
    s.send('isConnected()\n'.encode('utf-8'))
    return int(s.recv(buff).decode('utf-8'))

#Функция предназначена для получения информации по бумаге. Возвращает словарь, соответствующей таблице QLua с параметрами.
def getSecurityInfo(class_code, sec_code):
    s.send(f'getSecurityInfo("{class_code}", "{sec_code}")\n'.encode('utf-8'))
    return json.loads(s.recv(buff).decode('cp1251').replace("\n",""))

#Функция для выставления лимитных и стоп-заявок. Аргументы ticker - словарь, содержащий описание тикера после запроса
# getSecurityInfo(class_code, sec_code). Например SiU0=getSecurityInfo("SPBFUT", "SiU0"). price - цена, quant -количество 
# контрактов(лотов) в заявке, oper 'B'(по умолчанию) или 'S' - покупка или продажа, stop_price - цена активации стоп-заявки,
# если указана идентифицирует заявку как стоп-заявку. acc - торговый счет клиента (по умолчанию на ФОРТСе)
# Примеры выставления заявок:
# Лимитная заявка на покупку 1 контракта SiU0 по цене 70059. NewOrder(SiU0, 70059, 1, oper = 'B')
# Лимитная заявка на продажу 1 контракта SiU0 по цене 70888. NewOrder(SiU0, 70888, 1, oper = 'S')
# Для выставления стоп-заявки, задайте явно аргумент stop_price. Это цена активации стоп-заявки.
# Стоп-заявка на покупку 10 контрактов SiU0. При достижении ценой 71008 Купить 10 контрактов по цене 71028.
# NewOrder(SiU0, 71028, 10, oper = 'B', 71008)
def NewOrder(ticker, price, quant, oper = 'B', stop_price = False, acc = account_forts):
    global transid
    transid += 1 #Увеличиваем счетчит транзакций
    act = "NEW_STOP_ORDER" if stop_price else "NEW_ORDER" #Если указана цена активации заявки stop_price - это стоп-заявка
    stop_price_str = f'["STOPPRICE"] = "{stop_price}",' if stop_price else "" #если заявка лимитная, то это строка пустая
    s.send(('sendTransaction ({'+ f'["TRANS_ID"] = "{transid}",["ACTION"] = "{act}",["CLASSCODE"] = "{ticker["class_code"]}",["SECCODE"] = "{ticker["sec_code"]}",["OPERATION"] = "{oper}",["QUANTITY"] = "{quant}",["PRICE"] = "{price}",{stop_price_str}["ACCOUNT"] = "{acc}"' + '})\n').encode('utf-8'))
    return s.recv(buff).decode('cp1251').replace("\n","")

#Функция для снятия лимитных заявок по идентификатору заявки order_id - число или строка.
# transid - необязательный (в Питоне) номер транзакции. acc - торговый счет клиента (по умолчанию на ФОРТСе)
def LimitOrderKill(ticker, order_id, transid = transid, acc = account_forts):
    s.send(('sendTransaction ({'+ f'["TRANS_ID"] = "{transid}", ["ORDER_KEY"] = "{order_id}",["ACTION"] = "KILL_ORDER",["CLASSCODE"] = "{ticker["class_code"]}",["SECCODE"] = "{ticker["sec_code"]}",["ACCOUNT"] = "{acc}"' + '})\n').encode('utf-8'))
    return s.recv(buff).decode('cp1251').replace("\n","")

#Функция для снятия стоп-заявок по идентификатору заявки order_id - число или строка. 
def StopOrderKill(ticker, order_id, transid = transid, acc = account_forts):
    s.send(('sendTransaction ({'+ f'["TRANS_ID"] = "{transid}", ["STOP_ORDER_KEY"] = "{order_id}",["ACTION"] = "KILL_STOP_ORDER",["CLASSCODE"] = "{ticker["class_code"]}",["SECCODE"] = "{ticker["sec_code"]}",["ACCOUNT"] = "{acc}"' + '})\n').encode('utf-8'))
    return s.recv(buff).decode('cp1251').replace("\n","")

#################################################### Python Client Code ###############################################################           
#Сокет для отправки комманд в КВИК и получения результатов исполнения (через сервер в луа-скрипте) 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1',3585))
#По традиции поздороваемся с Квиком:
res = QLua('message("Привет, Квик!", 1)')
res
#Проверим знание Квиком арифметики 1-го класса:
res = QLua('3+2')
res
#Проверим, подключен ли Квик к торгам, уже с помощью функции в Питоне(см.код выше)
isConnected()
#В переменную-словарь SiU0 запросим из Квика все параметры контракта SiU0."SPBFUT" - код площадки, "SiU0" - код инструмента
SiU0 = getSecurityInfo("SPBFUT", "SiU0")
SiU0
#Выставим лимитную заявку на покупку 1 контракта SiU0 по цене 70059
res=NewOrder(SiU0, 70059, 1, 'B')
res
#Выставим стоп на продажу 1 контракт с ценой активации стопа 69888 и ценой исполнения 69850
res=NewOrder(SiU0, 69850, 1, 'S', 69888)
res
#Снимем лимитную и стоп-заявки по номеру заявки
LimitOrderKill(SiU0, 26040408900600)
StopOrderKill(SiU0, 149077121)
#ОСТОРОЖНО!!! КОД ВЫСТАВЛЯЕТ В ЦИКЛЕ 10 КОНТРАКТОВ SiU0 НА ПОКУПКУ ПО ЦЕНЕ 70200 И НИЖЕ С ШАГОМ В 2 ПУНКТА
for price in range(70200, 70180 , -2):
    NewOrder(SiU0, price, 1, 'B')
#После окончания успешной торговли не забывайте закрывать соединение, а в Квике остановите луа-скрипт.
s.close()
s

Клиент работает в режиме: отправка сообщения — получение ответа. Подключение постоянное. В конце работы закрывайте соединение c.close()
Скачать полный код можно со страницы https://www.jatotrade.com/download (в конце страницы)
Подписаться на мой канал в ютьюбе можно здесь.

Доброй охоты!

★73
48 комментариев
Крутяк, спасибо! Буду смотреть.
avatar
Replikant_mih, спасибо, в «бою» попробовать можно в понедельник.
Евгений Шибаев, Мне б просто убедиться, что сигнал приходит, даже просто принтануть его) — наверно и на выхах такое смогу).
avatar
Replikant_mih, естественно, вот это все и на выхах работает

#По традиции поздороваемся с Квиком:
res = QLua('message(«Привет, Квик!», 1)')
res
#Проверим знание Квиком арифметики 1-го класса:
res = QLua('3+2')
res
#Проверим, подключен ли Квик к торгам, уже с помощью функции в Питоне(см.код выше)
isConnected()

Евгений Шибаев, ага, супер!
avatar
Ребятки, я дико извиняюсь, нужно на дачу, на комменты отвечу после 23:00 по МСК
Евгений Шибаев, мой личный рекорд времени поездки до дачи в субботу 4.5 часа.
avatar
3Qu, не, у меня так много времени нет, до платника с Сокола 8 минут, по М11 22 и после 5 минут. Итого 35 минут. А, забыл — еще 500р.
А чем эта схема лучше sql c#?
avatar
Susanin, скажите, чем лучше против Quik+QLua?
Rostislav Kudryashov, я ничего не имею против lua. это просто инструменты.
но вот я не смог придумать как отладить скрипт на lua в квике. если только через логи в фаил. где хранить данные робота? интерфейс с базой данных?

avatar
Susanin, Так в теме с сокетами, луа нужен только чтоб связаться с квиком — подписаться на и получать события и отправлять управляющие команды, остальная логика на стороне Python кода.
avatar
Replikant_mih, кстати а как подключить к квику пару не зависимых процессов с отправкой транзакций? dll не позволяет это делать.
avatar
Susanin, Ну, Евгений вот только выложил код, через сокеты наверняка можно и несколько процессов, если нужно. Просто через разные порты связываться. Надо будет затестить, хотя для начала и одного мне хватит).
avatar
Replikant_mih, а если строчка придет частично? или только может полностью придти?
avatar
Susanin, Не знаю)), я ещё не запускал ни разу).
avatar
Replikant_mih, я как то делал приложение на сокетах. при пришлось делать свой протокол с размером сообщений, crc  и т.д. т.е. собирать сообщение целиком из разных пришедших частей. а потом уже его обрабатывать.
avatar
Susanin, А, ну это, видимо, если сообщение не влезает в настроенный лимит, можно лимит расширить, либо, если сообщения по какой-то причине должны быть огромными — писать какую-то специфичную логику обработки. Но это вроде не проблема.
avatar
Susanin, не валяй дурочку. Каждый скрипт QLua — отдельный (независимый) поток thread. Если обработка события в callback'ах OnOrder, OnTrade и т.п. отнимает слишком много времени у главного потока, то данные события кладут в структуру «очередь» и  разгружают её в этих отдельных thread'ах.
Потокобезопасность обеспечивают функции table.sinsert(), sremove().
Rostislav Kudryashov, а причем тут потоки вообще? какое отношение это имеет к придаче двоичных данных по сокетам?
avatar
Susanin, сочувствую.
Rostislav Kudryashov, вот и я тоже. отвечать не по теме это странно.
avatar
Susanin, если не нужен сервер на сети, годится библиотека SQLua (SQLite). А ещё лучше vfpoledb.dll через COM-интерфейс luacom. FoxPro — чемпион по скорости. Команда SEEK быстрее в 10 раз любого SQL. Хотя  SQL и Rollback тоже есть.
Но мои роботы хранят торговые позиции для перехода на следующий день в Lua-скиптах, содержащих соответствующие таблицы.
А отлаживают по складам (через дебагер) и читают по складам только первоклашки. Хотя в Lua есть модуль debug.
Но роботов я отлаживаю на демо-счетах.
QLua не «просто инструмент». Это идеальный инструмент.
Rostislav Kudryashov, мда.
avatar
function OnInit(path)
  -- create a TCP socket and bind it to the local host, at port IPPort
  server = assert(socket.bind("*", IPPort))
Опасно! Сервер будет слушать на всех интерфейсах, а не только localhost.

avatar
Sergey Zaytchenko, А как надо?
avatar
Replikant_mih, либо защищать протокол обмена, либо заменить на:
server = assert(socket.bind(«127.0.0.1», IPPort))
и запускать клиент и сервер на одной и той же машине.
avatar
Sergey Zaytchenko, Мм, спасибо, попробую.
avatar
Replikant_mih, только скобки ёлочки надо заменить на двойные кавычки
avatar
Sergey Zaytchenko, ага, спсб, учту. В незнакомой среде такие уточнения могут стоить часов работы)).
avatar
Sergey Zaytchenko, вообще если вы административным ресурсом не откроете соответствующий порт, то к вам на локал хост никто не достучится.
 
Евгений Шибаев, к сожалению это не всегда так, за Вас это может сделать  malware, или можно ошибиться в конфигурации vpn сервиса. Но я просто обратил внимание на то что комментарий к коду не соответствует его поведению.
avatar
Sergey Zaytchenko, да, по-любому спасибо, что разобрались в коде, часто пишется все «на коленке», не за всем успеваешь уследить…

Может, хреновый из меня айтишник (но я и не полностью айтишник), но так приятно когда тебя за ручку проводят через сложные процессы где много где можно оступиться и завязнуть.

 

Всегда когда что-то подобное делаешь, всегда вылезет куча нюансов:

— чет не работает.

— а, ну тут библиотека с этой не совместима.

— чет не работает, а ну тут же файл надо переименовать, на стэковерфлоу в 2007 году пост на эту тему был, не читал что ли?

— чет не работает.

— а, ну ты в реестре запись, наверно, не прописал и конечно же не отключил системный процесс А.

 

А тут такое:

— вот тебе папочка, скопируй её сюда, да смотри не перепутай — содержимое, а не всю папку.

Расписано какие конкретно как и зачем ячейки в ноутбуке запускать.

 

Может, я сегодня какой-то особенно сентиментальный, но это праздник какой-то)), так иногда хочется чтоб тебя вот так юзер-френдли сопровождали при решении сложных задач, почему весь мир IT не такой?))).

avatar
Replikant_mih, «а разве так можно было?» — Спасибо за оценку! Просто олд скул, ничего лишнего...)))

Респект ещё раз!
Подружил с php (пока на тест) — то что давно хотел сделать )

Правда при клозе скрипт ваш по такой ошибке выпадает сам...



Свой Мужик, вообще зачет! Ну можно, в принципе с чем угодно подружить. А При клозе пока да, принудительный баг, чтобы скрипт останавливал, потом можно снова запустить. Руки не дошли…
WebQuik прекрасно принимает и передает всю необходимую информацию через Websocket. Рабочий пример https://github.com/DmitryPukhov/pytrade
WebQuik <--> Websocket <--> Python
avatar
luks sluk, Спасибо огромное за информацию. А для доступа через WebQuik есть где-то документация по запросам и т.д.? Благодарю.
avatar
luks sluk, только при этом требует докер. Не у всех есть возможность их устанавливать в силу корп политик безопасности.
avatar
Whispered, и без докера прекрасно работает, коннекшн совершается тут https://github.com/DmitryPukhov/pytrade/blob/master/pytrade/connector/quik/WebQuikConnector.py
avatar
luks sluk, Спасибо!
avatar

Господа, проконсультируйте, пожалуйста, по некоторым вопросам. Пробую использовать питон для написания простенькой автоматической торговой системы. Питон выбрал, так как уже имел с ним небольшой опыт, да и язык универсальный, может где ещё пригодится. Но, как я погляжу, не очень то распространено его использование в связке с торговыми терминалами, поэтому приходится собирать информацию по крупицам, и публикации Евгения Шибаева, похоже, самый лучший источник на смартлабе, если не сказать больше-в рунете! :) Для начала завёл демо-счёт у финама и установил предложенный Квик 7.25. Луа-серверы из постов «Управление заявками в Квике из Питона» и «Стакан к празднику» как-то можно между собой совместить? Можно ли запустить их одновременно разными потоками? (уже попробовал-можно). Что для этого нужно, использовать разные порты? Я смотрю, что в одном скрипте используется порт 3585, а в другом 3587, это имеет какое-либо значение? Или совместить эти скрипты в одном файле? Вообще, сейчас попробовал запускать скрипты, но никаких данных не получил, возможно это связано с тем, что это было в воскресенье, торги не ведутся.
Что за аккаунт на фортс и мамбе упоминается в посте? Это те, которые используются для подкачки исторических данных? В квике в окне заявки я вижу «торговый счёт» и «код клиента». Это оно?
Как на питоне скачать в рантайме неглубокие исторические данные, что бы строить, скажем, скользящую среднюю? Для оптимизации параметров алгоритма на большой истории я просто скачиваю csv с сайта финама, а как быть с рантаймом?
Или вообще не заморачиваться, и работать с каким-нибудь TSLab? Но хотелось бы с питоном поковыряться, интересно же..

 

avatar
Андрей, ставьте Квик 8.5.2 и выше. С семеркой уже ничего работать не будет, т.к. биржа перешла на 19-ти значные заявки. По вопросу совмещения портов, я постараюсь в ближайшее время написать топик про tcp-сокет, там можно будет через один порт работать с Квиком как на получение данных из Квика так и на отправку приказов в Квик. Аккаунт на фортсе свой это «торговый счёт» и «код клиента» и они одинаковые. А на ММВБ — разные, это нужно учитывать при формировании заявок. Это оно.
Как на питоне скачать в рантайме неглубокие исторические данные, что бы строить, скажем, скользящую среднюю? Для оптимизации параметров алгоритма на большой истории я просто скачиваю csv с сайта финама, а как быть с рантаймом?
В этом топике https://smart-lab.ru/blog/663093.php показано как тащить любые котировки по истории с сайта Финама, функция GetCandles (ticker, time_frame, period_days) берет котировки по тикеру за последние period_days начиная с текущего момента, т.е. с реалтайм (последняя котировка на момент запроса) Там же и показано как строить например ЕМА-шки.
Или вообще не заморачиваться, и работать с каким-нибудь TSLab? Но хотелось бы с питоном поковыряться, интересно же..
Это уже на ваш выбор.
avatar
Подскажите пожалуйста, как сделать с использованием данного скрипта Qlua, чтобы из QLUA автоматически приходили обезличенные сделки в пайтон, не из пайтона вызывать каждую секунду проверку, а всётаки чтобы потоком без пропусков.?
avatar

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

....все тэги



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