Блог им. RomellaAkumov

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

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

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

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

<code>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</code>
  • avg_price — средняя цена позиции.

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

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

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

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

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

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

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

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

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

<code>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'])  # Печатаем полученные данные</code>

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

🛒 Функции заявок
<code>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)</code>

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

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

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

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

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

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

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

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

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

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

  • Считаем PnL.

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

<code>    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</code>
▶️ Как запускать скрипт в QUIK
  • В QUIK подключите QuikSharp.lua (из репозитория finsight/QUIKSharp).

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

  • Запустите Python-бота:

⚠️ Важные моменты

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

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

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

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

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

    • Защиту от овертрейдинга из-за багов.

    • Выход при потере связи с QUIK.

📌 Заключение

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

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

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

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

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

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

Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
2.7К | ★13
6 комментариев
«На хрена козе баян!?» ©
avatar
О, прикольно! Я похожее лет 15 тому назад делал. Замаялся с ситуациями, когда проблемы с переконнектом квика — таблицы не заполнены, а бот считает, что позиций просто нет. Нужны куча проверок на то, что квик законнектился и все таблицы подкачаны. В том числе пришлось даже делать свои записи размеров позиций, чтобы сравнивать с теми, что в таблицах.
avatar

Читайте на SMART-LAB:
Переход на новую модель депозитарного обслуживания
Уважаемые клиенты! Информируем вас о том, что в связи с изменением статуса депозитарной лицензии, функции по учету и хранению ценных бумаг...
Фото
Акционеры Аэрофлота одобрили выплату дивидендов по итогам 2025 года
Сегодня состоялось годовое заседание Общего собрания акционеров ПАО «Аэрофлот». Акционеры утвердили выплату дивидендов по итогам 2025 года в...
Фото
EUR/USD: пара продолжает снижаться на фоне укрепления доллара
Евро на протяжении всего рассматриваемого периода находился под давлением укрепляющегося доллара США, а пара EUR/USD обновила локальные минимумы....
Фото
Длинные ОФЗ: сколько можно заработать, если ключевая ставка ЦБ РФ продолжит снижаться?
Длинные ОФЗ с начала текущего года не демонстрировали выраженного снижения по доходности несмотря на продолжение цикла понижения ключевой ставки...

теги блога Roman crypto_maniac

....все тэги



UPDONW
Новый дизайн