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

Комментарии (0)