ВАЖНО: данная статья создана исключительно в образовательных целях. Торговля на бирже связана с большим риском и может привести к значительным убыткам.

Первая часть статьи доступна по ссылке.

Введение (и напоминание)

В предыдущей части статьи мы разобрали, как написать простейшего торгового бота на python с использованием Binance API. Стратегия, которую мы использовали, основывалась на статичном коридоре цены на паре ETHUSDT. Такая стратегия крайне редко применима на практике и в нашем случае служила только в качестве примера, однако, она проиллюстрировала важные методы при торговле с реальными стратегиями.

Кратко напомним основные шаги стратегии из предыдущей части статьи:

  1. Запросить последнюю цену актива на бирже (без разделения на BID или ASK в простейшем случае);

  2. Если мы вышли за верхнюю границу коридора: закрыть лонг и открыть шорт;

  3. Если мы вышли за нижнюю границу коридора: закрыть шорт и открыть лонг.

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

# imports and functions are omitted here as they stay exactly the same 

# flags to track the exchanges to avoid duplicated entries
in_short = False
in_long = False

# main loop
while True:
    # get latest price for the symbol
    latest_price = client.futures_symbol_ticker(symbol=symbol)['price']
    latest_price = float(latest_price)
    
    # check the condition (these conditions could be substitued by any other)
    if "<condintion>":
        # if we are in long position, close it
        if in_long:
            long_close(symbol=symbol, quantity=1)
            in_long = False
        # if we are not in short position, open it
        if not in_short:
            short_open(symbol=symbol, quantity=1)
            in_short = True
    elif "<other_contition>":
        # if we are in long position, close it
        if not in_long:
            long_open(symbol=symbol, quantity=1)
            in_long = True
        # if we are in short position, close it
        if in_short:
            short_close(symbol=symbol, quantity=1)
            in_short = False

    # 1 second sleep
    time.sleep(1)

То есть, если заменить условия <condition> и <other_condition> на некоторые реальные условия, то код выше может служить основой для полноценной торговой стратегии.

Базовые (статистические) стратегии

Переходя к настоящим торговым стратегиям на одном активе (торговля несколькими активами заслуживает отдельного серьезного разговора) хочется выделить три основных направления в статистических стратегиях (статистическими стратегиями будем называть те стратегии, которые основываются только на данных о предыдущей цене и объемах торговли и не учитывают "внешних" факторов влияющих на рынок):

  1. Пересечение скользящих средних (moving average crossover): стратегия рассматривает две скользящие средние с разными периодами и входит в лонг, когда линия с меньшим периодом пересекает линию с бОльшим периодом снизу вверх и входит в шорт, наоборот, при пересечении линии с меньшим периодом линии с бОльшим периодом сверху вниз.

  2. Динамический коридор цены (channel breakout): стратегия рассматривает одну скользящую среднюю а также коридор цены, верхняя граница которого соответствует максимуму цены за предыдущие n свеч, а нижняя -- минимуму за предыдущие n свеч.

  3. Линии Боллинджера (Bollinger bands): стратегия также строит коридор цены, но по другим правилам.

    Верхняя граница коридора = скользящая средняя + 2 стандартных отклонения цены;

    Нижняя граница коридора = скользящая средняя - 2 стандартных отклонения цены.

    Правила для входа и выхода стратегии определяются следующим образом:

    При пересечении цены верхней линии Боллинджера (снизу вверх) открывается шорт. Шорт закрывается при пересечении ценой скользящей средней (сверху вниз).

    При пересечении цены нижней линии Боллинджера (сверху вниз) открывается лонг. Лонг закрывается при пересечении ценой скользящей средней (снизу вверх).

Линии Боллинджера

В данной статье мы подробно разберем линии Боллинджера (реализация других стратегий, после освоения линий Боллинджера, не должна составить труда).

Начнем с определения:

Линии (полосы) Боллинджера (Bollinger bands) — инструмент технического анализа финансовых рынков, отражающий текущие отклонения цены акции, товара или валюты.

Основное предположение стратегии состоит в том, что при значительном отклонении цены от скользящей средней, она вернется назад. Преимущество линий Боллинджера -- простота их вычисления. Представим, что мы имеем данные о ценах закрытия 5 минутных свечей некоторого актива (см таблицу ниже), тогда мы можем вычислить линии Боллинджера используя следующие формулы:

