This code backtests a long only strategy using the lower Bollinger Band for entry and mean for exit. It follows the value of one unit of capital over multiple positions. No consideration is given to position size, just all in. The entry/exit is made on the close price so the position column is shifted so the position is applied to following days prices. Lookback period (for the mean and std rolling windows) and threshold (for the number of stds deviation for entry) are set at the top of the script.
'''
Backtest of long only strategy using Bollinger Band(s).
Follows subsequent value of 1 unit of capital in opening position.
'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
lookback = 20
threshold = 1.0
# Read data and calculate statistics/indicators
df= pd.read_csv('data/ADA.csv', header=0, index_col=0, parse_dates=True)
df['mean'] = df['close'].rolling(lookback).mean()
df['stdev'] = df['close'].rolling(lookback).std()
df['bband'] = df['mean'] - threshold * df['stdev']
# Position - calculated on day's close so applies to following days
df['entry'] = df['close'] < df['bband'] # condition - T/F
df['exit'] = df['close'] >= df['mean'] # condition - T/F
df['position'] = np.nan # create and initialise the column
df.loc[df['entry'], 'position'] = 1 # set value according to condition
df.loc[df['exit'], 'position'] = 0 # set value according to condition
df = df.fillna(method='ffill')
# Returns
df['diff'] = df['close'] - df['close'].shift(1)
df['daily_returns'] = df['diff'] / df['close'].shift(1)
# position was entered at END of previous day
df['strategy_returns'] = df['position'].shift(1) * df['daily_returns']
df['cumret'] = (df['strategy_returns'] + 1).cumprod()
# Plot
df.cumret.plot(label='ADAUSDT', figsize=(12, 6))
plt.xlabel('Date')
plt.ylabel('Cumulative Returns')
plt.legend()
plt.show()
Here’s a slightly different version using BBANDS from ta-lib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import talib.abstract as ta
lookback = 20
threshold = 1.0
# Read data and calculate statistics/indicators
df= pd.read_csv('data/ADA.csv', header=0, index_col=0, parse_dates=True)
bb = ta.BBANDS(df, timeperiod=lookback, nbdevdn=threshold)
df['mean'] = bb.middleband
df['bband'] = bb.lowerband
# Position - calculated on day's close so applies to following days
df['entry'] = df['close'] < df['bband'] # condition - T/F
df['exit'] = df['close'] >= df['mean'] # condition - T/F
df['position'] = np.nan # create and initialise the column
df.loc[df['entry'], 'position'] = 1 # set value according to condition
df.loc[df['exit'], 'position'] = 0 # set value according to condition
df = df.fillna(method='ffill')
# Returns
df['diff'] = df['close'] - df['close'].shift(1)
df['daily_returns'] = df['diff'] / df['close'].shift(1)
# position was entered at END of previous day
df['strategy_returns'] = df['position'].shift(1) * df['daily_returns']
df['cumret'] = (df['strategy_returns'] + 1).cumprod()
# Plot
df.cumret.plot(label='ADAUSDT', figsize=(12, 6))
plt.xlabel('Date')
plt.ylabel('Cumulative Returns')
plt.legend()
plt.show()