Portfolio backtesting is a critical aspect of quantitative finance and trading strategy development. VectorBT is a Python library that stands out for its efficiency and flexibility in backtesting portfolio strategies.
VectorBT is a powerful tool for portfolio backtesting and portfolio rebalancing in Python, offering a blend of performance, flexibility, and ease of use. It allows traders and investors to rigorously test and refine their trading strategies, fostering more informed and confident decision-making in the markets.
Here are the Steps involved in Portfolio Backtesting and Rebalancing using VectorBT
Installing the Python Libraries
First, you need to install VectorBT and other supporting libraries for backtesting portfolio trading strategies in Python.
pip install vectorbt
pip install yfinance
pip install pandas
pip install numpy
pip install pytz
Importing Python Libraries and VectorBT Settings
Import necessary libraries including VectorBT. Configure VectorBT settings for portfolio management, such as the frequency of data, return calculation, and initial settings for the portfolio:
#import libraries and set the vectorBT portfolio settings
import os
import numpy as np
import pandas as pd
import yfinance as yf
from datetime import datetime
import pytz
from numba import njit
import vectorbt as vbt
from vectorbt.generic.nb import nanmean_nb
from vectorbt.portfolio.nb import order_nb, sort_call_seq_nb
from vectorbt.portfolio.enums import SizeType, Direction
vbt.settings.array_wrapper['freq'] = 'days'
vbt.settings.returns['year_freq'] = '252 days'
vbt.settings.portfolio['seed'] = 42
vbt.settings.portfolio.stats['incl_unrealized'] = True
Define the Portfolio Symbols and their weights and Download the data from Yahoo Finance or from any other data sources
Select the stock symbols and weights for your portfolio. Use the yfinance
library to download historical adjusted close prices for the selected stocks:
# Define the stock symbols and time period
symbols = ['TCS.NS', 'HDFCBANK.NS', 'MARUTI.NS']
#Define the weights
weights = [np.array([0.25, 0.35, 0.4])]
start_date = '2010-01-01' # define start date
end_date = datetime.today().strftime('%Y-%m-%d') # format the current date as a string in 'YYYY-MM-DD' format
# Download historical data
price = yf.download(symbols, start=start_date, end=end_date)['Adj Close']
price
Output
[*********************100%%**********************] 3 of 3 completed
HDFCBANK.NS MARUTI.NS TCS.NS
Date
2010-01-04 153.989609 1397.335571 286.176147
2010-01-05 154.124985 1365.271973 286.252441
2010-01-06 154.228821 1316.455933 279.779999
2010-01-07 154.630539 1308.935547 271.994049
2010-01-08 154.833694 1290.786987 266.435333
... ... ... ...
2023-11-28 1528.650024 10537.549805 3470.149902
2023-11-29 1559.150024 10599.250000 3513.750000
2023-11-30 1558.800049 10608.700195 3487.600098
2023-12-01 1555.400024 10585.700195 3511.649902
2023-12-04 1609.400024 10599.450195 3512.449951
3436 rows × 3 columns
Calculate Returns and Statistical Measures
Compute daily returns, mean, standard deviation, and correlation of the returns
#calculate returns, mean, std deviation and correlation
returns = price.pct_change()
print('Mean')
print(returns.mean())
print('\nStandard Deviation')
print(returns.std())
print('\nCorrelation Table')
print(returns.corr())
Output
Mean
HDFCBANK.NS 0.000792
MARUTI.NS 0.000752
TCS.NS 0.000854
dtype: float64
Standard Deviation
HDFCBANK.NS 0.014737
MARUTI.NS 0.018022
TCS.NS 0.015777
dtype: float64
Correlation Table
HDFCBANK.NS MARUTI.NS TCS.NS
HDFCBANK.NS 1.000000 0.389584 0.254089
MARUTI.NS 0.389584 1.000000 0.165110
TCS.NS 0.254089 0.165110 1.000000
Build MultiIndex column hierarchy for Portfolio Backtesting
# Build column hierarchy such that one weight corresponds to one price series
num_tests = 1
_price = price.vbt.tile(num_tests, keys=pd.Index(np.arange(num_tests), name='symbol_group'))
_price = _price.vbt.stack_index(pd.Index(np.concatenate(weights), name='weights'))
print(_price.columns)
_price
Output
MultiIndex([(0.25, 0, 'HDFCBANK.NS'),
(0.35, 0, 'MARUTI.NS'),
( 0.4, 0, 'TCS.NS')],
names=['weights', 'symbol_group', None])
weights 0.25 0.35 0.40
symbol_group 0 0 0
HDFCBANK.NS MARUTI.NS TCS.NS
Date
2010-01-04 153.989609 1397.335571 286.176147
2010-01-05 154.124985 1365.271973 286.252441
2010-01-06 154.228821 1316.455933 279.779999
2010-01-07 154.630539 1308.935547 271.994049
2010-01-08 154.833694 1290.786987 266.435333
... ... ... ...
2023-11-28 1528.650024 10537.549805 3470.149902
2023-11-29 1559.150024 10599.250000 3513.750000
2023-11-30 1558.800049 10608.700195 3487.600098
2023-12-01 1555.400024 10585.700195 3511.649902
2023-12-04 1609.400024 10599.450195 3512.449951
3436 rows × 3 columns
Define Allocation Weights at the First Timestamp
# Define allocation weights at the first timestamp and rest of the arrays as null for Buy and Hold Portfolio Backtesting
size = np.full_like(price, np.nan)
size[0, :] = np.concatenate(weights) # allocate at first timestamp, do nothing afterwards
print(size.shape)
size
Output
(3437, 3)
array([[0.25, 0.35, 0.4 ],
[ nan, nan, nan],
[ nan, nan, nan],
...,
[ nan, nan, nan],
[ nan, nan, nan],
[ nan, nan, nan]])
Run Buy and Hold Portfolio Simulation with Fixed Weights
# Run Buy and Hold Portfolio Backtesting simulation
# Set initial capital
initial_capital = 1000000
portfolio = vbt.Portfolio.from_orders(
close=_price,
size=size,
size_type='targetpercent',
group_by='symbol_group',
cash_sharing=True,
fees=0.001,
init_cash=initial_capital,
freq='1D',
min_size =1,
size_granularity = 1
) # all weights sum to 1, no shorting, and 100% investment in risky assets
#print(len(portfolio.orders))
# Analyze results
stats = portfolio.stats()
print(stats)
Output
Start 2010-01-04 00:00:00
End 2023-12-05 00:00:00
Period 3437 days 00:00:00
Start Value 1000000.0
End Value 10204680.493299
Total Return [%] 920.468049
Benchmark Return [%] 915.258361
Max Gross Exposure [%] 99.997677
Total Fees Paid 998.761019
Max Drawdown [%] 34.433968
Max Drawdown Duration 368 days 00:00:00
Total Trades 3
Total Closed Trades 0
Total Open Trades 3
Open Trade PnL 9204680.493299
Win Rate [%] NaN
Best Trade [%] NaN
Worst Trade [%] NaN
Avg Winning Trade [%] NaN
Avg Losing Trade [%] NaN
Avg Winning Trade Duration NaT
Avg Losing Trade Duration NaT
Profit Factor NaN
Expectancy NaN
Sharpe Ratio 1.022579
Calmar Ratio 0.539215
Omega Ratio 1.195251
Sortino Ratio 1.501175
dtype: object
Set the Rebalancing Frequency as Quarterly
# Select the first index of each quarter
rb_mask = ~_price.index.to_period('Q').duplicated()
print(rb_mask.sum())
Allocate the Fixed weight of the Portfolio Symbol every Quarter for Rebalancing
rb_size = np.full_like(_price, np.nan)
rb_size[rb_mask, :] = np.concatenate(weights) # allocate at mask
print(rb_size.shape)
rb_size
Output
(3437, 3)
array([[0.25, 0.35, 0.4 ],
[ nan, nan, nan],
[ nan, nan, nan],
...,
[0.25, 0.35, 0.4 ],
[ nan, nan, nan],
[ nan, nan, nan]])
Run the Portfolio Backtesting simulation with Quarterly Rebalancing
# Run simulation, with rebalancing Quarterly
rb_portfolio = vbt.Portfolio.from_orders(
close=_price,
size=rb_size,
size_type='targetpercent',
group_by='symbol_group',
cash_sharing=True,
call_seq='auto', # important: sell before buy
fees=0.001,
init_cash=initial_capital,
freq='1D',
min_size =1,
size_granularity = 1
)
#print(len(portfolio.orders))
# Analyze results
rb_stats = rb_portfolio.stats()
print(rb_stats)
Output
Start 2010-01-04 00:00:00
End 2023-12-05 00:00:00
Period 3437 days 00:00:00
Start Value 1000000.0
End Value 12152617.650598
Total Return [%] 1115.261765
Benchmark Return [%] 915.258361
Max Gross Exposure [%] 99.997792
Total Fees Paid 18202.550483
Max Drawdown [%] 35.573617
Max Drawdown Duration 302 days 00:00:00
Total Trades 92
Total Closed Trades 89
Total Open Trades 3
Open Trade PnL 6655473.896085
Win Rate [%] 96.629213
Best Trade [%] 492.873291
Worst Trade [%] -25.832457
Avg Winning Trade [%] 122.649975
Avg Losing Trade [%] -12.796029
Avg Winning Trade Duration 1771 days 19:15:20.930232544
Avg Losing Trade Duration 372 days 08:00:00
Profit Factor 456.914936
Expectancy 50529.705107
Sharpe Ratio 1.084833
Calmar Ratio 0.564907
Omega Ratio 1.209034
Sortino Ratio 1.604894
dtype: object
Get the Portfolio Returns and Portfolio Drawdown
# Overall Portfolio Performance and Drawdown Metric
performance = rb_portfolio.total_return()
drawdowns = rb_portfolio.drawdowns
print(f"Total return: {performance*100:.2f}%")
print(f"Max drawdown: {drawdowns.max_drawdown()*100:.2f}%")
Output
Total return: 1115.26%
Max drawdown: -35.57%
Get the Individual Stock Returns
performance = rb_portfolio.total_return(group_by=False)*100
print("Individual Stock Returns")
print(performance)
Output
Individual Stock Returns
weights symbol_group
0.25 0 HDFCBANK.NS 254.237296
0.35 0 MARUTI.NS 386.338686
0.40 0 TCS.NS 469.258814
Name: total_return, dtype: float64
Access Quarterly Rebalanced Trade Details
# Accessing quarterly rebalanced trade details
trades = rb_portfolio.trades.records_readable
print("\nTrade Details:")
trades
Output
Trade Details:
Exit Trade Id Column Size Entry Timestamp Avg Entry Price Entry Fees Exit Timestamp Avg Exit Price Exit Fees PnL Return Direction Status Position Id
0 0 (0.25, 0, HDFCBANK.NS) 154.0 2010-01-04 153.989563 23.714393 2010-04-01 175.074295 26.961441 3.196373e+03 0.134786 Long Closed 0
1 1 (0.25, 0, HDFCBANK.NS) 35.0 2010-01-04 153.989563 5.389635 2010-07-01 173.304581 6.065660 6.645703e+02 0.123305 Long Closed 0
2 2 (0.25, 0, HDFCBANK.NS) 93.0 2010-01-04 153.989563 14.321029 2010-10-01 227.215790 21.131068 6.774587e+03 0.473052 Long Closed 0
3 3 (0.25, 0, HDFCBANK.NS) 18.0 2010-01-04 160.118923 2.882141 2011-04-01 212.042328 3.816762 9.279224e+02 0.321956 Long Closed 0
4 4 (0.25, 0, HDFCBANK.NS) 119.0 2010-01-04 160.118923 19.054152 2011-07-01 228.835663 27.231444 8.131006e+03 0.426731 Long Closed 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
87 87 (0.4, 0, TCS.NS) 80.0 2010-01-04 1024.540954 81.963276 2021-10-01 3554.484131 284.358730 2.020291e+05 2.464874 Long Closed 2
88 88 (0.4, 0, TCS.NS) 8.0 2010-01-04 1024.540954 8.196328 2022-01-03 3644.890381 29.159123 2.092544e+04 2.553026 Long Closed 2
89 89 (0.4, 0, TCS.NS) 28.0 2010-01-04 1355.218782 37.946126 2023-01-02 3156.698975 88.387571 5.031511e+04 1.325962 Long Closed 2
90 90 (0.4, 0, TCS.NS) 60.0 2010-01-04 1439.010100 86.340606 2023-10-03 3504.781494 210.286890 1.236497e+05 1.432115 Long Closed 2
91 91 (0.4, 0, TCS.NS) 1346.0 2010-01-04 1439.010100 1936.907595 2023-12-04 3512.449951 0.000000 2.788913e+06 1.439879 Long Open 2
92 rows × 14 columns
Plot Portfolio Equity and Portfolio Drawdown using Plotly Library
import plotly.graph_objs as go
# Extracting equity and drawdown data
equity_data = rb_portfolio.value()
drawdown_data = rb_portfolio.drawdown()*100
# Plotting the equity curve with Plotly
equity_trace = go.Scatter(x=equity_data.index, y=equity_data, mode='lines', name='Equity Curve')
equity_layout = go.Layout(title='Equity Curve', xaxis_title='Date', yaxis_title='Equity')
equity_fig = go.Figure(data=[equity_trace], layout=equity_layout)
equity_fig.show()
# Plotting the drawdown curve as a reddish-brown area plot with Plotly
drawdown_trace = go.Scatter(
x=drawdown_data.index,
y=drawdown_data,
mode='lines',
name='Drawdown Curve',
fill='tozeroy',
line=dict(color='brown')
)
drawdown_layout = go.Layout(
title='Drawdown Curve',
xaxis_title='Date',
yaxis_title='Drawdown %',
template='plotly_white'
)
drawdown_fig = go.Figure(data=[drawdown_trace], layout=drawdown_layout)
drawdown_fig.show()
Output

Get the Remaining Cash and Value of the Portfolio
rb_portfolio.plot(subplots=['cash', 'value']).show()

Plot Allocation of Weights Over Period of Time
def plot_allocation(rb_pf):
# Plot weights development of the portfolio
rb_asset_value = rb_pf.asset_value(group_by=False)
rb_value = rb_pf.value()
rb_idxs = np.flatnonzero((rb_pf.asset_flow() != 0).any(axis=1))
rb_dates = rb_pf.wrapper.index[rb_idxs]
fig = (rb_asset_value.vbt / rb_value).vbt.plot(
trace_names=symbols,
trace_kwargs=dict(
stackgroup='one'
)
)
for rb_date in rb_dates:
fig.add_shape(
dict(
xref='x',
yref='paper',
x0=rb_date,
x1=rb_date,
y0=0,
y1=1,
line_color=fig.layout.template.layout.plot_bgcolor
)
)
fig.show()
plot_allocation(rb_portfolio)
Output

In the next tutorial we will be exploring more about VectorBT Grid Optimization using Superfast Supertrend
Reference