BB_{high}(n) = MA(n) + 2 STD(n);\quad BB_{low}(n) = MA(n) - 2STD(n)

В таблице представлены цены закрытия а также значения скользящих средних и отклонений, а также значения линий Боллиндджера:

Время закрытия

Цена закрытия

Скользящая средняя с периодом 3 свечи (15 минут)

Стандартное отклонение с периодом 3 свечи (15 минут)

Боллинджер верхняя граница

Боллинджер верхняя граница

12:00

100.0

NaN

NaN

NaN

NaN

12:05

100.1

NaN

NaN

NaN

NaN

12:10

100.2

100.1

0.1

100.3

99.9

12:15

100.1

100.133333

0.57735

100.248803

100.017863

12:25

100.3

100.2

0.1

100.4

100.0

12:30

100.5

100.3

0.2

100.7

99.9

12:35

100.2

100.333333

0.152753

100.638838

100.027828

12:40

100.0

100.233333

0.251661

100.736656

99.730011

12:45

99.5

99.9

0.360555

100.621110

99.178890

На рисунке 1 ниже представлены линии Боллинджера (синий и фиолетовый цвета), а также скользящая средняя (оранжевый цвет). Помимо линий Боллинджера представлены точки входа и выхода из сделок (согласно стратегии). Они отмечены вертикальными линиями. Вертикальная пунктирная линия соответствует моменту входа в сделку, а вертикальная сплошная линия -- моменту выхода из сделки. Число рядом с подписью внизу сделки обозначает ее доход (убыток) в процентах.

Рис 1. Линии Боллинджера и скользящая средняя, а также моменты открытия и закрытия сделок в соответствии со стратегией
Рис 1. Линии Боллинджера и скользящая средняя, а также моменты открытия и закрытия сделок в соответствии со стратегией

Имплементация на Python

Теперь мы можем использовать теорию выше для имплементации стратегии на языке python. Существенное отличие текущей стратегии от стратегии статичного коридора цены в первой части статьи состоит в том, что теперь мы хотим получать цену актива только лишь в момент закрытия свечи, то есть, каждые 5 минут (константу 5 минут можно легко изменять).

Вычисление линий Боллинджера

Код ниже показывает, как можно вычислять линии Боллинджера для интервалов в 5 минут с периодом 3 свечи (15 минут):

import numpy as np
import time
from datetime import datetime
from binance.client import Client

# list to keep all closing prices
prices = []

# lists to keep all stats needed
moving_average_values = []
bollinger_band_high_values = []
bollinger_band_low_values = []

# flags to track the exchanges to avoid duplicated entries
in_short = False
in_long = False

# period of Bollinger bands
period = 3

last_time = ""

# main loop
while True:
    current_time = datetime.now()
    # if current candlestick has closed and current time is not equal to the
    # latest record (to avoid duplicated price records)
    if current_time.minute % 5 == 0 and current_time.strftime('%Y-%m-%d %H:%M') != last_time:
        last_time = current_time.strftime('%Y-%m-%d %H:%M')
        # get latest price for the symbol
        latest_price = client.futures_symbol_ticker(symbol=symbol)['price']
        latest_price = float(latest_price)
        prices.append(latest_price)
    
        # print latest price with current time
        print(current_time, latest_price)

        # calculate moving average and deviation
        ma = np.mean(prices[-period:])
        moving_average_values.append(ma)

        std = np.std(prices[-period:], ddof=1)

        # calculate Bollinger bands
        bb_high = ma + 2 * std
        bb_low = ma - 2 * std

        bollinger_band_high_values.append(bb_high)
        bollinger_band_low_values.append(bb_low)

Правила входа (выхода) в сделки

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

# check if current closing price is higher than the upper band
# but previous closing price is lower than the upper band
# this indicates that the crossing occured on current candlestick
if prices[-2] < bollinger_band_high_values[-2] and prices[-1] > bollinger_band_high_values[-1]:
    # if we are not in short position, open it
    if not in_short:
        short_open(symbol=symbol, quantity=1)
        in_short = True

# check if current closing price is lower than the lower band
# but previous closing price is higher than the lower band
# this indicates that the crossing occured on current candlestick
if prices[-1] < bollinger_band_low_values[-1] and prices[-2] > bollinger_band_low_values[-2]:
    # if we are not in long position, open it
    if not in_long:
        long_open(symbol=symbol, quantity=1)
        in_long = True

