• Stars
    star
    122
  • Rank 291,951 (Top 6 %)
  • Language
    Python
  • Created almost 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Backtesting several trading strategy and rank them according their profit return.
  1. Introduction and Roadmap
  2. Code review
    1. Get candlesticks
    2. Get results
    3. Backtest
  3. Analysis
  4. Links and Addresses

Introduction and Roadmap

First, our goal is to backtest several trading strategy on different cryptocurrencies and timeframes and rank them according their profit return. We won't bother developping a complex strategy, we will stick to basics ones using SMA and RSI relying on the Ta-Lib library and backtrader and we will vary the period used between 10 and 30. backtesting code | requirements needed

The cryptocurrencies datas needed are collected through the Binance API. code to get the wanted kandlesticks | file containing this datas

These datas will be processed by the get_result.py code that will run through all the datas in the data file and save the result in result/.

Finally the top 10 strategies will be saved to a JSON file so that we may reuse them.

 

Later, the algorithm will be adapted for trading futures and the strategies will be improved. The bot will then be connected to Binance and take automatic positions according the best strategy returned by the backtest.

You can have a look at that repository for an improved and different trading bot : tradingview-webhook-trading-bot

Ideally, the backtest will run automatically each X period and reset the best strategy.

 

Code review

Get candlesticks

To get the historical candlesticks we want for backtesting our strategies we use an unofficial Python wrapper for the Binance exchange REST API v3.

pip install python-binance

Then import the client and connect your Binance API key and API secret.

from binance.client import Client
client = Client(config.API_KEY, config.API_SECRET)

You can then get the historical candlesticks you want by defining the cryptocurrency pair you want to get (BTCUSDT for example), the interval represented by one kandlesticks (1 month, 4h...) and its date.

candlesticks = client.get_historical_klines("BTCUSDT", Client.KLINE_INTERVAL_4HOUR, "1 Jan, 2017", "25 Dec, 2020")

The datas we obtain are as follow :

[
  [
    1499040000000,      // Open time
    "0.01634790",       // Open
    "0.80000000",       // High
    "0.01575800",       // Low
    "0.01577100",       // Close
    "148976.11427815",  // Volume
    1499644799999,      // Close time
    "2434.19055334",    // Quote asset volume
    308,                // Number of trades
    "1756.87402397",    // Taker buy base asset volume
    "28.46694368",      // Taker buy quote asset volume
    "17928899.62484339" // Ignore.
  ]
]

Before saving these datas to a csv we need to divide the timestamp we obtain ('Open time') by 1000 to ignore the miliseconds.

We fetched the historical candlesticks for the BTCUSDT and ETHUSDT pairs for each 1w-3d-1d-12h-8h-6h-4h-2h-1h-30m-15m timeframes.

Get results

The get_result.py code will basically set all the parameters we want to use on our backtest.py program and will run through all of them to get as many strategy and how they performed. For that we need to import our backtest code along with some others.

Next thing to do is to define the parameters we want to take into account.

commission_val = 0.04 # 0.04% taker fees binance usdt futures
portofolio = 10000.0 # amount of money we start with
stake_val = 1
quantity = 0.10 # percentage to buy based on the current portofolio amount
# here it would correspond to a unit equivalent to 1000$ if the value of our portofolio didn't change

start = '2017-01-01'
end = '2020-12-31'
strategies = ['SMA', 'RSI']
periodRange = range(10, 31)
plot = False

