Перед прочтением этой статьи — ВАЖНО следующее: основная цель данной статьи заключается в том, чтобы показать как просто можно создать торгового робота, который может торговать российскими акциями или зарубежными акциями. Важно понимать, что создавая бота, вы лично н��сете ответственность за принимаемые им решения, инвестиционные операции и связанные с ними риски. Я не несу ответственности за решения, которые вы можете принять после прочтения этого материала. И я не даю никаких инвестиционных рекомендаций или советов. Не забывайте, что боты способны принести большие убытки, поэтому используйте их с осторожностью.

Пару слов обо мне

Программирование для меня это хобби и любимое дело. А так я сертифицированный системный архитектор. Поэтому прошу не особо ругать за код:‑)

Выбор брокера и библиотек

Как вы знаете, брокеров много))) но нам нужны те, у которых есть API — программный интерфейс через который наш торговый робот сможет отправлять заявки на покупку и продажу акций.

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

Чтобы вас долго не мучать с выбором хорошего брокера для торгового бота, я приведу мои решения, которые сформировались после длительной практики по написанию торговых ботов, работающих в live режиме — прямо сейчас торгующих российскими акциями.

список сделок за сегодня, таймфрейм H1
список сделок за сегодня, таймфрейм H1

Нужный и важный компонент в разработке торгового бота — это возможность тестирования вашей стратегии на истории, например используя простую библиотеку BackTrader.

Итак приступим! )))

1й вариант — если очень сильно хочется иметь робота, который может торговать практически через любого брокера — то есть очень хорошее решение использовать библиотеку QuikPy в связке с библиотекой BackTraderQuik — использование этих двух библиотек позволит вашему торговому роботу работать с любым брокером, у которого есть возможность предоставить вам торговый терминал Quik. А этот торговый терминал есть у большинства брокеров.

Если вам интересно, как это настроить и сделать, я могу рассказать в отдельной подробной статье, просто голосуйте за это! )))

Множество примеров по этой связке специально для вас выложил вот здесь.

2й вариант — он немного ограничивает в выборе брокеров, но даёт прекрасную возможность общаться с разработчиками API брокеров, и они!! заметьте быстро и эффективно исправляют косяки) и добавляют функционал — это большой плюс!

Для брокера Финам — API еще в разработке ))) библиотеки FinamPy + BackTraderFinam

Для брокера Тинькофф — библиотека BackTraderTinkoff
* Несколько примеров кода опубликовал в их репозитории — пример стратегии которая использует только API Тинькофф

Для брокера Алор — библиотеки AlorPy и BackTraderAlor

ОФФТОПИК: Если кому интересно подключение к криптобирже — то я написал свою библиотеку backtrader_binance, она работает так же, т. е. один и тот же код, можно использовать для разных активов, вот про нее статья.

Итак, выбираем последнего брокера — Алор. )) Если вам интересно увидеть как написать торгового робота для Финам или Тинькофф — как это настроить и сделать, я могу рассказать в отдельной подробной статье, просто голосуйте за это! )))

Приступаем к написанию торгового бота

Подготовка окружения

  1. Устанавливаем последнюю версию Python 3.11;

  2. Устанавливаем среду разработки PyCharm Community 2023.1;

  3. Запускаем PyCharm Community;

  4. В нём создаем новый проект, давайте его назовём alor_trade_robot и укажем что создаем виртуальное окружение Virtualenv, с Python 3.11 => нажимаем "Create";

Создание нового проекта для алго-трейдинга
Создание нового проекта для алго-трейдинга
  1. После того, как проект создался и в нём создалось виртуальное окружение, мы стали готовы к установке необходимых библиотек))) Кликаем внизу слева на "Terminal" для открытия терминала, в котором как раз и будем вводить команды установки библиотек;

Открытый терминал проекта
Открытый терминал проекта
  1. Устанавливаем необходимые библиотеки:

    В терминале вводим команды для подключения к брокеру Алор по API:

    git clone https://github.com/WISEPLAT/AlorPy

    для интеграции API Алора с Backtrader:

    git clone https://github.com/WISEPLAT/BackTraderAlor

    после этих манипуляций у нас появилось две папки AlorPy и BackTraderAlor

    установка AlorPy и BackTraderAlor
    установка AlorPy и BackTraderAlor

Теперь необходимо установить библиотеку тестирования торговых стратегий Backtrader

pip install git+https://github.com/WISEPLAT/backtrader.git

P.S. Пожалуйста, используйте Backtrader из моего репозитория (так как вы можете размещать в нем свои коммиты).

