Потому что это важнее, чем живая торговля

Это вторая история из серии Создание торгового бота. Вам нужно знать Backtrader, чтобы понять эту историю. Если вы не знаете, что такое Backtrader, или хотите найти другие истории из этой серии, вам следует проверить следующую историю:



Для этой серии я сделал репозиторий на GitHub. Если вы хотите использовать его, чтобы следовать коду, вы можете найти его здесь: Серия торговых ботов.

Что мы получим

В конце этой истории у нас будет гибкий торговый бот с функцией тестирования на истории.

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

Вот что вы получите в конце этой истории:

Вход:

results = bot.backtest(strategy, some_parameters)
for result in results:
    print(f"Net profit: {profit}")

Выход:

Net profit: 14075.772535935259
Net profit: 9407.347764489063
Net profit: 27047.968861593035
Net profit: 23669.66175297717
Net profit: 15670.56778873867

Начиная

Как я уже говорил в предыдущем рассказе, мы будем создавать торгового бота на Python. Первое, что нужно сделать, это настроить проект и среду.

На данный момент вам понадобятся Backtrader, Pandas и Yfinance.

pip install backtrader2
pip install pandas
pip install yfinance

Когда начать?

Начнем с создания нового класса для нашего бота. Мы также реализуем основной метод, который нам понадобится:

class TradingBot:
    def backtest():
        pass

Хорошо, теперь, как протестировать? Если вы следили за моей серией Backtrader, вы знаете, как мы можем это сделать.

Нам нужно сначала объявить Cerebro, потом добавить данные, добавить нашу стратегию, наш сайзер, наши анализаторы и т.д…

Это дает нам представление о параметрах, которые нам нужно передать backtest :

def backtest(self, strategy, backtest_parameters, data_source, sizer=bt.sizers.FixedSize, strategy_parameters=None,
             sizer_parameters=None, analyzers=None):

Некоторые параметры могут сбивать с толку:

  • backtest_parameters: дата начала тестирования, дата окончания, начальные денежные средства, символ и т. д.
  • data_source: класс, который мы будем использовать для извлечения наших данных обратного тестирования (Open High Low Close DataFrame).
  • sizer: объект, используемый для работы с размером нашей позиции.

Теперь у нас есть все необходимое для реализации нашего метода backtest:

def backtest(self, strategy, backtest_parameters, data_source, sizer=bt.sizers.FixedSize, strategy_parameters=None,
             sizer_parameters=None, analyzers=None):
    cerebro = bt.Cerebro()

    data = data_source.get_data(backtest_parameters)
    datafeed = bt.feeds.PandasData(dataname=data)
    cerebro.adddata(datafeed)

    initial_cash = backtest_parameters.get('initial_cash', 10000)
    commission = backtest_parameters.get('commission', 0.001)
    slippage = backtest_parameters.get('slippage', 0.001)

    cerebro.broker.setcash(initial_cash)
    cerebro.broker.setcommission(commission=commission)
    cerebro.broker.set_slippage_perc(slippage)

    cerebro.adddata(datafeed)

    if not strategy_parameters:
        strategy_parameters = {}
    cerebro.optstrategy(strategy, **strategy_parameters)

    if not sizer_parameters:
        sizer_parameters = {}
    cerebro.addsizer(sizer, **sizer_parameters)

    if analyzers:
        for analyzer in analyzers:
            cerebro.addanalyzer(analyzer)

    results = cerebro.run(maxcpus=1)
    return results

Несколько слов об этой линейке:

datafeed = bt.feeds.PandasData(dataname=data)

Здесь мы просто создаем DataFeed с нашими данными (фрейм данных OHLCV).

Источники данных

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

В интерфейсе у нас будет один публичный метод: get_data. Этот метод должен возвращать кадр данных OHLCV.

Мы будем использовать этот метод, чтобы обернуть другой метод: _get_data. Этот метод является закрытым и абстрактным. Нам нужно переопределить его, чтобы определить поведение конкретного источника данных.

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

from abc import ABC, abstractmethod
class DataSource(ABC):

    def get_data(self, backtest_parameters):
        start_date = backtest_parameters.get('start_date', dt.datetime(2019, 1, 1))
        end_date = backtest_parameters.get('end_date', dt.datetime(2020, 1, 1))
        timeframe = backtest_parameters.get('timeframe', Timeframes.d1)
        symbol = backtest_parameters.get('symbol', 'BTC-USD')

        print(f'Getting data for {symbol} from {start_date} to {end_date} with {timeframe.name} timeframe with {self.__class__.__name__} data source')
        return self._get_data(self._get_start_date(start_date), self._get_end_date(end_date), self._get_timeframe(timeframe), self._get_symbol(symbol))

    @abstractmethod
    def _get_data(self, start_date, end_date, timeframe, symbol) -> pd.DataFrame:
        pass

    def _get_start_date(self, start_date):
        return start_date

    def _get_end_date(self, end_date):
        return end_date

    def _get_timeframe(self, timeframe):
        return timeframe

    def _get_symbol(self, symbol):
        return symbol

