Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

Mastering VectorBT – Portfolio Backtesting and Rebalancing – Part 2 – Python Tutorial

5 min read

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

Portfolio Optimization – VectorBT

Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

[Live Coding Webinar] Build Your First Trading Bridge for…

In this course, you will be learning to build your own trading bridge using Python. This 60-minute session is perfect for traders, Python enthusiasts,...
Rajandran R
1 min read

How to Place Orders Concurrently using ThreadPoolExecutor – Python…

Creating concurrent orders is essential for active traders, especially those handling large funds, as it allows for executing multiple trade orders simultaneously, thereby maximizing...
Rajandran R
2 min read

Host your Python Flask Web Application using pyngrok and…

Ngrok offers several significant advantages for developers, especially when it comes to testing applications or hosting machine learning models. Ngrok allows you to expose...
Rajandran R
1 min read

Leave a Reply

Get Notifications, Alerts on Market Updates, Trading Tools, Automation & More