И наконец у нас есть некоторые зависимости, которые вам нужно так же установить

pip install requests pytz websockets matplotlib

Создание конфигурации для торговой стратегии

Чтобы было легче разобраться с этими библиотеками, есть множество примеров внутри этих папок AlorPy и BackTraderAlor.

Нам нужны два файла 02 - Symbols.py и Strategy.py из папки BackTraderAlor\DataExamples

02 - Symbols.py и Strategy.py
02 - Symbols.py и Strategy.py

Копируем их в корень нашего проекта.

помещаем файлы в корень проекта
помещаем файлы в корень проекта

Также нам понадобится файл конфигурации, он находится в папке AlorPy\Config.py

файл конфигурации
файл конфигурации

Его тоже копируем в корень проекта в папку my_config(её нужно создать), должно получиться так:

создаем свой файл конфигурации
создаем свой файл конфигурации

Перед запуском примера 02 — Symbols.py, необходимо:

1) получить свой API ключ и вписать его в поле RefreshToken;

2) узнать свой UserName и вписать его в поле UserName;

3) узнать значение портфеля для Фондового рынка и вписать его в поле PortfolioStocks
— через файл AlorPy\Examples\02 — Accounts.py — получаем это значение;

4) узнать значение портфеля для Срочного рынка и вписать его в поле PortfolioFutures
— через файл AlorPy\Examples\02 — Accounts.py — получаем это значение;

5) узнать значение портфеля для Валютного рынка и вписать его в поле PortfolioFx
— через файл AlorPy\Examples\02 — Accounts.py — получаем это значение.

Напоминаю, что всё это прописываем в файле my_config\Config.py

И как все эти значения получить, так же прописано в этом же файле.

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

Такой конфиг примерно получится:

Как получить токен Refresh Token

1) Открыть счет в Алор (можно сделать удаленно!);

2) Для получения тестового логина/пароля демо счета оставить заявку в Telegram на https://t.me/AlorOpenAPI;

3) Зарегистрироваться на https://alor.dev/login;

4) Выбрать «Токены для доступа к API».

Проверки подключения к Алору через API

В файле 02 — Symbols.py мы должны подключить свой конфиг файл my_config\Config.py для этого открываем его и правим одну строку:

меняем AlorPy.Config на my_config.Config станет так:

назначение своего конфиг файла
назначение своего конфиг файла

Отключите LiveBars режим, установив LiveBars=False

Теперь запускаем пример для проверки подключения... Должно получиться так:

LifeDars=False, и бары пришли
LifeDars=False, и бары пришли

Как мы видим — тест удался — бары пришли.

Теперь можно приступать к созданию первого торгового робота!!

Создание торгового робота для торговли акциями

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

импорт необходимых_библиотек

класс Индикаторов

класс Стратегии/Торговой системы

# --- основной раздел ---
подключение по API к бирже
задание параметров запуска стратегии
запуск стратегии
  получение данных по тикеру/тикерам по API
  обработка этих данных стратегией
  выставление заявок на покупку/продажу
возврат результатов из стратегии
вывод результатов

Как мы видим, нам осталось реализовать пункты:

  • «обработка этих данных стратегией»;

  • «выставление заявок на покупку/продажу»;

  • возврат результатов из стратегии;

  • вывод результатов.

Остальные пункты сделаны в этом примере — код достаточно интуитивный, но я всё равно приведу его здесь, ещё раз, отключив лишнее — комментируя — специально не удаляя, чтобы вам легче было сравнить.

Итак основной файл для запуска торговой стратегии называется 02 — Symbols.py, вот его код:

from datetime import date, datetime
from backtrader import Cerebro, TimeFrame
from BackTraderAlor.ALStore import ALStore  # Хранилище Alor
from my_config.Config import Config  # Файл конфигурации
import Strategy as ts  # Торговые системы