In the strategies list we index all the strategies names written in the backtest code that we want to evaluate (the strategies names here are only indicators since we didn't write any complex strategy but only made simple use of these indicators), by default SMA and RSI use a period of 14 but we can also specify a range of values that we will use instead in order to test as many differents settings as we can to find the best ones.

If plot was set to True we would have a graphical view for each result :

BTCUSDT-RSI-14-2017-2020-12h

This is the result of a simple strategy using RSI with a period 14 on the BTCUSDT pair with a timeframe for each candlesticks of 12h. We buy 1 unit if the rsi < 30 and we are not already in a position, and sell this unit when rsi > 70. We started with a portofolio of 50000 and ended up losing since our final value is 49110.90

We then loop through each strategy in each file and in each period, and we get in return additional details for each strategy such as the portofolio final amount, the total number of wins and losses, the net profit and loss and finally the SQN value which is an indicator designed to assist traders in determining the global quality of a trading system.

For that we call the runbacktest function imported from backtest.py.

end_val, totalwin, totalloss, pnl_net, sqn = backtest.runbacktest(datapath, start, end, period, strategy, commission_val, portofolio, stake_val, quantity, plot)

At last, we save in differents files the result of each strategy as follow :

Pair,Timeframe,Start,End,Strategy,Period,Final value,%,Total win,Total loss,SQN
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,10,8460.692,-15.478,663,2167,-1.64
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,11,8531.827,-14.768,616,2082,-1.56
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,12,8561.091,-14.551,574,2010,-1.54
...
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,28,11423.668,14.121,283,1223,1.2
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,29,11033.763,10.226,269,1226,0.88
BTCUSDT,1h,2017-01-01,2020-12-31,SMA,30,11023.452,10.123,265,1199,0.88

For example for this strategy (SMA-BTCUSDT-20170101-20201231-1h.csv) using SMA on the 1 hour timeframe BTCUSDT pair from 2017 to 2020, we can notice that the lower the SMA period is the lower our sqn and profit will be (in that case even negative), and conversely when the SMA period is higher our profit is better.

The code was run again between 2018-03-01 and 2020-11-15 to exclude the bullrun period and test our strategies during upward trend and range period.

Backtest

The backtest.py code was mostly based on the Backtrader Quickstart Guide. However, some modifications were applied and functionalities added to respond our needs.

Two strategies were implemented :

  • Using SMA :
class SMAStrategy(bt.Strategy):
    params = (
        ('maperiod', None),
        ('quantity', None)
    )

This strategy is based on the SMA indicator. If we are not already in a position and the closure price of the last candlestick is higher than the indicator (i.e. we cross the sma from bellow to top), then we buy a size equivalent to 10% of the current portofolio amount. We sell when the opposite happen.

SMA crossed

# Check if we are in the market
if not self.position:
    # Not yet ... we MIGHT BUY if ...
    if self.dataclose[0] > self.sma[0]:
        # Keep track of the created order to avoid a 2nd order
        self.amount = (self.broker.getvalue() * self.params.quantity) / self.dataclose[0]
        self.order = self.buy(size=self.amount)
else:
    # Already in the market ... we might sell
    if self.dataclose[0] < self.sma[0]:
        # Keep track of the created order to avoid a 2nd order
        self.order = self.sell(size=self.amount)
  • Using RSI :
class RSIStrategy(bt.Strategy):
    params = (
        ('maperiod', None),
        ('quantity', None)
    )

Based on the RSI indicator, if we are not already in a position and the rsi go below 30 then we buy a size equivalent to 10% of the current portofolio amount that we will sell when rsi > 70.

# Check if we are in the market
if not self.position:
    # Not yet ... we MIGHT BUY if ...
    if self.rsi < 30:
        # Keep track of the created order to avoid a 2nd order
        self.amount = (self.broker.getvalue() * self.params.quantity) / self.dataclose[0]
        self.order = self.buy(size=self.amount)
else:
    # Already in the market ... we might sell
    if self.rsi > 70:
        # Keep track of the created order to avoid a 2nd order
        self.order = self.sell(size=self.amount)
  • Strategy selection

Depending on the parameter given in the runbacktest function we will add one of the two strategy we have written and give it the period we want to use and quantity in % of our portofolio that we want to use.

if strategy == 'SMA':
    cerebro.addstrategy(SMAStrategy, maperiod=period, quantity=quantity)
elif strategy == 'RSI':
    cerebro.addstrategy(RSIStrategy, maperiod=period, quantity=quantity)
else :
    print('no strategy')
    exit()

 

  • Get compression and timeframe

When we create the data feed we need to define the compression and timeframe.

data = bt.feeds.GenericCSVData(
        dataname = datapath,
        dtformat = 2, 
        compression = compression, 
        timeframe = timeframe,
        fromdate = datetime.datetime.strptime(start, '%Y-%m-%d'),
        todate = datetime.datetime.strptime(end, '%Y-%m-%d'),
        reverse = False)

cerebro.adddata(data)

For a 3 days timeframe (one candlestick represent 3 days) we would have compression = 3 and timeframe = bt.TimeFrame.Days. However, for a 2h timeframe compression = 120 but timeframe = bt.TimeFrame.Minutes.

To automatically do this conversion the runbacktest() function will analyse the given datapath name and pass it to another function that will read the file name and retrieve its timeframe (e.g. data/ETHUSDT-2017-2020-4h.csv).

def timeFrame(datapath):
    """
    Select the write compression and timeframe.
    """
    sepdatapath = datapath[5:-4].split(sep='-') # ignore name file 'data/' and '.csv'
    tf = sepdatapath[3]

    if tf == '1mth':
        compression = 1
        timeframe = bt.TimeFrame.Months
    elif tf == '12h':
        compression = 720
        timeframe = bt.TimeFrame.Minutes
    
    ...

    elif tf == '8h':
        compression = 480
        timeframe = bt.TimeFrame.Minutes
    else:
        print('dataframe not recognized')
        exit()

    return compression, timeframe

Later this function can be improved to save more lines by autocomputing the compression if we have a hour timeframe (e.g. if 'h' take what's in the front, say 12 and multiply it by 60 => 720 and timeframe = bt.TimeFrame.Minutes).

 

To complete our program we've added two analyzers from backtrader (analyzers reference) :

cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")
cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn")

It enable us to get the total number of win/loss, the net pnl and the sqn after a strategy was run.

totalwin, totalloss, pnl_net = getWinLoss(stratexe.analyzers.ta.get_analysis())
sqn = getSQN(stratexe.analyzers.sqn.get_analysis())
def getWinLoss(analyzer):
    return analyzer.won.total, analyzer.lost.total, analyzer.pnl.net.total
    
def getSQN(analyzer):
    return round(analyzer.sqn,2)

 

Analysis

Find the analysis in README.ipynb notebook.

 


Links and Addresses

  • Binance and Binance future:

https://accounts.binance.com/en/register?ref=MJB86NYU to register to Binance and save 10% comission fee.

https://www.binance.com/en/futures/ref/154947021 for binance future and to also save 10%.

 

If it was interesting and you've learned something feel free to support me by sending any amount of cryptos to one of the following addresses :)

  • Ethereum, Matic Mainet and BSC : 0xa1eF4B0eA4f2a45Dc60baba368BDB8656B6fD580

  • Solana : 2c1F4Rs63nL1VCuLP7Qz776X1NemfNUfYjMWTALQzrwh

  • Bitcoin : 3DXrC7ZXxYa4bFbjL3ofgWxfeeiAzzHWf4

 

Thank you for reading all the way through !