AI Создаем простого грид-бота для Московской биржи через QUIK и Python

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 041
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


Алгоритмическая торговля на Московской бирже с помощью терминала QUIK остаётся популярным способом автоматизировать стратегии. В этой статье мы напишем грид-бота, который выставляет ордера сеткой вокруг текущей цены и зарабатывает на колебаниях.

? Что такое грид-бот


Грид-бот (от англ. grid — сетка) — это торговый алгоритм, который выставляет ордера (лимитки) на покупку и продажу через равные интервалы цены.

Простейший сценарий:


  • Цена идёт вниз — бот набирает позицию по мере снижения.


  • Цена возвращается вверх — бот закрывает покупки продажами, фиксируя прибыль на каждом "шаге сетки".

Таким образом бот "ловит пилу", зарабатывая на флэте и колебаниях.

В коде ниже реализована версия с:


  • стопом/тейком для бота.


  • Пересчётом средней цены позиции.


  • Подсчётом реализованного и нереализованного PnL.
⚙️ Подключение Python к QUIK


Чтобы Python "видел" терминал QUIK, нужен связующий слой. Есть несколько способов:


  • QUIK LUA scripts (QLua) — встроенные скрипты на Lua.


  • QuikSharp — надстройка, которая через Lua общается с QUIK и слушает события.


  • QuikPy — Python-обёртка над QuikSharp.

Мы будем использовать QuikPy, так как это самый удобный вариант.

Устанавливаем библиотеку с github.

Подготовка QUIK


  1. Скопируйте папку QUIK\lua в папку установки QUIK. В ней находятся скрипты LUA.


  2. Скопируйте папку QUIK\socket в папку установки QUIK.


  3. Запустите QUIK. Из меню Сервисы выберите LUA скрипты. Нажмите кнопку Добавить. Выберете скрипт QuikSharp.lua Нажмите кнопку OK. Выделите скрипт из списка. Нажмите кнопку Запустить.

Если в окне сообщений QUIK выдаст QUIK# is waiting for client connection..., то скрипт запущен успешно. Теперь Python может обмениваться данными с QUIK через QuikPy.

? Разбор кода грид-бота


В начале скрипта инициализируются глобальные переменные и делаем импорты:

from QuikPy import QuikPy # Работа с QUIK из Python через LUA скрипты QuikSharp
import time


unrealized_pnl = 0
avg_price = 0
position = 0
result = 0
class_code = 'TQBR' # Код площадки
sec_code = 'SBER' # Код тикера
trans_id = 12358 # Номер транзакции
diff = gridrange*2 / grid #ход цены для лимитки
flag = True

  • avg_price — средняя цена позиции.


  • position — текущая позиция в лотах.


  • realized_pnl и unrealized_pnl — реализованная и бумажная прибыль.

Параметры вводятся вручную:

lot = int(input('введите лотаж позиции'))
grid = int(input('суммарное количество лимитных ордеров:'))
gridrange = float(input('Какой ход цены для гриб бота?')) // 2
local_stop = -(int(input('Какой убыток за 1 цикл вы готовы понести?')) )
grid_stop = -(int(input('какой убыток грид бота вообщем вы готовы понести?')) )
quantity = int(input('Количество акций в лотах на одну линию сетки')) # Кол-во в лотах

Здесь мы определяем:


  • Количество лимиток в сетке (grid).


  • Диапазон цены (gridrange).


  • Локальные и глобальные стопы/тейки.

? Обработчики событий QUIK

def on_trans_reply(data):
"""Обработчик события ответа на транзакцию пользователя"""
print('OnTransReply')
print(data['data']) # Печатаем полученные данные


def on_order(data):
"""Обработчик события получения новой / изменения существующей заявки"""
print('OnOrder')
print(data['data']) # Печатаем полученные данные


def on_trade(data):
"""Обработчик события получения новой / изменения существующей сделки
Не вызывается при закрытии сделки
"""
print('OnTrade')
print(data['data']) # Печатаем полученные данные


def on_futures_client_holding(data):
"""Обработчик события изменения позиции по срочному рынку"""
print('OnFuturesClientHolding')
print(data['data']) # Печатаем полученные данные


def on_depo_limit(data):
"""Обработчик события изменения позиции по инструментам"""
print('OnDepoLimit')
print(data['data']) # Печатаем полученные данные


def on_depo_limit_delete(data):
"""Обработчик события удаления позиции по инструментам"""
print('OnDepoLimitDelete')
print(data['data']) # Печатаем полученные данные

QUIK шлёт данные в реальном времени. Мы подписываемся на события: исполнение заявок, сделки, изменение позиции.

? Функции заявок


def buy():
transaction = {
'ACTION': 'NEW_ORDER',
'CLASSCODE': class_code,
'SECCODE': sec_code,
'OPERATION': 'B',
'PRICE': str(0), # рыночная заявка
'QUANTITY': str(quantity),
'TYPE': 'M'}
qp_provider.SendTransaction(transaction)

def sell():
transaction = {
'ACTION': 'NEW_ORDER',
'CLASSCODE': class_code,
'SECCODE': sec_code,
'OPERATION': 'S',
'PRICE': str(0), # рыночная заявка
'QUANTITY': str(quantity),
'TYPE': 'M'}
qp_provider.SendTransaction(transaction)