# Несколько тикеров для нескольких торговых систем по одному временнОму интервалу
if __name__ == '__main__':  # Точка входа при запуске этого скрипта
    symbols = ('MOEX.SBER', 'MOEX.GAZP', 'MOEX.LKOH', 'MOEX.GMKN',)  # Кортеж тикеров
    store = ALStore(UserName=Config.UserName, RefreshToken=Config.RefreshToken, Boards=Config.Boards, Accounts=Config.Accounts)  # Хранилище Alor
    cerebro = Cerebro(stdstats=False)  # Инициируем "движок" BackTrader. Стандартная статистика сделок и кривой доходности не нужна
    for symbol in symbols:  # Пробегаемся по всем тикерам
        # data = store.getdata(dataname=symbol, timeframe=TimeFrame.Minutes, compression=1, fromdate=date.today(), LiveBars=False)  # Исторические и новые бары тикера с начала сессии
        data = store.getdata(dataname=symbol, timeframe=TimeFrame.Minutes, compression=15, fromdate=datetime(2021, 10, 4), LiveBars=False)  # Исторические и новые бары тикера с начала сессии
        cerebro.adddata(data)  # Добавляем тикер
    # cerebro.addstrategy(ts.PrintStatusAndBars, name="One Ticker", symbols=('MOEX.SBER',))  # Добавляем торговую систему по одному тикеру
    # cerebro.addstrategy(ts.PrintStatusAndBars, name="Two Tickers", symbols=('MOEX.GAZP', 'MOEX.LKOH',))  # Добавляем торговую систему по двум тикерам
    cerebro.addstrategy(ts.PrintStatusAndBars, name="All Tickers")  # Добавляем торговую систему по всем тикерам
    cerebro.run()  # Запуск торговой системы

Внесенные в него изменения:

  • поменял таймфрейм на M15;

  • оставил применение торговой системы/стратегии ко всем тикерам;

  • и дату старта получения баров установил на datetime(2021, 10, 4).

Теперь основной файл стратегии Strategy.py, вот его код:

import backtrader as bt


class PrintStatusAndBars(bt.Strategy):
    """
    - Отображает статус подключения
    - При приходе нового бара отображает его цены/объем
    - Отображает статус перехода к новым барам
    """
    params = (  # Параметры торговой системы
        ('name', None),  # Название торговой системы
        ('symbols', None),  # Список торгуемых тикеров. По умолчанию торгуем все тикеры
    )

    def log(self, txt, dt=None):
        """Вывод строки с датой на консоль"""
        dt = bt.num2date(self.datas[0].datetime[0]) if not dt else dt  # Заданная дата или дата последнего бара первого тикера ТС
        print(f'{dt.strftime("%d.%m.%Y %H:%M")}, {txt}')  # Выводим дату и время с заданным текстом на консоль

    def __init__(self):
        """Инициализация торговой системы"""
        self.isLive = False  # Сначала будут приходить исторические данные

    def next(self):
        """Приход нового бара тикера"""
        # if self.p.name:  # Если указали название торговой системы, то будем ждать прихода всех баров
        #     lastdatetimes = [bt.num2date(data.datetime[0]) for data in self.datas]  # Дата и время последнего бара каждого тикера
        #     if lastdatetimes.count(lastdatetimes[0]) != len(lastdatetimes):  # Если дата и время последних баров не идентичны
        #         return  # то еще не пришли все новые бары. Ждем дальше, выходим
        #     print(self.p.name)
        for data in self.datas:  # Пробегаемся по всем запрошенным тикерам
            if not self.p.symbols or data._name in self.p.symbols:  # Если торгуем все тикеры или данный тикер
                self.log(f'{data._name} - {bt.TimeFrame.Names[data.p.timeframe]} {data.p.compression} - Open={data.open[0]:.2f}, High={data.high[0]:.2f}, Low={data.low[0]:.2f}, Close={data.close[0]:.2f}, Volume={data.volume[0]:.0f}',
                         bt.num2date(data.datetime[0]))

    def notify_data(self, data, status, *args, **kwargs):
        """Изменение статсуса приходящих баров"""
        data_status = data._getstatusname(status)  # Получаем статус (только при LiveBars=True)
        print(f'{data._name} - {self.p.name} - {data_status}')  # Статус приходит для каждого тикера отдельно
        self.isLive = data_status == 'LIVE'  # В Live режим переходим после перехода первого тикера

Внесенные изменения в него:

  • в функции def next(self): - закомментил синхронность получения баров - т.к. для моей стратегии это не нужно.

Делаем контрольный запуск, чтобы удостовериться, что все работает и ничего не сломали:

контрольный запуск
контрольный запуск

Видим, что пришли 15-минутные бары и все ОК.