Теперь мы реализуем конкретный источник данных. Но перед этим мы реализуем Enum для определения таймфреймов.

from enum import Enum
import backtrader as bt


class Timeframes(Enum):
    m1 = (bt.TimeFrame.Minutes, 1)
    m5 = (bt.TimeFrame.Minutes, 5)
    m15 = (bt.TimeFrame.Minutes, 15)
    m30 = (bt.TimeFrame.Minutes, 30)
    h1 = (bt.TimeFrame.Minutes, 60)
    h4 = (bt.TimeFrame.Minutes, 240)
    d1 = (bt.TimeFrame.Days, 1)
    w1 = (bt.TimeFrame.Weeks, 1)
    mo1 = (bt.TimeFrame.Months, 1)

Вы можете реализовать любой таймфрейм, который хотите, и реализовать их так, как хотите. Я выбрал формат Backtrader, то есть (timeframe, resolution) .

Теперь давайте создадим источник данных. Если вам нравится API, вы можете реализовать свой собственный источник данных и делать то, что хотите для этой части. Просто убедитесь, что вы возвращаете OHLCV DataFrame (я должен был добавить тест, чтобы проверить правильность DataFrame в get_data). Для себя я буду реализовывать Yfinance API в качестве источника данных:

import yfinance as yf

class Yfinance(DataSource):

Во-первых, я должен сделать свои таймфреймы совместимыми с Yfinance. Итак, я буду использовать метод _get_timeframe:

def _get_timeframe(self, timeframe):
    try:
        timeframe = timeframe.name[-1] + timeframe.name[:-1]
        if timeframe not in ['1m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo']:
            raise ValueError
        return timeframe
    except ValueError:
        raise ValueError(f'Yfinance does not support {timeframe} timeframe')

Затем я должен убедиться, что мой символ поддерживается Yfinance. Например, если я хочу извлечь данные «BTC-USDT» из Yfinance, это не сработает, поэтому я должен поймать ошибку.

def _get_symbol(self, symbol):
    try:
        ticker = yf.Ticker(symbol)
        info = ticker.info
        if not info.get('regularMarketPrice', None):
            raise ValueError
        return symbol
    except ValueError as e:
        raise ValueError(f'Yfinance does not support {symbol} symbol')

Теперь я могу переопределить метод _get_data:

def _get_data(self, start_date, end_date, timeframe, symbol):
    data = yf.download(symbol, start=start_date, end=end_date, interval=timeframe)
    return yf.download(symbol, start_date, end_date, interval=timeframe)

Я почти уверен, что проблем не будет, потому что я проверял возможные ошибки другими методами, и я уверен, что мои параметры в формате, поддерживаемом Yfinance.

Беги беги беги!

Итак, теперь у меня есть класс TradingBot и источник данных Yfinance, который я могу использовать для загрузки данных тестирования на истории. Чего не хватает?

Ничего, я могу просто поместить все в скрипт и запустить его:

import backtrader as bt

from trading_bot import TradingBot
from timeframes import Timeframes
from data_sources.yfinance import Yfinance


bot = TradingBot()
data_source = Yfinance()

backtest_parameters = {
    'start_date': '2010-01-01',
    'end_date': '2022-01-01',
    'timeframe': Timeframes.d1,
    'symbol': 'AAPL',
    'initial_cash': 10000,
    'commission': 0.001,
    'slippage': 0.001
}

strategy = bt.strategies.MA_CrossOver
strategy_parameters = {
    'fast': range(10, 15),
}

sizer = bt.sizers.PercentSizer
sizer_parameters = {
    'percents': 99
}

analyzers = [
    bt.analyzers.TradeAnalyzer
    ]

results = bot.backtest(strategy, backtest_parameters, data_source, strategy_parameters=strategy_parameters, sizer=sizer,
                       sizer_parameters=sizer_parameters, analyzers=analyzers)
for result in results:
    print(f"Net profit: {result[0].analyzers.tradeanalyzer.get_analysis()['pnl']['net']['total']}")

Когда я запускаю этот код, он дает мне это:

Net profit: 14075.772535935259
Net profit: 9407.347764489063
Net profit: 27047.968861593035
Net profit: 23669.66175297717
Net profit: 15670.56778873867

Итак, функция тестирования на истории работает!

Очевидно, вы можете изменить параметры, которые я использовал для запуска кода, и он (надеюсь) по-прежнему будет работать.

Что дальше?

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

Чтобы найти другие истории из этой серии и больше о совмещении трейдинга и Python, прочтите эту статью: Улучшите свою торговлю с помощью Python

Чтобы узнать больше о моих рассказах о Python, нажмите здесь!

Если вам понравилась история, не забудьте похлопать и, возможно, подпишитесь на меня, если хотите узнать больше о моем содержании :)

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

Если вы еще не подписаны на Medium и хотите поддержать меня или получить доступ ко всем моим историям, вы можете использовать мою ссылку:



Сообщение от InsiderFinance

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь: