Skip to main content

Tool for testing trading strategies on historical data

Project description

backintime 1.6.3

✨ Tool for testing trading strategies on historical data ✨
Such testing does not guarantee the same results in real trading, but it gives a rough estimate of a strategy's success. The smaller timeframe is used, the more accurate the results will be.

Note: Margin trading is not supported as for now. Expected in 2.x releases.

Features

  • Use CSV or Binance API as a data source
  • The same data can be represented in various timeframes
    (few short candles is used to represent longer one)
  • Market, Limit, Take Profit, Take Profit Limit, Stop Loss, Stop Loss Limit orders management
  • Builtin indicators. See list.
  • Trading statistics (win rate, profit/loss, avg. profit, etc.) with FIFO, LIFO or AVCO Profit/Loss estimation algorithms
  • Export orders, trades and statistics to csv

Indicators

  • MA - Moving Average
  • EMA - Exponential Moving Average
  • MACD - Moving Average Convergence Divergence
  • BBANDS - Bollinger Bands
  • RSI - Relative Strength Index
  • ATR - Average True Range
  • ADX - Average Directional Movement Index
  • DMI - Directional Movement Indicator
  • PIVOT - Pivot Points (Traditional, Fibonacci or Classic)

Installation

python3 -m pip install backintime

How to use

Sample with MACD strategy is provided below. The strategy buys when the MACD line crosses from below to above the signal line, (i.e. histogram becomes non-zero), and sells when MACD line crosses from above to below the signal line. Note that this strategy results in a high losses and only provided as a reference. Generally no one uses only one indicator in a trading strategy.

from datetime import datetime
from backintime import TradingStrategy, run_backtest
from backintime.trading_strategy import TradingStrategy
from backintime.timeframes import Timeframes as tf
from backintime.indicator_params import MACD
from backintime.data.binance import BinanceCandlesFactory


class MacdStrategy(TradingStrategy): # (1)
    title = "Sample MACD Strategy"
    indicators = { MACD(tf.H1) }	# (2)

    def tick(self):	# (3)
        macd = self.analyser.macd(tf.H1)
        if not self.position and macd.crossover_up():
            self.buy()	# (4)
        elif self.position and macd.crossover_down():
            self.sell()	# (5)


feed = BinanceCandlesFactory('BTCUSDT', tf.M15)	# (6)
since = datetime.fromisoformat("2020-01-01 00:00+00:00")
until = datetime.fromisoformat("2021-01-01 00:00+00:00")

result = run_backtest(MacdStrategy, feed,
                      10_000, since, until, 
                      maker_fee='0.005', taker_fee='0.005')
print(result)
print(result.get_stats('FIFO'))
result.export()

To implement a strategy, you need to create a class derived from TradingStrategy (1), specify which indicators is used (2) and provide algorithm implementation in tick method (3). When the corresponding conditions are met, the strategy calls buy (4) or sell (5) methods, which are aliases for Market order submission. Here the backtesting is done with candlesticks from Binance API on 15 minutes timeframe (6). However, consider using CSV file instead for better performance.


The following sample with "SMA resistance breakout" strategy demonstrates more advanced features: Limit orders, Take Profit and Stop Loss. The strategy buys when the last close crosses from below to above SMA line and sells at Take Profit or Stop Loss levels. MACD and DMI signals are used as filters.

import typing as t
from datetime import datetime
from decimal import Decimal
from backintime import TradingStrategy, run_backtest
from backintime.timeframes import Timeframes as tf
from backintime.data.binance import BinanceCandlesFactory
from backintime.indicator_params import SMA, MACD, DMI, PIVOT
from backintime.analyser.indicators.dmi import DMIResultSequence
from backintime.analyser.indicators.macd import MacdResultSequence
from backintime.broker import TakeProfitOptions, StopLossOptions


def macd_hist_up(macd: MacdResultSequence) -> bool:
    """True, if MACD hist > 0."""
    return macd[-1].hist > 0


def dmi_buy_signal(dmi: DMIResultSequence) -> bool:
    """True, if +DI > -DI."""
    return dmi.positive_di[-1] > dmi.negative_di[-1]


class SmaResistanceBreakout(TradingStrategy):
    title = "SMA resistance breakout"
    candle_timeframes = { tf.M15 }
    indicators = {
        SMA(tf.M15, 55),
        MACD(tf.M15),
        DMI(tf.M15, 14),
        PIVOT(tf.D1, 15)
    }

    def __init__(self, broker, analyser, candles):
        self.prev_close: t.Optional[Decimal] = None
        self.curr_close: t.Optional[Decimal] = None
        super().__init__(broker, analyser, candles)

    def tick(self):
        self.prev_close = self.curr_close
        self.curr_close = self.candles.get(tf.M15).close
        sma = self.analyser.sma(tf.M15, period=55)[-1]
        sma_crossover_up = self.curr_close > sma and \
                           self.prev_close and self.prev_close <= sma

        if self.broker.max_fiat_for_maker and sma_crossover_up and \
                macd_hist_up(self.analyser.macd(tf.M15)) and \
                dmi_buy_signal(self.analyser.dmi(tf.M15, 14)):
            # Calculate buy price: current SMA + 0.2%
            limit_price = Decimal(sma) * Decimal('1.002')
            # Set up TP at PIVOT' R2 level
            pivot = self.analyser.pivot(tf.D1, 15)
            take_profit_trigger = pivot[-1].r2
            take_profit = TakeProfitOptions(percentage_amount=Decimal('100.00'),
                                            trigger_price=take_profit_trigger)
            # Set up SL at current close price - 5%
            stop_loss_trigger = self.candles.get(tf.M15).close * Decimal('0.95')
            stop_loss = StopLossOptions(percentage_amount=Decimal('100.00'), 
                                        trigger_price=stop_loss_trigger)
            # Submit limit buy with TP & SL
            self.limit_buy(order_price=limit_price,
                           amount=self.broker.max_fiat_for_maker,
                           take_profit=take_profit,
                           stop_loss=stop_loss)