....
24.04.2023 19:45, MOEX.GMKN - Minutes 15 - Open=15676.00, High=15682.00, Low=15672.00, Close=15678.00, Volume=216
24.04.2023 20:00, MOEX.SBER - Minutes 15 - Open=235.23, High=235.23, Low=235.03, Close=235.07, Volume=10064
24.04.2023 20:00, MOEX.GAZP - Minutes 15 - Open=181.87, High=182.00, Low=181.80, Close=181.94, Volume=13728
24.04.2023 20:00, MOEX.LKOH - Minutes 15 - Open=4707.50, High=4707.50, Low=4705.00, Close=4706.00, Volume=1439
24.04.2023 20:00, MOEX.GMKN - Minutes 15 - Open=15672.00, High=15682.00, Low=15672.00, Close=15682.00, Volume=153
24.04.2023 20:15, MOEX.SBER - Minutes 15 - Open=235.03, High=235.11, Low=235.00, Close=235.06, Volume=11403
24.04.2023 20:15, MOEX.GAZP - Minutes 15 - Open=181.87, High=182.30, Low=181.85, Close=182.16, Volume=29052
24.04.2023 20:15, MOEX.LKOH - Minutes 15 - Open=4706.00, High=4714.00, Low=4705.50, Close=4714.00, Volume=2092
24.04.2023 20:15, MOEX.GMKN - Minutes 15 - Open=15682.00, High=15682.00, Low=15676.00, Close=15682.00, Volume=265
24.04.2023 20:30, MOEX.SBER - Minutes 15 - Open=235.06, High=235.11, Low=235.03, Close=235.05, Volume=4995
24.04.2023 20:30, MOEX.GAZP - Minutes 15 - Open=182.16, High=182.23, Low=181.91, Close=181.96, Volume=18775
24.04.2023 20:30, MOEX.LKOH - Minutes 15 - Open=4714.00, High=4714.00, Low=4711.00, Close=4712.00, Volume=642
24.04.2023 20:30, MOEX.GMKN - Minutes 15 - Open=15682.00, High=15694.00, Low=15680.00, Close=15692.00, Volume=440
24.04.2023 20:45, MOEX.SBER - Minutes 15 - Open=235.04, High=235.11, Low=235.02, Close=235.06, Volume=10582
24.04.2023 20:45, MOEX.GAZP - Minutes 15 - Open=181.96, High=181.96, Low=181.70, Close=181.81, Volume=13281
24.04.2023 20:45, MOEX.LKOH - Minutes 15 - Open=4712.00, High=4714.00, Low=4710.00, Close=4711.00, Volume=2349
24.04.2023 20:45, MOEX.GMKN - Minutes 15 - Open=15694.00, High=15696.00, Low=15686.00, Close=15686.00, Volume=180
24.04.2023 21:00, MOEX.SBER - Minutes 15 - Open=235.06, High=235.10, Low=235.01, Close=235.10, Volume=7533
24.04.2023 21:00, MOEX.GAZP - Minutes 15 - Open=181.81, High=181.94, Low=181.75, Close=181.91, Volume=5271
24.04.2023 21:00, MOEX.LKOH - Minutes 15 - Open=4711.00, High=4711.00, Low=4710.00, Close=4711.00, Volume=1955
24.04.2023 21:00, MOEX.GMKN - Minutes 15 - Open=15688.00, High=15694.00, Low=15688.00, Close=15692.00, Volume=137
24.04.2023 21:15, MOEX.SBER - Minutes 15 - Open=235.10, High=235.15, Low=235.09, Close=235.15, Volume=9973
24.04.2023 21:15, MOEX.GAZP - Minutes 15 - Open=181.91, High=181.97, Low=181.82, Close=181.89, Volume=7075
24.04.2023 21:15, MOEX.LKOH - Minutes 15 - Open=4711.00, High=4713.00, Low=4710.50, Close=4712.50, Volume=1519
24.04.2023 21:15, MOEX.GMKN - Minutes 15 - Open=15692.00, High=15698.00, Low=15688.00, Close=15698.00, Volume=229
24.04.2023 21:30, MOEX.SBER - Minutes 15 - Open=235.14, High=235.18, Low=235.11, Close=235.17, Volume=7582
24.04.2023 21:30, MOEX.GAZP - Minutes 15 - Open=181.87, High=182.14, Low=181.86, Close=182.03, Volume=9900
24.04.2023 21:30, MOEX.LKOH - Minutes 15 - Open=4713.00, High=4713.00, Low=4708.50, Close=4709.50, Volume=2170
24.04.2023 21:30, MOEX.GMKN - Minutes 15 - Open=15696.00, High=15698.00, Low=15690.00, Close=15694.00, Volume=73
24.04.2023 21:45, MOEX.SBER - Minutes 15 - Open=235.16, High=235.30, Low=235.13, Close=235.15, Volume=22209
24.04.2023 21:45, MOEX.GAZP - Minutes 15 - Open=182.03, High=182.15, Low=182.01, Close=182.01, Volume=5444
24.04.2023 21:45, MOEX.LKOH - Minutes 15 - Open=4709.50, High=4712.00, Low=4708.00, Close=4710.50, Volume=894
24.04.2023 21:45, MOEX.GMKN - Minutes 15 - Open=15690.00, High=15696.00, Low=15660.00, Close=15694.00, Volume=938
24.04.2023 22:00, MOEX.SBER - Minutes 15 - Open=235.15, High=235.26, Low=235.11, Close=235.22, Volume=11980
24.04.2023 22:00, MOEX.GAZP - Minutes 15 - Open=182.04, High=182.13, Low=181.90, Close=182.11, Volume=10208
24.04.2023 22:00, MOEX.LKOH - Minutes 15 - Open=4711.00, High=4719.00, Low=4709.00, Close=4717.00, Volume=4481
24.04.2023 22:00, MOEX.GMKN - Minutes 15 - Open=15694.00, High=15700.00, Low=15690.00, Close=15700.00, Volume=444
24.04.2023 22:15, MOEX.SBER - Minutes 15 - Open=235.25, High=235.28, Low=235.22, Close=235.22, Volume=8336
24.04.2023 22:15, MOEX.GAZP - Minutes 15 - Open=182.11, High=182.11, Low=181.96, Close=182.05, Volume=5406
24.04.2023 22:15, MOEX.LKOH - Minutes 15 - Open=4718.50, High=4719.00, Low=4716.50, Close=4719.00, Volume=2532
24.04.2023 22:15, MOEX.GMKN - Minutes 15 - Open=15700.00, High=15702.00, Low=15688.00, Close=15700.00, Volume=161
24.04.2023 22:30, MOEX.SBER - Minutes 15 - Open=235.24, High=235.24, Low=235.18, Close=235.20, Volume=3598
24.04.2023 22:30, MOEX.GAZP - Minutes 15 - Open=182.05, High=182.07, Low=181.96, Close=181.99, Volume=6737
24.04.2023 22:30, MOEX.LKOH - Minutes 15 - Open=4718.50, High=4719.00, Low=4716.50, Close=4717.00, Volume=1011
24.04.2023 22:30, MOEX.GMKN - Minutes 15 - Open=15698.00, High=15700.00, Low=15688.00, Close=15698.00, Volume=134
24.04.2023 22:45, MOEX.SBER - Minutes 15 - Open=235.20, High=235.20, Low=235.02, Close=235.08, Volume=14638
24.04.2023 22:45, MOEX.GAZP - Minutes 15 - Open=182.00, High=182.11, Low=181.99, Close=182.06, Volume=3898
24.04.2023 22:45, MOEX.LKOH - Minutes 15 - Open=4718.00, High=4719.00, Low=4714.00, Close=4717.00, Volume=1931
24.04.2023 22:45, MOEX.GMKN - Minutes 15 - Open=15692.00, High=15700.00, Low=15692.00, Close=15692.00, Volume=185
24.04.2023 23:00, MOEX.SBER - Minutes 15 - Open=235.09, High=235.15, Low=235.03, Close=235.10, Volume=9085
24.04.2023 23:00, MOEX.GAZP - Minutes 15 - Open=182.06, High=182.09, Low=182.00, Close=182.03, Volume=7404
24.04.2023 23:00, MOEX.LKOH - Minutes 15 - Open=4716.50, High=4719.00, Low=4715.00, Close=4718.00, Volume=1214
24.04.2023 23:00, MOEX.GMKN - Minutes 15 - Open=15698.00, High=15710.00, Low=15690.00, Close=15690.00, Volume=604
24.04.2023 23:15, MOEX.SBER - Minutes 15 - Open=235.10, High=235.17, Low=235.03, Close=235.14, Volume=12324
24.04.2023 23:15, MOEX.GAZP - Minutes 15 - Open=182.03, High=182.10, Low=181.69, Close=181.86, Volume=19210
24.04.2023 23:15, MOEX.LKOH - Minutes 15 - Open=4717.50, High=4717.50, Low=4714.50, Close=4716.00, Volume=509
24.04.2023 23:15, MOEX.GMKN - Minutes 15 - Open=15698.00, High=15700.00, Low=15688.00, Close=15694.00, Volume=78

