Алгоритмическая торговля на Московской бирже с помощью терминала 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.