feed = BinanceCandlesFactory('BTCUSDT', tf.M15)
since = datetime.fromisoformat("2020-03-01 00:00+00:00")
until = datetime.fromisoformat("2021-05-01 00:00+00:00")

result = run_backtest(SmaResistanceBreakout, feed, 
                      10_000, since, until, 
                      maker_fee='0.005', taker_fee='0.005')

print(result)
print(result.get_stats('FIFO'))
print(result.get_stats('LIFO'))
print(result.get_stats('AVCO'))
result.export()

Core concepts

Backtesting

Backtesting is done in run_backtest function, which basically initializes required objects, prefetches market data if needed and runs the similar loop:

for candle in candles:
    broker.update(candle)   # Review whether orders can be executed
    analyser.update(candle) # Store data for indicators calculation
    candles.update(candle)  # Update candles on required timeframes
    strategy.tick()         # Trading strategy logic here

TradingStrategy

Base class for trading strategies. Strategy must provide algorithm implementation in tick method, which runs each time a new candle closes.

Broker

Broker provides orders management in a simulated market environment. The broker executes/activates orders whose conditions fits the market. Supports Market, Limit, Take Profit, Take Profit Limit, Stop Loss, Stop Loss Limit orders. Order execution policy of builtin broker:

  • Market orders: All market orders will be executed when a new candle closes. The price of execution is the candle's OPEN price.

  • Limit orders: Limit order will be executed at the limit price or better: lower or equal price for BUY orders and higher or equal price for SELL orders. First, the order_price of each order will be compared to the OPEN price of a new candle:

    BUY orders will be executed if `order_price` >= OPEN. 
    SELL orders will be executed if `order_price` <= OPEN.
    

    Then, remaining BUYs will be compared to LOW, and remaining SELLs - to HIGH. Fill price is the first price that matched limit price.

  • Take Profit/Stop Loss orders:
    TP/SL orders will be activated if the trigger_price is within the price bounds of a candle. This check is performed in two steps:

    1) For each order: activate if trigger_price == OPEN
    2) For each order: activate if LOW <= trigger_price <= HIGH
    

    When a TP/SL order is triggered, it will be treated as a market or limit order, depending on whether order_price is set for the order.

Limit, Take Profit and Stop Loss orders are reviewed in the order of their submission (oldest first).

Analyser

Indicators calculation. See list of supported indicators.

Candles

Provides the last candle representation for various timeframes. It is useful for checking properties of a candle on one timeframe (H1, for example), while having data on another (for instance, M1).

DataProvider

Provides candles in historical order. DataProvider is an iterable object that can be created for specific date range (since, until); Yields OHLCV candle during iteration.

BacktestingResult

Provides export to CSV and stats such as Win Rate, Profit/Loss, Average Profit, Best/Worst deal, etc. Supports estimation in FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or AVCO (Average Cost) algorithms. The algorithm name specifies the order in which BUYs must be considered to estimate profit or loss.

All these algorithms produce the same result if SELL order always follows only one BUY.

Prefetching

Indicators require a certain amount of data to get a correct result. For example, to calculate the SMA (simple moving average) with a period of 9, 9 values are required. So, the strategy will get the wrong result of the SMA indicator, until all 9 candles are accumulated.

In order for the strategy to get the correct values right from the start, prefetching of market data is used. You can configure this behavior by choosing from the following options and passing it as prefetch_option argument to the run_backtest function:

  • PREFETCH_UNTIL (default) - prefetch the required amount of data until since date; the amount of data required is calculated automatically. In this case, the strategy backtesting will be started from the since date. This option is convenient when market data is requested from the exchange API, because situations when the exchange does not have enough historical data are quite rare.

  • PREFETCH_SINCE - prefetch the required amount of data from since date; the amount of data required for this is calculated automatically. In this case, the strategy backtesting will be launched starting from the dynamically calculated date. This option may be useful when working with a CSV file when you are not sure if it contains enough data before the since date.

  • PREFETCH_NONE - Do not prefetch data.

Where since date is the value of the argument since passed to the run_backtest function.

Some thoughts

I plan to add support for margin trading (will allow testing of short and leveraged strategies) and implement backtesting as an event based system using a queue for communication (will allow running backtesting on distributed systems).

Docs

There is no documentation yet because the code is unstable (but works). As for now, you can browse sources or type help in REPL.

License

MIT

Author

Akim Mukhtarov @akim_int80h

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

backintime-1.6.3.tar.gz (43.1 kB view hashes)

Uploaded Source

Built Distribution

backintime-1.6.3-py3-none-any.whl (54.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page