Теперь можно приступать к самому интересному - написанию торговой стратегии для робота!

Пишем стратегию для торгового робота

Все будем тестировать на истории - делать backtesting для нашей торговой стратегии.

1) устанавливаем, сколько денег у нас на счету и размер комиссии

cerebro.broker.setcash(3000000)  # Устанавливаем сколько денег
cerebro.broker.setcommission(commission=0.01)  # Установить комиссию

2) результат работы торговой стратегии возвращаем в главный файл и выводим на экран

print('Стоимость портфеля: %.2f' % cerebro.broker.getvalue())
print('Свободные средства: %.2f' % cerebro.broker.get_cash())

пункты 1) и 2) добавляем в файл 02 - Symbols.py

3) если вы захотите включить live режим работы вашей торговой стратегии, то это делается следующими четырьмя строчками ‑!!! но не рекомендую этого делать, т.к. все заявки на покупку и продажу сразу начнут попадать на биржу и будут пытаться выполняться так, как у вас написано в коде!! - если вы пробегаетесь по истории - то и скрипт будет пытаться выставить в рынок по "старой" цене... а текущая цена далеко уже не та... БУДЬТЕ ЗДЕСЬ ВНИМАТЕЛЬНЫ! Для live режима — не пробегайтесь по истории.

exchange = 'MOEX'  # Биржа
portfolio = Config.PortfolioStocks  # Портфель фондового рынка    
broker = store.getbroker(use_positions=False, portfolio=portfolio, exchange=exchange)  # Брокер Alor
cerebro.setbroker(broker)  # Устанавливаем брокера

