Python

Реализация стратегии grid-трейдинга на Python

Этот пост посвящен реализации стратегии grid trading’a на Python.

Что такое grid-трейдинг?

Стратегия Grid Trading размещает набор ордеров на покупку и продажу через равные промежутки времени выше и ниже текущей цены. Когда цена колеблется вверх или вниз, исполняются long и short ордера, которые будут приносить прибыль, поскольку цена продолжает двигаться вверх и вниз по сетке.

Существует два варианта стратегии:

  • По диапазону рынка
  • По рыночному тренду

Логика торговли

В этой записи будет показано, как реализовать стратегию grid торговли для ранжирования рынка. Логика торговли выглядит следующим образом:

Определение параметров для grid стратегии:

  • Размер сетки: 1%
  • Количество сеток 5
  • Take profit: 1% выше и ниже сетки
  • Stop loss: 1% выше и ниже сетки
  • Период сброса сетки: 7 дней

Мы будем использовать индикатор Chop, чтобы определить является ли текущий рынок трендовым или колеблющимся. Если текущий рынок находится в диапазоне, то мы открываем 5 лимитных ордеров на покупку и 5 лимитных ордеров на продажу выше и ниже текущей рыночной цены. Также устанавливаем соответствующие уровни тейк-профита и стоп-лосса на 1% выше и ниже лимитной цены.

Шаг 1: Расчет данных для индикатора Chop

Прежде всего, мы определяем параметры сетки при инициализации.

from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import pandas_ta as ta

class AlgoEvent:
    def __init__(self):
        self.timer = datetime(1970,1,1)
        self.grid_setup_time = datetime(1970,1,1)
        self.grid_size_pct = 0.01
        self.grid_num = 5
        self.tp_pct = 0.01
        self.sl_pct = 0.01
        self.numObs = 30
        self.is_runningGrid = False
        self.trade_size = 0.1
        

В этой заметке будет использоваться функция API getHistoricalBar для сбора исторических данных свечей. Затем применяется библиотека pandas_ta.chop для расчета серий данных chop.

    def compute_CHOP(self, data):
        high = [data[t]['h'] for t in data]
        low = [data[t]['l'] for t in data]
        close = [data[t]['c'] for t in data]
        df = pd.DataFrame.from_dict({"high":high,"low":low,"close":close})
        chop_series = ta.chop(high=df['high'], low=df['low'], close=df['close'], length=14, atr_length=1)
        return chop_series.to_numpy()


    def on_marketdatafeed(self, md, ab):
        # вызов стратегий каждые 24 часа
        if md.timestamp >= self.timer+timedelta(hours=24):
            # update timer
            self.timer = md.timestamp

            # получить последние OHLC свечи
            res = self.evt.getHistoricalBar(
                contract = {"instrument":md.instrument}, 
                numOfBar = self.numObs, 
                interval = "D"
            )
            if len(res)<self.numObs:
                return

            chop_series = self.compute_CHOP(res)

            self.evt.consoleLog(chop_series)

Шаг 2: Определение диапазона рынка и настройка сетки

Индикатор CHOP располагается в диапазоне от 0 до 100.  Это означает, что рынок находится в диапазоне, если значение закрывается до 100. И рынок является трендовым, если значение закрывается до 0. Таким образом, в логике кода представленного ниже (строки 21-23) мы считаем, что рынок находится в зоне диапазона, когда предыдущее значение CHOP ниже 50, а текущее значение выше 50.

Настройка сетки лимитных ордеров на покупку описана в строках 32-45; сетка лимитных ордеров на продажу в строке 47-60.

    def on_marketdatafeed(self, md, ab):
        # вызов стратегий каждые 24 часа
        if md.timestamp >= self.timer+timedelta(hours=24):
            # update timer
            self.timer = md.timestamp

            # получение последних OHLC свечей
            res = self.evt.getHistoricalBar(
                contract = {"instrument":md.instrument}, 
                numOfBar = self.numObs, 
                interval = "D"
            )
            if len(res)<self.numObs:
                return

            chop_series = self.compute_CHOP(res)

            self.evt.consoleLog(chop_series)

            # проверка нахождения рынка в тренде или в диапазоне
            is_ranging = True
            if chop_series[-1]>50 and chop_series[-2]<=50:
                is_ranging = False

            # установка нового grid для is_ranging рынка
            if not self.is_runningGrid and is_ranging:
                self.is_runningGrid = True
                self.grid_setup_time = md.timestamp

                for i in range(1,self.grid_num+1):

                    # лимитный ордер на покупку
                    price = md.bidPrice*(1-i*self.grid_size_pct)
                    #ask = md.askPrice*(1-i*self.grid_size_pct)
                    order = AlgoAPIUtil.OrderObject(
                        instrument = md.instrument,
                        openclose = 'open',
                        buysell = 1,    # 1=buy, -1=sell
                        ordertype = 1,      # 1=limit order
                        price = price,
                        volume = self.trade_size,
                        takeProfitLevel = price*(1+self.tp_pct),
                        stopLossLevel = price*(1-self.sl_pct)
                    )
                    self.evt.sendOrder(order)
                    
                    # лимитный ордер на продажу
                    #bid = md.bidPrice*(1+i*self.grid_size_pct)
                    price = md.askPrice*(1+i*self.grid_size_pct)
                    order = AlgoAPIUtil.OrderObject(
                        instrument = md.instrument,
                        openclose = 'open',
                        buysell = -1,    # 1=buy, -1=sell
                        ordertype = 1,      # 1=лимитный ордер
                        price = price,
                        volume = self.trade_size,
                        takeProfitLevel = price*(1-self.tp_pct),
                        stopLossLevel = price*(1+self.sl_pct)
                    )
                    self.evt.sendOrder(order)