Теперь разберем условия выхода из сделки:

# check if current closing price is lower than the moving average
# but previous closing price is higher than the moving average
# this indicates that the crossing occured on current candlestick
if prices[-2] > moving_average_values[-2] and prices[-1] < moving_average_values[-1]:
    if in_short:
        short_close(symbol=symbol, quantity=1)
        in_short = False

# vice versa
if prices[-2] < moving_average_values[-2] and prices[-1] > moving_average_values[-1]:
    if in_long:
        long_close(symbol=symbol, quantity=1)
        in_long = False

Наконец, соберем все кусочки вместе и получим полноценную стратегию!

import numpy as np
import time
from datetime import datetime
from binance.client import Client

# exchange operations:
def short_open(symbol, quantity):
    client.futures_create_order(symbol=symbol, 
                                side='SELL', 
                                type='MARKET', 
                                quantity=quantity)
    
def short_close(symbol, quantity):
    client.futures_create_order(symbol=symbol, 
                                side='BUY', 
                                type='MARKET', 
                                quantity=quantity)
    
def long_open(symbol, quantity):
    client.futures_create_order(symbol=symbol, 
                                side='BUY', 
                                type='MARKET', 
                                quantity=quantity)

def long_close(symbol, quantity):
    client.futures_create_order(symbol=symbol, 
                                side='SELL', 
                                type='MARKET', 
                                quantity=quantity)

# initialize Client with your API keys
api_key = "<your_api_key>"
api_secret = "<your_private_api_key>"
client = Client(api_key,  api_secret)

# traded symbol
symbol = "ETHUSDT"

# list to keep all closing prices
prices = []

# lists to keep all stats needed
moving_average_values = []
bollinger_band_high_values = []
bollinger_band_low_values = []

# flags to track the exchanges to avoid duplicated entries
in_short = False
in_long = False

# period of Bollinger bands
period = 3

last_time = ""

# main loop
while True:
    current_time = datetime.now()
    # if current candlestick has closed and current time is not equal to the
    # latest record (to avoid duplicated price records)
    if current_time.minute % 5 == 0 and current_time.strftime('%Y-%m-%d %H:%M') != last_time:
        last_time = current_time.strftime('%Y-%m-%d %H:%M')
        # get latest price for the symbol
        latest_price = client.futures_symbol_ticker(symbol=symbol)['price']
        latest_price = float(latest_price)
        prices.append(latest_price)
    
        # print latest price with current time
        print(current_time, latest_price)

        # calculate moving average and deviation
        ma = np.mean(prices[-period:])
        moving_average_values.append(ma)

        std = np.std(prices[-period:], ddof=1)

        # calculate Bollinger bands
        bb_high = ma + 2 * std
        bb_low = ma - 2 * std

        bollinger_band_high_values.append(bb_high)
        bollinger_band_low_values.append(bb_low)

        # descion of the strategy
        # short exchanges
        if prices[-2] < bollinger_band_high_values[-2] and prices[-1] > bollinger_band_high_values[-1]:
            if not in_short:
                short_open(symbol=symbol, quantity=1)
                in_short = True

        if prices[-2] > moving_average_values[-2] and prices[-1] < moving_average_values[-1]:
            if in_short:
                short_close(symbol=symbol, quantity=1)
                in_short = False

        # long conditions
        if prices[-1] < bollinger_band_low_values[-1] and prices[-2] > bollinger_band_low_values[-2]:
            if not in_long:
                long_open(symbol=symbol, quantity=1)
                in_long = True
        
        if prices[-2] < moving_average_values[-2] and prices[-1] > moving_average_values[-1]:
            if in_long:
                long_close(symbol=symbol, quantity=1)
                in_long = False

    time.sleep(1)

