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

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 (в конце страницы)
Подписаться на мой канал в ютьюбе можно здесь.

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

48 Комментариев
  • Replikant_mih
    20 июня 2020, 11:54
    Крутяк, спасибо! Буду смотреть.
      • Replikant_mih
        20 июня 2020, 12:22
        Евгений Шибаев, Мне б просто убедиться, что сигнал приходит, даже просто принтануть его) — наверно и на выхах такое смогу).
          • Replikant_mih
            20 июня 2020, 12:55
            Евгений Шибаев, ага, супер!
    • 3Qu
      20 июня 2020, 12:39
      Евгений Шибаев, мой личный рекорд времени поездки до дачи в субботу 4.5 часа.
  • Susanin
    20 июня 2020, 13:23
    А чем эта схема лучше sql c#?
    • Rostislav Kudryashov
      20 июня 2020, 15:25
      Susanin, скажите, чем лучше против Quik+QLua?
      • Susanin
        20 июня 2020, 15:40
        Rostislav Kudryashov, я ничего не имею против lua. это просто инструменты.
        но вот я не смог придумать как отладить скрипт на lua в квике. если только через логи в фаил. где хранить данные робота? интерфейс с базой данных?

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

    • Replikant_mih
      20 июня 2020, 16:38
      Sergey Zaytchenko, А как надо?
      • Sergey Zaytchenko
        20 июня 2020, 16:43
        Replikant_mih, либо защищать протокол обмена, либо заменить на:
        server = assert(socket.bind(«127.0.0.1», IPPort))
        и запускать клиент и сервер на одной и той же машине.
        • Replikant_mih
          20 июня 2020, 16:50
          Sergey Zaytchenko, Мм, спасибо, попробую.
          • Sergey Zaytchenko
            20 июня 2020, 17:02
            Replikant_mih, только скобки ёлочки надо заменить на двойные кавычки
            • Replikant_mih
              20 июня 2020, 17:21
              Sergey Zaytchenko, ага, спсб, учту. В незнакомой среде такие уточнения могут стоить часов работы)).
      • Sergey Zaytchenko
        20 июня 2020, 22:02
        Евгений Шибаев, к сожалению это не всегда так, за Вас это может сделать  malware, или можно ошибиться в конфигурации vpn сервиса. Но я просто обратил внимание на то что комментарий к коду не соответствует его поведению.
  • Replikant_mih
    20 июня 2020, 23:30

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

     

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

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

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

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

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

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

     

    А тут такое:

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

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

     

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

  • Свой Мужик
    06 июля 2020, 21:20

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

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



  • luks sluk
    29 июля 2020, 17:40
    WebQuik прекрасно принимает и передает всю необходимую информацию через Websocket. Рабочий пример https://github.com/DmitryPukhov/pytrade
    WebQuik <--> Websocket <--> Python
    • Whispered
      28 октября 2021, 13:25
      luks sluk, только при этом требует докер. Не у всех есть возможность их устанавливать в силу корп политик безопасности.
      • luks sluk
        31 октября 2021, 11:54
        Whispered, и без докера прекрасно работает, коннекшн совершается тут https://github.com/DmitryPukhov/pytrade/blob/master/pytrade/connector/quik/WebQuikConnector.py
  • Андрей
    28 декабря 2020, 21:19

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

     

  • sanvir
    08 июня 2022, 19:40
    Подскажите пожалуйста, как сделать с использованием данного скрипта Qlua, чтобы из QLUA автоматически приходили обезличенные сделки в пайтон, не из пайтона вызывать каждую секунду проверку, а всётаки чтобы потоком без пропусков.?

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

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