Простейшие функции отправки заявок на покупку и продажу

? Основной цикл


Получаем текущую цену:

price = float(qp_provider.GetParamEx(class_code, sec_code, 'LAST')['data']['param_value'])

Строим сетку вокруг неё:

a = []
for x in range(grid // -2, grid // 2 + 1):
a.append(round(lastdealprice + diff * x, 1))

В бесконечном цикле:


  • Проверяем текущую цену.


  • Если цена пересекла уровень сетки — покупаем/продаём.


  • Пересчитываем среднюю цену позиции.


  • Считаем PnL.


  • Смотрим на условия стопа/тейка.

while gridprofit < grid_take and grid_stop < gridprofit:

qp_provider = QuikPy() # Подключение к локальному запущенному терминалу QUIK
qp_provider.OnTransReply = on_trans_reply # Ответ на транзакцию пользователя. Если транзакция выполняется из QUIK, то не вызывается
qp_provider.OnOrder = on_order # Получение новой / изменение существующей заявки
qp_provider.OnTrade = on_trade # Получение новой / изменение существующей сделки
qp_provider.OnFuturesClientHolding = on_futures_client_holding # Изменение позиции по срочному рынку
qp_provider.OnDepoLimit = on_depo_limit # Изменение позиции по инструментам
qp_provider.OnDepoLimitDelete = on_depo_limit_delete # Удаление позиции по инструментам


class_code = 'TQBR' # Код площадки
sec_code = 'SBER' # Код тикера
trans_id = 12345 # Номер транзакции
price = round(float(qp_provider.GetParamEx(class_code, sec_code, 'LAST')['data']['param_value']), 1)
quantity = 3 # Кол-во в лотах


lastdealprice = round(float(qp_provider.GetParamEx(class_code, sec_code, 'LAST')['data']['param_value']), 1)

print(price)
a = []
for x in range(grid//-2, grid//2 + 1):
a.append (round(lastdealprice + diff*x, 1))
index = len(a) // 2

print(a)

print("\n Grid net prices: " + str(a) + '\nDifference between trade levels is: ' + str(diff) )
while total_pnl < local_take and total_pnl > local_stop:
lastPrice = round(float(qp_provider.GetParamEx(class_code, sec_code, 'LAST')['data']['param_value']), 1)
if lastPrice in a and lastPrice > lastdealprice:
for i in range(len(a)):
if lastPrice % 0.1 == a %0.1 and index != i:
index = i
# Продажа

sell()
print(f'sell @ {lastPrice}')
pnl = (lastPrice - avg_price) * quantity * lot
realized_pnl += pnl
position -= quantity
print(f'Реализованный PnL: {realized_pnl:.2f}')
if position != 0:
avg_price = (avg_price * position + lastPrice * quantity) / (position)
else:
avg_price = 0
lastdealprice = lastPrice
time.sleep(5)

if lastPrice in a and lastPrice < lastdealprice:
for i in range(len(a)):
if lastPrice % 0.1 == a %0.1 and index != i:
index = i
# Покупка
buy()
print(f'buy @ {lastPrice}')
position += quantity
if position != 0:
avg_price = (avg_price * position + lastPrice * quantity) / (position)
else:
avg_price = 0
print(f'Средняя цена: {avg_price:.2f}')
lastdealprice = lastPrice
time.sleep(5)

# Подсчет нереализованного PnL
unrealized_pnl = (lastPrice - avg_price) * position if position != 0 else 0.0
total_pnl = realized_pnl + unrealized_pnl

print(f'Позиция: {position}, Реализ. PnL: {realized_pnl:.2f}, Нереализ. PnL: {unrealized_pnl:.2f}, Всего: {total_pnl:.2f}')

time.sleep(1) # Чтобы не перегружать QUIK запросами

if position > 0 and (total_pnl <= local_stop or total_pnl >= local_take):
for i in range(position):
sell()
elif position < 0 and (total_pnl <= local_stop or total_pnl >= local_stop):
for i in range(position):
buy()
print('result' + str(total_pnl))


gridprofit += total_pnl
▶️ Как запускать скрипт в QUIK


  • В QUIK подключите QuikSharp.lua (из репозитория finsight/QUIKSharp).


  • Запустите QUIK (с этим Lua-скриптом).


  • Запустите Python-бота:
⚠️ Важные моменты


  • Код работает только на живом QUIK с подключением к бирже.


  • Для тестов используйте демо-счёт или бумажный счёт.


  • В продакшн-версии обязательно добавьте:

    • Логирование в файл.


    • Проверку остатков и денег на счёте.


    • Защиту от повторного открытия сделок.


    • Выход при потере связи с QUIK.
? Заключение


Мы написали полноценного грид-бота под QUIK на Python:


  • Подключение к терминалу через QuikPy.


  • Построение сетки цен.


  • Автоматические покупки/продажи.


  • Подсчёт прибыли и стопов.

Такой код можно расширить: добавить гибкие уровни, динамический шаг сетки, защиту от резких движений рынка. Я показал самый простой вариант для демонстрации возможностей бота и использования библиотеки quickpy.
 
Сверху Снизу