Спасибо за прочтение!

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


  1. milssky
    26.12.2023 19:21
    +2

    Судя по комментам в коде, его писал chatGPT =)


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      Код от начала и до конца (и даже все комментарии) написаны мной лично


  1. Shamanische
    26.12.2023 19:21
    +3

        # if current candlestick closed
        if datetime.now().minute % 5 == 0:
            # get latest price for the symbol
            latest_price = client.futures_symbol_ticker(symbol=symbol)['price']
            latest_price = float(latest_price)
            prices.append(latest_price)

    Вот это условие будет в течение 60 секунд добавлять цены в массив и запрашивать клиентский API, и еще, нужно проверять не текущее время суток, а время изменения цены, иначе, предположим, если рынок "замрет" на час, массив будет наполнен протухшими ценами


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      Да, Вы абсолютно правы, сейчас исправлю. Пытался упростить код, но допустил глупую ошибку :)

      UPD: исправлено, спасибо за Ваше замечание!


      1. Shamanische
        26.12.2023 19:21

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


        1. Alexander_Shulzhenko Автор
          26.12.2023 19:21

          Да, это правда, это можно сделать с помощью функции Klines, описанной в первой части статьи, однако Binance (по крайней мере по моему опыту) крайне редко ломает свечи


      1. ikrusenstern
        26.12.2023 19:21

        Судя по стилю речи этот комментарий писал chatGPT =)


  1. milssky
    26.12.2023 19:21
    +1

    У вас очень много бесполезного дублирования кода.

    '%Y-%m-%d %H:%M'

    Прекрасно выносится в константы. Это удобно, расположено в одном месте.

    prices[-2] < bollinger_band_high_values[-2] and prices[-1] > bollinger_band_high_values[-1]

    Вот эти чудовища с индексами. Получите их один раз из своих контейнеров перед условиями. Аналогично со вторыми с конца.

    Четыре функции вверху, которых на самом деле одна с разными параметрами. Ну или две. Исследуйте partial из functools или просто тупо через равно уберите дублирование кода :)

    Я не очень вникал в предметную область, но недостаточно ли собирать за одну итерацию цикла текущее время? :)


    Так же, ваш код не готов к ошибкам. Что будет если binance приляжет на секунду или запрос от вашего client будет неудачным? Наверняка будет выброшено исключение, которые остановит эту программу. И ее придется запустить заново ручками.


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      В тексте указано, что для простоты функции дублированы, поскольку открыть шорт и закрыть лонг для рынка фьючерсов -- это одно и то же (на этот счет даже написан комментарий, возможно, в первой части статьи). В то же время обработка всех исключений и ошибок сделает код более объемным и трудным для понимания. Задача сделать код оптимальным по количеству символов также не стояла :) Зато, надеюсь, некоторые дублирования помогут читателю лучше понять суть алгоритма


      1. milssky
        26.12.2023 19:21

        Ну помогут. Его будут тупо копировать :)


  1. EPIDEMIASH
    26.12.2023 19:21
    -1

    А зачем, если бинанс не для россии?


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      В целом, биржи используют очень похожие API, поэтому перепрофилировать код для работы с другой биржей не должно быть очень сложно :)


      1. milssky
        26.12.2023 19:21

        Профилирование -- это про другое ;) от вашего кода была бы польза, если бы алгоритм этот был отделен от всей мишуры. Вот это был бы прорыв, самолёты бы начали строить быстрее и все такое :) а сейчас...


  1. dyadyaSerezha
    26.12.2023 19:21

    И главный вопрос - оно того стоит?)


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      Смотря, в чем измерять)


      1. dyadyaSerezha
        26.12.2023 19:21
        +1

        В дополнительных манях в месяц)


        1. Alexander_Shulzhenko Автор
          26.12.2023 19:21

          Красота торговых роботов в том, что они предоставляют безграничный простор для творчества, поэтому успех алгоритма зависит от Вас :)


    1. omgiafs
      26.12.2023 19:21
      +2

      Автоматизированная рулетка.

      Я не очень сообразителен, поэтому мне в своё время понадобилось что-то около недели знакомства с предметом, чтобы понять, что весь этот "технический анализ" - это попытка увидеть будущее, основываясь на графиках прошлого. Причем ладно бы был известен список факторов, влияющих на результат, и их можно было бы как-то мониторить, но там результат зависит от буквально всего. Лично я бы предпочёл иметь в друзьях не самого крутого "технического аналитика", а любимого сыночку верхних работников ЦБ. В первом случае ты пытаешься угадать будущее, во втором - ты его знаешь.


      1. Alexander_Shulzhenko Автор
        26.12.2023 19:21

        Вы во многом правы) Технический анализ -- это действительно "предсказание" будущего на основе некоторых статистик из прошлого (в нашем случае -- скользящего среднего и отклонения). Весь технический анализ держится примерно на том, что рынок ведет себя "согласно некоторому паттерну", который и может описываться правилами технического анализа. И иногда это правда, поскольку, если углубиться в теорию случайных процессов, первое что Вы увидите -- это (скорее всего), что цена актива (P_t) не стационарный процесс, а значит не имеет свойства "отката" (mean-reverting property). Однако локально (на этом и держится тех анализ), возможно, это и имеет место быть...
        Но, на моем опыте, техническому анализу очень мешают всплески волатильности (и резкие движения (скачки) цены), которые ведут к значительным потерям. По поводу предсказания всплесков волатильности можете посмотреть мой репозиторий с моделью)
        https://github.com/AlexanderShulzhenko/Short-term-Volatility-Prediction


        1. dyadyaSerezha
          26.12.2023 19:21

          Репориторий с любой прекрасной моделью не говорит ни о чем. Значение имеет только одно - насколько он лучше рынка по прибыли.


          1. Alexander_Shulzhenko Автор
            26.12.2023 19:21

            Так я и не утверждал ничего с помощью репозитория. Единственный посыл был в том, что "вот можно пытаться предсказывать всплески волатильности с помощью каких-то признаков"


      1. dyadyaSerezha
        26.12.2023 19:21

        Не могу не согласиться полностью, назвав это до кучи гаданием на кофейной гуще. Не в смысле примененной теории, а в смысле конечного результата.

        Уверен, что большинство проф.трейдеров имеют все эти анализы нв 10 порядков круче, глубже и ширше, но где-то читал про исследование о том, что половина проф.трейдеров имеют итоговый процент роста их фондов меньше среднего по рынку (брать случайные акции или по индексу) - что и должно быть - если кто-то выигрывает, то только за счёт того, что кто-то проигрывает.


        1. omgiafs
          26.12.2023 19:21

          Так вся биржевая торговля - игра с нулевой суммой. Особенно - бинарные опционы, их в части европейских государств признали просто азартной игрой. Все пытаются именно что выиграть у остальных. При этом тратя кучу ресурсов на, фактически, перераспределенние ресурсов. Меня в том числе и это оттолкнуло от всей биржевой торговли, потому что никакой пользы обществу ты не приносишь этой деятельностью. Да и себе тоже, по большому счёту. Стал богаче (конкретно ты - ой, вряд ли), отобрав у другого такого же лудомана. Просто жажда наживы и всё. Поэтому вокруг этой темы куча инфоцыган, у которых в ЦА, судя по картинкам и текстам в ТГ, аудитория подросткового и чуть выше возраста, так как истории успешного успеха у каждого второго на фото - "с пачками деняк в салоне бэхи". Для лудоманов постарше предлагают Баффета, сдабривая это книжками тех единиц, которые пришли к успеху. Те сотни и тысячи, чьи деньги успешным единицам перетекли в результате "торговли", увы, книг не написали.

          Чисто технически вся эта "торговля" - задача интересная, к тому же азарт наживы подогревает интерес. Но лично я, узнав про суть всей движухи, по этическим причинам заниматься этой гадостью не буду. Для меня деньги "пахнут". Потому я неконкурентоспособный и сознательно выбрал проигрыш в эволюционной борьбе.


  1. IvanTheCrazy
    26.12.2023 19:21

    А почему бы не использовать talib для вычисления тех же полос боллинджера? Одна строчка и готово


    1. Alexander_Shulzhenko Автор
      26.12.2023 19:21

      Спасибо что упомянули talib, хотел написать об этой библиотеке в следующей части. Конечно, можно использовать talib для простоты, однако, я хотел показать именно техническую сторону вычисления индикаторов


  1. nnh
    26.12.2023 19:21
    +1

    А как этот API решает вопросы контроля? В коде есть магические команды, типа: client.futures_create_order

    Но, это же деньги. Значит контроль должен быть. И по своему опыту, говоря о любых скриптах такого рода, первое о чем надо говорить - это не стратегии, а том как это все заставить работать постоянно, с восстановлением состояния после падений, контролем исполнения команд и т.д. И это будет большая часть кода. Но постоянно вижу скрипты где этот вопрос вообще не разбирается, а есть только алгоритмы принятия решений. Хотя они по большей части бессмысленны.