Этот код не добавляем! Просто для инфо, как включить live режим.

Класс торговой системы имеет несколько основных методов:

  1. init - итак понятно - здесь инициализируем вспомогательные переменные и индикаторы для потоков данных;

  2. start - здесь однократно вспомогательным переменным присваиваем значения;

  3. next - вызывается каждый раз при приходе нового бара по тикеру;

  4. notify_order - вызывается, когда происходит покупка или продажа;

  5. notify_trade - вызывается когда меняется статус позиции;

  6. notify_data - вызывается когда меняется статус прихода бара на live режим.

Вы можете по желанию расширять/добавлять новые методы/функционал.

4) В init добавляем:

self.order = None
self.orders_bar_executed = {}

5) В start добавляем:

for data in self.datas:  # Пробегаемся по всем запрошенным тикерам
    ticker = data._dataname  # имя тикера
    self.orders_bar_executed[ticker] = 0

6) В next добавляем:

ticker = data._dataname  # имя тикера
_close = data.close[0]  # текущий close
_low = data.low[0]  # текущий low
_high = data.high[0]  # текущий high
_open = data.open[0]  # текущий close

# Проверка, мы в рынке?
if not self.position:
    # Ещё нет... мы МОГЛИ БЫ КУПИТЬ, если бы...
    if self.data.close[0] < self.data.close[-1]:
        # текущее закрытие меньше предыдущего закрытия
        if self.data.close[-1] < self.data.close[-2]:
            # ПОКУПАЙ, ПОКУПАЙ, ПОКУПАЙ!!! (с параметрами по умолчанию)
            self.log('BUY CREATE, %.2f' % self.data.close[0])
            # Следим за созданным ордером, чтобы избежать второго дублирующегося ордера
            self.order = self.buy(data=data)  # , size=size)
else:
    # Уже в рынке? ... мы могли бы продать
    try:
        # продаём после 5 баров от момента покупки...
        if len(self) >= (self.orders_bar_executed[data._name] + 5):
            # ПРОДАВАЙ, ПРОДАВАЙ, ПРОДАВАЙ!!! (с параметрами по умолчанию)
            self.log('SELL CREATE, %.2f' % self.data.close[0])
            # Следим за созданным ордером, чтобы избежать второго дублирующегося ордера
            self.order = self.sell(data=data)
    except:
        print("error...")

7) добавляем функцию notify_trade:

def notify_trade(self, trade):
    if not trade.isclosed:
        return
    self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm))

8) добавляем функцию notify_order:

def notify_order(self, order):
    ticker = order.data._name
    size = order.size

    if order.status in [order.Submitted, order.Accepted]:
        # Buy/Sell order submitted/accepted to/by broker - Nothing to do
        return

    # Проверка, мы в рынке?
    # Внимание: брокер может отклонить заявку, если недостаточно денег
    if order.status in [order.Completed]:
        if order.isbuy():
            self.log('BUY EXECUTED, %.2f' % order.executed.price)
        elif order.issell():
            self.log('SELL EXECUTED, %.2f' % order.executed.price)

        self.bar_executed = len(self)
        self.orders_bar_executed[order.data._name] = len(self)

    elif order.status in [order.Canceled, order.Margin, order.Rejected]:
        self.log('Order Canceled/Margin/Rejected')

    # Запись: отложенного ордера - нет
    self.order = None

