Concepts
Backtesting
Backtesting is the general method for seeing how well a strategy or model would have done ex-post. Backtesting assesses the viability of a trading strategy by discovering how it would play out using historical data. If backtesting works, traders and analysts may have the confidence to employ it going forward.
Strategy
Strategies is a stack of algos which defines what asset to hold or not hold, how to weight each asset in the portfolio, and when to rebalance the portfolio.
graph TD Strategy --> Selection Strategy --> Weights Strategy --> Rebalance style Strategy padding:5px
Evaluating Results
Here’s an example backtest result:
Stat above50sma
------------------- ------------
Start 2010-01-03
End 2017-02-22
Risk-free rate 0.00%
Total Return 82.61%
Daily Sharpe 0.56
Daily Sortino 0.68
CAGR 8.80%
Max Drawdown -31.96%
Calmar Ratio 0.28
MTD 7.10%
3m 13.58%
6m 28.80%
YTD 6.85%
1Y 35.60%
3Y (ann.) 16.24%
5Y (ann.) 13.20%
10Y (ann.) 8.80%
Since Incep. (ann.) 8.80%
Daily Sharpe 0.56
Daily Sortino 0.68
Daily Mean (ann.) 10.09%
Daily Vol (ann.) 18.12%
Daily Skew -0.54
Daily Kurt 4.49
Best Day 5.78%
Worst Day -7.99%
Monthly Sharpe 0.51
Monthly Sortino 0.72
Monthly Mean (ann.) 10.74%
Monthly Vol (ann.) 20.97%
Monthly Skew -0.50
Monthly Kurt 0.46
Best Month 13.64%
Worst Month -16.03%
Yearly Sharpe 0.65
Yearly Sortino 2.03
Yearly Mean 10.91%
Yearly Vol 16.75%
Yearly Skew -0.13
Yearly Kurt -0.68
Best Year 34.85%
Worst Year -13.42%
Avg. Drawdown -3.51%
Avg. Drawdown Days 54.34
Avg. Up Month 4.92%
Avg. Down Month -4.59%
Win Year % 71.43%
Win 12m % 68.00%
The most important metrics are the top 5.
- Total returns over the backtest period.
- Daily Sharpe ratio - the higher the better. The ratio incorporates returns and volatility in one number.
- Daily Sortino similar Sharpe except it only accounts for negative volatility.
- CAGR Compound annual growth rate is the rate of return that would be required for an investment to grow from its beginning balance to its ending balance.
- Max Drawdown is the maximum loss from the the prior peak.
While the highest total returns is desired, volatility or drawndowns is equally as important. In live-trading, the drawndown incurred may not recover like the backtesting results which represent a risk that must be minimized.
Per Investopedia:
- Usually, any Sharpe ratio greater than 1.0 is considered acceptable to good by investors.
- A ratio higher than 2.0 is rated as very good.
- A ratio of 3.0 or higher is considered excellent.
- A ratio under 1.0 is considered sub-optimal.
Types of Algos
Algos (bt.algos) are the defines selection process, and weighting process of a strategy.
Selection Algos
This is the implementation of the selection (you don’t need to implement it). It takes in a signal in a form of a dataframe with each row as a date, and a column of either selected True or False.
class SelectWhere(bt.Algo):
"""
Selects securities based on an indicator DataFrame.
Selects securities where the value is True on the current date (target.now).
Args:
* signal (DataFrame): DataFrame containing the signal (boolean DataFrame)
Sets:
* selected
"""
def __init__(self, signal):
self.signal = signal
def __call__(self, target):
# get signal on target.now
if target.now in self.signal.index:
sig = self.signal.ix[target.now]
# get indices where true as list
selected = list(sig.index[sig])
# save in temp - this will be used by the weighing algo
target.temp['selected'] = selected
# return True because we want to keep on moving down the stack
return True
While you can implement most algos with SelectWhere, there are other select algos available such as select top N or bottom N, select momentum. See API.
Weight Algos
Weight algos determine how to weight the selected assets.
- WeighEqually weights equally based on N assets selected.
- WeighRandomly weights randomly
- WeighSpecified weights based on a fixed weights for entire strategy.
- WeighTarget weights based on each period for target weights.
- WeighERC weights based on equal risk contribution. See https://en.wikipedia.org/wiki/Risk_parity
- WeighInvVol weights heavier on the least volatile asset. eg. inverse proportional to their volatility.
- WeighMeanVar weights based on Markowitz mean-variance optimization. See http://en.wikipedia.org/wiki/Modern_portfolio_theory#The_efficient_frontier_with_no_risk-free_asset
Pandas and DataFrames
Pandas is an open source library providing high performance, easy to use data structures and data analysis tools for Python. It is used extenstively to manipulate data and converts them into signals for the trading strategy.
Viewing
> data.head()
BTC/USDT ETH/USDT
Open High Low Close Volume Open High Low Close Volume
Timestamp
2018-01-01 13715.65 13818.55 12750.00 13380.00 8609.915844 733.01 763.55 716.80 754.99 53909.25885
2018-01-02 13382.16 15473.49 12890.02 14675.11 20078.092111 754.99 899.50 749.06 855.28 113257.78355
2018-01-03 14690.00 15307.56 14150.00 14919.51 15905.667639 855.13 950.01 810.00 934.03 89041.45688
2018-01-04 14919.51 15280.00 13918.04 15059.54 21329.649574 934.03 1009.72 890.01 940.00 102894.47384
2018-01-05 15059.56 17176.24 14600.00 16960.39 23251.491125 940.00 1045.00 930.00 959.30 97374.01630
The platform returns data as a multi-level dataframe. Selecting all the columns for BTC.
> data['BTC/USDT'].head()
Open High Low Close Volume
Timestamp
2018-01-01 13715.65 13818.55 12750.00 13380.00 8609.915844
2018-01-02 13382.16 15473.49 12890.02 14675.11 20078.092111
2018-01-03 14690.00 15307.56 14150.00 14919.51 15905.667639
2018-01-04 14919.51 15280.00 13918.04 15059.54 21329.649574
2018-01-05 15059.56 17176.24 14600.00 16960.39 23251.491125
Getting all of the Close prices.
> data.xs('Close', level=1, axis=1)
BTC/USDT ETH/USDT
Timestamp
2018-01-01 13380.00 754.99
2018-01-02 14675.11 855.28
2018-01-03 14919.51 934.03
2018-01-04 15059.54 940.00
2018-01-05 16960.39 959.30