Алгоритмическая торговля на Московской бирже с помощью терминала 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.
ссылка на оригинал статьи https://habr.com/ru/articles/937842/
Добавить комментарий