пункты 4), 5), 6), 7) и 8) добавляем в файл стратегии Strategy.py

В принципе описание кода достаточно интуитивно показывает смысл стратегии, что мы всегда покупаем и продаем через 5 баров.

Стратегия: мы всегда покупаем и продаем через 5 баров
Стратегия: мы всегда покупаем и продаем через 5 баров

Итак, код файла 02 - Symbols.py:

from datetime import date, datetime
from backtrader import Cerebro, TimeFrame
from BackTraderAlor.ALStore import ALStore  # Хранилище Alor
from my_config.Config import Config  # Файл конфигурации
import Strategy as ts  # Торговые системы

# Несколько тикеров для нескольких торговых систем по одному временнОму интервалу
if __name__ == '__main__':  # Точка входа при запуске этого скрипта
    symbols = ('MOEX.SBER', 'MOEX.GAZP', 'MOEX.LKOH', 'MOEX.GMKN',)  # Кортеж тикеров
    store = ALStore(UserName=Config.UserName, RefreshToken=Config.RefreshToken, Boards=Config.Boards, Accounts=Config.Accounts)  # Хранилище Alor
    cerebro = Cerebro(stdstats=False)  # Иници��руем "движок" BackTrader. Стандартная статистика сделок и кривой доходности не нужна

    cerebro.broker.setcash(3000000)  # Устанавливаем сколько денег
    cerebro.broker.setcommission(commission=0.01)  # Установить комиссию

    for symbol in symbols:  # Пробегаемся по всем тикерам
        # data = store.getdata(dataname=symbol, timeframe=TimeFrame.Minutes, compression=1, fromdate=date.today(), LiveBars=False)  # Исторические и новые бары тикера с начала сессии
        data = store.getdata(dataname=symbol, timeframe=TimeFrame.Minutes, compression=15, fromdate=datetime(2021, 10, 4), LiveBars=False)  # Исторические и новые бары тикера с начала сессии
        cerebro.adddata(data)  # Добавляем тикер
    # cerebro.addstrategy(ts.PrintStatusAndBars, name="One Ticker", symbols=('MOEX.SBER',))  # Добавляем торговую систему по одному тикеру
    # cerebro.addstrategy(ts.PrintStatusAndBars, name="Two Tickers", symbols=('MOEX.GAZP', 'MOEX.LKOH',))  # Добавляем торговую систему по двум тикерам
    cerebro.addstrategy(ts.PrintStatusAndBars, name="All Tickers")  # Добавляем торговую систему по всем тикерам

    results = cerebro.run()  # Запуск торговой системы
    print('Стоимость портфеля: %.2f' % cerebro.broker.getvalue())
    print('Свободные средства: %.2f' % cerebro.broker.get_cash())

Итак, код файла Strategy.py

import backtrader as bt