Шаг 3: Сброс сетки

В логике нашей стратегий мы будем сбрасывать сетку каждые 7 дней. В функций reset_grid() отменяются все невыполненные ордера.

    def reset_grid(self):
        # отмена всех невыполненных заявок
        positions, osOrder, pendOrder = self.evt.getSystemOrders()
        for tradeID in pendOrder:
            order = AlgoAPIUtil.OrderObject(
                tradeID=tradeID,
                openclose='cancel'
            )
            self.evt.sendOrder(order)


    def on_marketdatafeed(self, md, ab):
        # вызов стратегий каждые 24 часа
        if md.timestamp >= self.timer+timedelta(hours=24):
            # update timer
            self.timer = md.timestamp

            # ....

            # сброс сетки 
            if md.timestamp>self.grid_setup_time+timedelta(days=7):
                self.grid_setup_time = md.timestamp
                self.reset_grid()
                self.is_runningGrid = False

Полный исходный код

Подытожив вышеперечисленное, рабочий скрипт будет представлен ниже.

from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import pandas_ta as ta

class AlgoEvent:
    def __init__(self):
        self.timer = datetime(1970,1,1)
        self.grid_setup_time = datetime(1970,1,1)
        self.grid_size_pct = 0.01
        self.grid_num = 5
        self.tp_pct = 0.01
        self.sl_pct = 0.01
        self.numObs = 30
        self.is_runningGrid = False
        self.trade_size = 0.1
        
    def start(self, mEvt):
        self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt)
        self.evt.start()
    
    def compute_CHOP(self, data):
        high = [data[t]['h'] for t in data]
        low = [data[t]['l'] for t in data]
        close = [data[t]['c'] for t in data]
        df = pd.DataFrame.from_dict({"high":high,"low":low,"close":close})
        chop_series = ta.chop(high=df['high'], low=df['low'], close=df['close'], length=14, atr_length=1)
        return chop_series.to_numpy()

    def reset_grid(self):
        # отменить все неисполненные ордера
        positions, osOrder, pendOrder = self.evt.getSystemOrders()
        for tradeID in pendOrder:
            order = AlgoAPIUtil.OrderObject(
                tradeID=tradeID,
                openclose='cancel'
            )
            self.evt.sendOrder(order)


    def on_marketdatafeed(self, md, ab):
        # вызов стратегий каждые 24 часа
        if md.timestamp >= self.timer+timedelta(hours=24):
            # update timer
            self.timer = md.timestamp

            # получение последних OHLC свечей
            res = self.evt.getHistoricalBar(
                contract = {"instrument":md.instrument}, 
                numOfBar = self.numObs, 
                interval = "D"
            )
            if len(res)<self.numObs:
                return

            chop_series = self.compute_CHOP(res)

            self.evt.consoleLog(chop_series)

            # проверка нахождения рынка в тренде или в диапазоне
            is_ranging = True
            if chop_series[-1]>50 and chop_series[-2]<=50:
                is_ranging = False

            # установка нового grid'a для is_ranging рынка
            if not self.is_runningGrid and is_ranging:
                self.is_runningGrid = True
                self.grid_setup_time = md.timestamp

                for i in range(1,self.grid_num+1):

                    # лимитный ордер на покупку
                    price = md.bidPrice*(1-i*self.grid_size_pct)
                    #ask = md.askPrice*(1-i*self.grid_size_pct)
                    order = AlgoAPIUtil.OrderObject(
                        instrument = md.instrument,
                        openclose = 'open',
                        buysell = 1,    # 1=buy, -1=sell
                        ordertype = 1,      # 1=limit order
                        price = price,
                        volume = self.trade_size,
                        takeProfitLevel = price*(1+self.tp_pct),
                        stopLossLevel = price*(1-self.sl_pct)
                    )
                    self.evt.sendOrder(order)
                    
                    # лимитный ордер на продажу
                    #bid = md.bidPrice*(1+i*self.grid_size_pct)
                    price = md.askPrice*(1+i*self.grid_size_pct)
                    order = AlgoAPIUtil.OrderObject(
                        instrument = md.instrument,
                        openclose = 'open',
                        buysell = -1,    # 1=buy, -1=sell
                        ordertype = 1,      # 1=limit order
                        price = price,
                        volume = self.trade_size,
                        takeProfitLevel = price*(1-self.tp_pct),
                        stopLossLevel = price*(1+self.sl_pct)
                    )
                    self.evt.sendOrder(order)

            # сброс сетки 
            if md.timestamp>self.grid_setup_time+timedelta(days=7):
                self.grid_setup_time = md.timestamp
                self.reset_grid()
                self.is_runningGrid = False

Результат работы стратегий

Давайте попробуем протестировать скрипт. Настройки теста:

  • Инструмент: BTCUSD
  • Период: 2021.01 – 2021.12
  • Стартовый депозит: $100,000
  • Интервал: 1-час
  • Разрешить Short: True
To top