Алгоритмическая торговля на Московской бирже с помощью терминала QUIK остаётся популярным способом автоматизировать стратегии. В этой статье мы напишем грид-бота, который выставляет ордера сеткой вокруг текущей цены и зарабатывает на колебаниях.
? Что такое грид-бот
Грид-бот (от англ. grid — сетка) — это торговый алгоритм, который выставляет ордера (лимитки) на покупку и продажу через равные интервалы цены.
Простейший сценарий:
Цена идёт вниз — бот набирает позицию по мере снижения.
Цена возвращается вверх — бот закрывает покупки продажами, фиксируя прибыль на каждом "шаге сетки".
Таким образом бот "ловит пилу", зарабатывая на флэте и колебаниях.
В коде ниже реализована версия с:
стопом/тейком для бота.
Пересчётом средней цены позиции.
Подсчётом реализованного и нереализованного PnL.
⚙️ Подключение Python к QUIK
Чтобы Python "видел" терминал QUIK, нужен связующий слой. Есть несколько способов:
QUIK LUA scripts (QLua) — встроенные скрипты на Lua.
QuikSharp — надстройка, которая через Lua общается с QUIK и слушает события.
QuikPy — Python-обёртка над QuikSharp.
Мы будем использовать QuikPy, так как это самый удобный вариант.
Устанавливаем библиотеку с github.
Подготовка QUIK
Скопируйте папку QUIK\lua в папку установки QUIK. В ней находятся скрипты LUA.
Скопируйте папку QUIK\socket в папку установки QUIK.
Запустите 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[i] %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[i] %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.