class PrintStatusAndBars(bt.Strategy):
    """
    - Отображает статус подключения
    - При приходе нового бара отображает его цены/объем
    - Отображает статус перехода к новым барам
    """
    params = (  # Параметры торговой системы
        ('name', None),  # Название торговой системы
        ('symbols', None),  # Список торгуемых тикеров. По умолчанию торгуем все тикеры
    )

    def log(self, txt, dt=None):
        """Вывод строки с датой на консоль"""
        dt = bt.num2date(self.datas[0].datetime[0]) if not dt else dt  # Заданная дата или дата последнего бара первого тикера ТС
        print(f'{dt.strftime("%d.%m.%Y %H:%M")}, {txt}')  # Выводим дату и время с заданным текстом на консоль

    def __init__(self):
        """Инициализация торговой системы"""
        self.isLive = False  # Сначала будут приходить исторические данные
        self.order = None
        self.orders_bar_executed = {}

    def start(self):
        for data in self.datas:  # Пробегаемся по всем запрошенным тикерам
            ticker = data._dataname  # имя тикера
            self.orders_bar_executed[ticker] = 0

    def next(self):
        """Приход нового бара тикера"""
        # if self.p.name:  # Если указали название торговой системы, то будем ждать прихода всех баров
        #     lastdatetimes = [bt.num2date(data.datetime[0]) for data in self.datas]  # Дата и время последнего бара каждого тикера
        #     if lastdatetimes.count(lastdatetimes[0]) != len(lastdatetimes):  # Если дата и время последних баров не идентичны
        #         return  # то еще не пришли все новые бары. Ждем дальше, выходим
        #     print(self.p.name)
        for data in self.datas:  # Пробегаемся по всем запрошенным тикерам
            if not self.p.symbols or data._name in self.p.symbols:  # Если торгуем все тикеры или данный тикер
                self.log(f'{data._name} - {bt.TimeFrame.Names[data.p.timeframe]} {data.p.compression} - Open={data.open[0]:.2f}, High={data.high[0]:.2f}, Low={data.low[0]:.2f}, Close={data.close[0]:.2f}, Volume={data.volume[0]:.0f}',
                         bt.num2date(data.datetime[0]))

                ticker = data._dataname  # имя тикера
                _close = data.close[0]  # текущий close
                _low = data.low[0]  # текущий low
                _high = data.high[0]  # текущий high
                _open = data.open[0]  # текущий close

                # Проверка, мы в рынке?
                if not self.position:
                    # Ещё нет... мы МОГЛИ БЫ КУПИТЬ, если бы...
                    if self.data.close[0] < self.data.close[-1]:
                        # текущее закрытие меньше предыдущего закрытия
                        if self.data.close[-1] < self.data.close[-2]:
                            # ПОКУПАЙ, ПОКУПАЙ, ПОКУПАЙ!!! (с параметрами по умолчанию)
                            self.log('BUY CREATE, %.2f' % self.data.close[0])
                            # Следим за созданным ордером, чтобы избежать второго дублирующегося ордера
                            self.order = self.buy(data=data)  # , size=size)
                else:
                    # Уже в рынке? ... мы могли бы продать
                    try:
                        # продаём после 5 баров от момента покупки...
                        if len(self) >= (self.orders_bar_executed[data._name] + 5):
                            # ПРОДАВАЙ, ПРОДАВАЙ, ПРОДАВАЙ!!! (с параметрами по умолчанию)
                            self.log('SELL CREATE, %.2f' % self.data.close[0])
                            # Следим за созданным ордером, чтобы избежать второго дублирующегося ордера
                            self.order = self.sell(data=data)
                    except:
                        print("error...")

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm))

    def notify_order(self, order):
        ticker = order.data._name
        size = order.size

        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Проверка, мы в рынке?
        # Внимание: брокер может отклонить заявку, если недостаточно денег
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log('BUY EXECUTED, %.2f' % order.executed.price)
            elif order.issell():
                self.log('SELL EXECUTED, %.2f' % order.executed.price)

            self.bar_executed = len(self)
            self.orders_bar_executed[order.data._name] = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        # Запись: отложенного ордера - нет
        self.order = None

    def notify_data(self, data, status, *args, **kwargs):
        """Изменение статуса приходящих баров"""
        data_status = data._getstatusname(status)  # Получаем статус (только при LiveBars=True)
        print(f'{data._name} - {self.p.name} - {data_status}')  # Статус приходит для каждого тикера отдельно
        self.isLive = data_status == 'LIVE'  # В Live режим переходим после перехода первого тикера

Теперь давайте запустим эту стратегию и посмотрим результат!

результат запуска стратегии
результат запуска стратегии

Стоимость портфеля: 1 294 456.74

Свободные средства: 45 568 288.40

ЧТО???

Круто? Да? )))
Круто? Да? )))

Сразу скажу, что это не грааль — и в коде есть небольшая ошибка, которая приводит к таким ошеломляющим результатам)))

Кому интересно, заснял видео с разоблачением такой сверх доходности, доступно по ссылке.

Иногда лучше один раз увидеть, чем сто раз прочитать

Поэтому создание этой стратегии есть по шагам в видео, доступно по ссылке

Итог

Напоминаю, что цель данной статьи была показать — насколько легко теперь вы можете создавать своих собственных торговых роботов. Ни каких финансовых консультаций или рекомендаций не даю — просто пишем код для торгового бота. И без многих проверок — не запускайте торгового робота в live режиме...

Итак, просто пишите код торгового робота, тестируете его на истории, включаете Live режим, и запускаете в работу)

Как мне видится, получилось довольно интересно:‑) И жду ваших коммитов / фиксов / идей!

P. S. Это код выложил на GitHub по этой ссылке. Не забудьте свой конфиг файл положить в my_config\Config.py

Всем хорошего дня! Спасибо за уделенное время! Если считаете полезным такие статьи то жду вашей позитивной оценки))