TQQQ For The Long Term Minimal v1.4 backtest using synthetic LETFs¶

This a backtest of the aforementioned strategy using synthetically generated LETFs data that allows us to have valid and accurate simulated close prices, dating all the way back to the inception date of the corresponding unleveraged ETF they are based on. Real and simulated data are combined together to form a longer time-series.

In [ ]:
import pandas as pd
import yfinance as yf
import pandas_ta as ta
import plotly.express as px
from IPython.display import IFrame
In [ ]:
# Import the LETFs data
data = pd.read_csv("./data/extended-leveraged-etfs.csv", index_col=0, parse_dates=True)
data
Out[ ]:
SSO UPRO SPXL SH SDS SPXS QLD TQQQ PSQ QID ... TYO UBT TMF TBF TBT TMV UGL GLL UVXY TLT
Date
1986-05-19 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN 0.69170 NaN NaN NaN NaN NaN NaN 10.60539
1986-05-20 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN 0.68715 NaN NaN NaN NaN NaN NaN 10.58417
1986-05-21 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN 0.70741 NaN NaN NaN NaN NaN NaN 10.69021
1986-05-22 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN 0.70279 NaN NaN NaN NaN NaN NaN 10.66902
1986-05-23 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN 0.70658 NaN NaN NaN NaN NaN NaN 10.69021
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2022-10-12 39.42 27.97 52.77 17.30 54.48 29.31 35.26 18.10 15.14 28.14 ... 13.84 24.81 8.06000 22.69 33.01 141.25999 47.6000 36.62 13.25000 100.35000
2022-10-13 41.49 30.14 56.85 16.85 51.63 27.04 36.86 19.31 14.79 26.89 ... 14.03 24.32 7.85000 22.93 33.63 145.06000 46.9100 37.12 12.69000 99.39000
2022-10-14 39.60 28.08 52.98 17.23 53.93 28.87 34.62 17.57 15.25 28.51 ... 14.20 23.97 7.67000 23.12 34.19 148.89000 45.7400 38.08 13.14000 98.57000
2022-10-17 41.61 30.21 57.00 16.79 51.22 26.66 36.97 19.30 14.72 26.56 ... 14.18 23.68 7.53000 23.26 34.61 151.49001 45.9700 37.84 12.61000 98.09000
2022-10-18 42.57 31.11 58.99 16.59 49.96 25.73 37.55 19.78 14.59 26.17 ... 14.13 23.80 7.59000 23.21 34.40 150.53999 46.1852 37.72 12.21000 98.32000

9180 rows × 43 columns

We now download the additional unleveraged ETFs required for this specific strategy backtest, and add them to the dataframe. It's possible to generate synthetic versions of these unleveraged ETFs too (there's already TLT in the data above, for example), but the results might be less accurate depending on what index you choose to generate the returns.

In [ ]:
# Download the additional unleveraged ETFs required for this specific strategy backtest
unleveraged_etfs = yf.download(["SPY", "QQQ"])["Adj Close"]
unleveraged_etfs.index = unleveraged_etfs.index.tz_localize(None) # remove time info while keeping index type as datetime
# Add the unleveraged ETFs to the main dataframe
merged_df = pd.concat([data, unleveraged_etfs], axis=1)
merged_df
[*********************100%***********************]  2 of 2 completed
Out[ ]:
SSO UPRO SPXL SH SDS SPXS QLD TQQQ PSQ QID ... TMF TBF TBT TMV UGL GLL UVXY TLT QQQ SPY
Date
1986-05-19 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0.69170 NaN NaN NaN NaN NaN NaN 10.60539 NaN NaN
1986-05-20 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0.68715 NaN NaN NaN NaN NaN NaN 10.58417 NaN NaN
1986-05-21 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0.70741 NaN NaN NaN NaN NaN NaN 10.69021 NaN NaN
1986-05-22 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0.70279 NaN NaN NaN NaN NaN NaN 10.66902 NaN NaN
1986-05-23 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0.70658 NaN NaN NaN NaN NaN NaN 10.69021 NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2022-10-13 41.49 30.14 56.85 16.85 51.63 27.04 36.86 19.31 14.79 26.89 ... 7.85000 22.93 33.63 145.06000 46.9100 37.12 12.69000 99.39000 268.820007 365.970001
2022-10-14 39.60 28.08 52.98 17.23 53.93 28.87 34.62 17.57 15.25 28.51 ... 7.67000 23.12 34.19 148.89000 45.7400 38.08 13.14000 98.57000 260.739990 357.630005
2022-10-17 41.61 30.21 57.00 16.79 51.22 26.66 36.97 19.30 14.72 26.56 ... 7.53000 23.26 34.61 151.49001 45.9700 37.84 12.61000 98.09000 269.350006 366.820007
2022-10-18 42.57 31.11 58.99 16.59 49.96 25.73 37.55 19.78 14.59 26.17 ... 7.59000 23.21 34.40 150.53999 46.1852 37.72 12.21000 98.32000 271.480011 371.130005
2022-10-19 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN 270.489990 368.500000

9181 rows × 45 columns

We now get a subset of this dataframe with only the tickers that are relevant for this particular strategy, and we do a sanity check to make sure there are no issues.

In [ ]:
strategy_symbols = ["SPY", "TQQQ", "SPXL", "UVXY", "SQQQ", "TLT", "QQQ"]
strategy_data = merged_df[strategy_symbols].copy()
strategy_data
Out[ ]:
SPY TQQQ SPXL UVXY SQQQ TLT QQQ
Date
1986-05-19 NaN NaN NaN NaN NaN 10.60539 NaN
1986-05-20 NaN NaN NaN NaN NaN 10.58417 NaN
1986-05-21 NaN NaN NaN NaN NaN 10.69021 NaN
1986-05-22 NaN NaN NaN NaN NaN 10.66902 NaN
1986-05-23 NaN NaN NaN NaN NaN 10.69021 NaN
... ... ... ... ... ... ... ...
2022-10-13 365.970001 19.31 56.85 12.69000 59.11 99.39000 268.820007
2022-10-14 357.630005 17.57 52.98 13.14000 64.38 98.57000 260.739990
2022-10-17 366.820007 19.30 57.00 12.61000 58.00 98.09000 269.350006
2022-10-18 371.130005 19.78 58.99 12.21000 56.59 98.32000 271.480011
2022-10-19 368.500000 NaN NaN NaN NaN NaN 270.489990

9181 rows × 7 columns

In [ ]:
strategy_data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 9181 entries, 1986-05-19 to 2022-10-19
Data columns (total 7 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   SPY     7486 non-null   float64
 1   TQQQ    5943 non-null   float64
 2   SPXL    8769 non-null   float64
 3   UVXY    8264 non-null   object 
 4   SQQQ    5943 non-null   float64
 5   TLT     9180 non-null   float64
 6   QQQ     5944 non-null   float64
dtypes: float64(6), object(1)
memory usage: 573.8+ KB

UVXY is currently of dtype object. Let's try to convert the column to float64, so we can use it in our calculations.

In [ ]:
strategy_data["UVXY"] = strategy_data["UVXY"].astype("float64")
strategy_data["UVXY"].info()
<class 'pandas.core.series.Series'>
DatetimeIndex: 9181 entries, 1986-05-19 to 2022-10-19
Series name: UVXY
Non-Null Count  Dtype  
--------------  -----  
8264 non-null   float64
dtypes: float64(1)
memory usage: 143.5 KB

We now create the signals columns we need, using built-in pandas methods or pandas-ta.

In [ ]:
# Signals
strategy_data["SPY_MA200"] = ta.sma(strategy_data["SPY"], length=200)
strategy_data["TQQQ_RSI10"] = ta.rsi(strategy_data["TQQQ"], length=10)
strategy_data["SPXL_RSI10"] = ta.rsi(strategy_data["SPXL"], length=10)
strategy_data["SPY_RSI10"] = ta.rsi(strategy_data["SPY"], length=10)
strategy_data["QQQ_RSI10"] = ta.rsi(strategy_data["QQQ"], length=10)
strategy_data["UVXY_RSI10"] = ta.rsi(strategy_data["UVXY"], length=10)
strategy_data["TQQQ_MA20"] = ta.sma(strategy_data["TQQQ"], length=20)
strategy_data["SQQQ_RSI10"] = ta.rsi(strategy_data["SQQQ"], length=10)
strategy_data["TLT_RSI10"] = ta.rsi(strategy_data["TLT"], length=10)
strategy_data["QQQ_5dgain"] = strategy_data["QQQ"].pct_change(5) * 100
strategy_data["TQQQ_1dgain"] = strategy_data["TQQQ"].pct_change(1) * 100
strategy_data["TQQQ_10dSTDreturn"] = strategy_data["TQQQ_1dgain"].rolling(10).std()
In [ ]:
# Drop missing values so we start the backtest from the first day all signals and prices are available
strategy_data.dropna(inplace=True)
In [ ]:
# Add "BUY" column in first position
strategy_data.insert(0, "BUY", pd.NA) # pd.NA is the new pandas native version of np.nan
In [ ]:
strategy_data
Out[ ]:
BUY SPY TQQQ SPXL UVXY SQQQ TLT QQQ SPY_MA200 TQQQ_RSI10 SPXL_RSI10 SPY_RSI10 QQQ_RSI10 UVXY_RSI10 TQQQ_MA20 SQQQ_RSI10 TLT_RSI10 QQQ_5dgain TQQQ_1dgain TQQQ_10dSTDreturn
Date
1999-04-07 NaN 86.938591 81.72095 45.31803 2.879196e+20 2.102258e+08 33.35910 47.432098 75.215919 63.071977 62.293455 63.046299 64.348921 31.463376 71.300601 34.759583 61.585289 3.345116 -3.161540 6.725346
1999-04-08 NaN 88.040405 83.63468 47.05327 2.817470e+20 2.053532e+08 33.66911 47.809162 75.298414 65.188269 67.303069 67.243478 66.252551 30.133641 72.093586 33.332605 68.707198 5.717654 2.341786 6.329253
1999-04-09 NaN 88.060799 85.15391 47.49072 2.644424e+20 2.016712e+08 33.57613 48.105450 75.376765 66.863324 68.476215 67.319813 67.755756 26.628145 72.914247 32.221921 64.709255 4.200681 1.816507 5.661916
1999-04-12 NaN 88.999359 83.40080 48.55757 2.655143e+20 2.058710e+08 33.63814 47.782230 75.454958 62.977932 71.269533 70.799538 64.284968 27.210950 73.899929 34.968226 66.168040 -0.112632 -2.058755 5.549664
1999-04-13 NaN 88.428055 79.98025 47.59537 2.781925e+20 2.143630e+08 33.42109 47.135807 75.530900 55.931812 65.457097 66.043419 57.715211 34.090945 74.441727 40.394174 57.003922 -1.657761 -4.101340 4.904016
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2022-10-12 NaN 356.559998 18.10000 52.77000 1.325000e+01 6.335000e+01 100.35000 262.660004 413.231901 29.380442 32.266999 33.369398 30.256253 65.041294 21.645000 68.684792 34.975614 -6.851552 -0.220507 6.327154
2022-10-13 NaN 365.970001 19.31000 56.85000 1.269000e+01 5.911000e+01 99.39000 268.820007 412.704691 37.938121 43.105232 45.241914 40.328453 57.202852 21.356000 57.165998 31.833812 -3.910495 6.685083 6.389549
2022-10-14 NaN 357.630005 17.57000 52.98000 1.314000e+01 6.438000e+01 98.57000 260.739990 412.132767 31.784061 36.884882 38.488365 33.316123 61.360555 21.004500 65.221002 29.333059 -3.106658 -9.010875 6.806726
2022-10-17 NaN 366.820007 19.30000 57.00000 1.261000e+01 5.800000e+01 98.09000 269.350006 411.613316 42.150708 45.896145 47.993457 44.700758 54.439482 20.710000 52.053713 27.907179 1.103563 9.846329 7.220773
2022-10-18 NaN 371.130005 19.78000 58.99000 1.221000e+01 5.659000e+01 98.32000 271.480011 411.121348 44.739629 49.835568 51.869085 47.179520 49.735237 20.469500 49.595027 29.725895 3.322554 2.487047 6.484238

5924 rows × 20 columns

Generate asset allocation history¶

We create an empty dataframe that will be populated with our asset allocation history as we iterate over the strategy_data dataframe and calculate the strategy signal for each row (day). This allocation dataframe will later be passed to vectorbt.pro for backtesting.

In [ ]:
strategy_allocations = pd.DataFrame(index=strategy_data.index, columns=["TQQQ", "SQQQ", "UVXY", "TLT"])
strategy_allocations
Out[ ]:
TQQQ SQQQ UVXY TLT
Date
1999-04-07 NaN NaN NaN NaN
1999-04-08 NaN NaN NaN NaN
1999-04-09 NaN NaN NaN NaN
1999-04-12 NaN NaN NaN NaN
1999-04-13 NaN NaN NaN NaN
... ... ... ... ...
2022-10-12 NaN NaN NaN NaN
2022-10-13 NaN NaN NaN NaN
2022-10-14 NaN NaN NaN NaN
2022-10-17 NaN NaN NaN NaN
2022-10-18 NaN NaN NaN NaN

5924 rows × 4 columns

We now write our strategy logic and iterate over each row of the dataframe, finding the asset allocations for each row and putting them in the strategy_allocations dataframe (1 = 100% allocation, 0.5 = 50% etc.)

In [ ]:
# Strategy logic 

for row in strategy_data.index:
    if strategy_data.at[row, "SPY"] > strategy_data.at[row, "SPY_MA200"]: # using .at when workining with single values is more efficient than .loc
        if strategy_data.at[row, "TQQQ_RSI10"] > 79:
            strategy_allocations.at[row, "UVXY"] = 1
        else:
            if strategy_data.at[row, "SPXL_RSI10"] > 80:
                strategy_allocations.at[row, "UVXY"] = 1
            else:
                if strategy_data.at[row, "QQQ_5dgain"] < -6:
                    if strategy_data.at[row, "TQQQ_1dgain"] > 5:
                        strategy_allocations.at[row, "SQQQ"] = 1
                    else:
                        if strategy_data.at[row, "TQQQ_RSI10"] > 31:
                            strategy_allocations.at[row, "SQQQ"] = 1
                        else:
                            strategy_allocations.at[row, "TQQQ"] = 1
                else:
                    if strategy_data.at[row, "QQQ_RSI10"] > 80:
                        strategy_allocations.at[row, "SQQQ"] = 1
                    else:
                        if strategy_data.at[row, "TQQQ_10dSTDreturn"] > 5:
                            strategy_allocations.at[row, "TLT"] = 1
                        else:
                            strategy_allocations.at[row, "TQQQ"] = 1
    else:
        if strategy_data.at[row, "TQQQ_RSI10"] < 31:
            strategy_allocations.at[row, "TQQQ"] = 1
        else:
            if strategy_data.at[row, "SPY_RSI10"] < 30:
                strategy_allocations.at[row, "TQQQ"] = 1
            else:
                if strategy_data.at[row, "UVXY_RSI10"] > 74:
                    if strategy_data.at[row, "UVXY_RSI10"] > 84:
                        if (
                            strategy_data.at[row, "SQQQ_RSI10"]
                            > strategy_data.at[row, "TLT_RSI10"]
                        ):
                            strategy_allocations.at[row, "SQQQ"] = 1
                        else:
                            strategy_allocations.at[row, "TLT"] = 1
                    else:
                        strategy_allocations.at[row, "UVXY"] = 1
                else:
                    if strategy_data.at[row, "TQQQ"] > strategy_data.at[row, "TQQQ_MA20"]:
                        if strategy_data.at[row, "SQQQ_RSI10"] < 31:
                            strategy_allocations.at[row, "SQQQ"] = 1
                        else:
                            strategy_allocations.at[row, "TQQQ"] = 1
                    else:
                        if (
                            strategy_data.at[row, "SQQQ_RSI10"]
                            > strategy_data.at[row, "TLT_RSI10"]
                        ):
                            strategy_allocations.at[row, "SQQQ"] = 1
                        else:
                            strategy_allocations.at[row, "TLT"] = 1
In [ ]:
# Replace NaN values with 0
strategy_allocations.fillna(0, inplace=True)
In [ ]:
strategy_allocations
Out[ ]:
TQQQ SQQQ UVXY TLT
Date
1999-04-07 0 0 0 1
1999-04-08 0 0 0 1
1999-04-09 0 0 0 1
1999-04-12 0 0 0 1
1999-04-13 1 0 0 0
... ... ... ... ...
2022-10-12 1 0 0 0
2022-10-13 0 1 0 0
2022-10-14 0 1 0 0
2022-10-17 0 1 0 0
2022-10-18 0 1 0 0

5924 rows × 4 columns

Saving the data to .csv¶

In [ ]:
# Save strategy_allocations to CSV for later use
strategy_allocations.to_csv("./data/strategy_allocations.csv")

We also save a dataframe with the close prices of the instruments used in our strategy allocation. This will be needed for backtesting in vectorbt.pro.

In [ ]:
strategy_prices = strategy_data[["TQQQ", "SQQQ", "UVXY", "TLT"]]
strategy_prices.to_csv("./data/strategy_prices.csv")
In [ ]:
# Save the entire strategy data. This won't be needed later, but it's nice to have, just in case we want to reuse it somewhere.
strategy_data.to_csv("./data/strategy_data.csv")

Backtesting the strategy¶

To run this backtest you'll need to have a valid subscription to vectorbt.pro.

Loading data into vectorbt.pro and preparing it for the backtest¶

First, we are loading the tickers close prices from the .csv file we saved earlier.

In [ ]:
import vectorbtpro as vbt

vbt_prices = vbt.CSVData.fetch("./data/strategy_prices.csv", index_col=0, parse_dates=True) # same syntax you would use with pd.read_csv
In [ ]:
vbt_prices.data
Out[ ]:
{'strategy_prices':                                TQQQ          SQQQ          UVXY        TLT
 Date                                                                      
 1999-04-07 00:00:00+00:00  81.72095  2.102258e+08  2.879196e+20   33.35910
 1999-04-08 00:00:00+00:00  83.63468  2.053532e+08  2.817470e+20   33.66911
 1999-04-09 00:00:00+00:00  85.15391  2.016712e+08  2.644424e+20   33.57613
 1999-04-12 00:00:00+00:00  83.40080  2.058710e+08  2.655143e+20   33.63814
 1999-04-13 00:00:00+00:00  79.98025  2.143630e+08  2.781925e+20   33.42109
 ...                             ...           ...           ...        ...
 2022-10-12 00:00:00+00:00  18.10000  6.335000e+01  1.325000e+01  100.35000
 2022-10-13 00:00:00+00:00  19.31000  5.911000e+01  1.269000e+01   99.39000
 2022-10-14 00:00:00+00:00  17.57000  6.438000e+01  1.314000e+01   98.57000
 2022-10-17 00:00:00+00:00  19.30000  5.800000e+01  1.261000e+01   98.09000
 2022-10-18 00:00:00+00:00  19.78000  5.659000e+01  1.221000e+01   98.32000
 
 [5924 rows x 4 columns]}

We now generate a dataframe in a format compatible with vectorbt.pro, which will be populated with our allocation weights later.

In [ ]:
symbol_wrapper = vbt_prices.get_symbol_wrapper(symbols=["TQQQ", "SQQQ", "UVXY", "TLT"], freq='1d') # the order of symbols= should match the order of columns in strategy_allocations
print(symbol_wrapper.shape)
print(strategy_allocations.shape)
(5924, 4)
(5924, 4)

The wrapper should have the same number of rows and columns as the data we have loaded. Now we can fill symbol_wrapper with data from our allocations dataframe.

In [ ]:
tickers_allocations = vbt.PortfolioOptimizer.from_allocations(symbol_wrapper, strategy_allocations)
allocations = tickers_allocations.allocations # can also type tickers_allocations.get_allocations()
allocations
Out[ ]:
symbol TQQQ SQQQ UVXY TLT
Date
1999-04-07 00:00:00+00:00 0.0 0.0 0.0 1.0
1999-04-08 00:00:00+00:00 0.0 0.0 0.0 1.0
1999-04-09 00:00:00+00:00 0.0 0.0 0.0 1.0
1999-04-12 00:00:00+00:00 0.0 0.0 0.0 1.0
1999-04-13 00:00:00+00:00 1.0 0.0 0.0 0.0
... ... ... ... ...
2022-10-12 00:00:00+00:00 1.0 0.0 0.0 0.0
2022-10-13 00:00:00+00:00 0.0 1.0 0.0 0.0
2022-10-14 00:00:00+00:00 0.0 1.0 0.0 0.0
2022-10-17 00:00:00+00:00 0.0 1.0 0.0 0.0
2022-10-18 00:00:00+00:00 0.0 1.0 0.0 0.0

5924 rows × 4 columns

Now we get a dataframe with the close prices from the data we have loaded.

In [ ]:
close_prices = vbt_prices.get()
close_prices
Out[ ]:
TQQQ SQQQ UVXY TLT
Date
1999-04-07 00:00:00+00:00 81.72095 2.102258e+08 2.879196e+20 33.35910
1999-04-08 00:00:00+00:00 83.63468 2.053532e+08 2.817470e+20 33.66911
1999-04-09 00:00:00+00:00 85.15391 2.016712e+08 2.644424e+20 33.57613
1999-04-12 00:00:00+00:00 83.40080 2.058710e+08 2.655143e+20 33.63814
1999-04-13 00:00:00+00:00 79.98025 2.143630e+08 2.781925e+20 33.42109
... ... ... ... ...
2022-10-12 00:00:00+00:00 18.10000 6.335000e+01 1.325000e+01 100.35000
2022-10-13 00:00:00+00:00 19.31000 5.911000e+01 1.269000e+01 99.39000
2022-10-14 00:00:00+00:00 17.57000 6.438000e+01 1.314000e+01 98.57000
2022-10-17 00:00:00+00:00 19.30000 5.800000e+01 1.261000e+01 98.09000
2022-10-18 00:00:00+00:00 19.78000 5.659000e+01 1.221000e+01 98.32000

5924 rows × 4 columns

Running the backtest¶

We now have everything that is needed to run our backtest: a dataframe with the close prices and one with the target allocations, both in a valid format recognized by vectorbt.pro . We can proceed with running the backtest and showing the results.

In [ ]:
portfolio = vbt.Portfolio.from_orders(
    
    close=close_prices,
    #bm_close=put_your_benchmark_close_prices_here,
    size=allocations, # Here we pass the allocations we calculated earlier
    size_type="targetpercent",
    group_by=True,  
    cash_sharing=True,
    call_seq="auto",
    freq='D', # needed to calculate annualized returns and some metrics like sharpe, sortino etc in portfolio.stats()
    fees=0, # can specify a transaction fee
    )

Read the documentation of Portfolio.from_orders() for more info. It's possible compare the performance to a benchmark if you pass the desired benchmark strategy/ticker time series to #bm_close . Beware that the close_prices dataframe and the benchmark you are providing must have the same index for accurate comparisons.

Analyze backtest performance¶

In this notebook we will show just the tip of the iceberg of what's possible with vectorbt.pro . Read the documentation for more ideas and metrics.

In [ ]:
# This is the most basic overview of the portfolio metrics
portfolio.stats()
Out[ ]:
Start                         1999-04-07 00:00:00+00:00
End                           2022-10-18 00:00:00+00:00
Period                               5924 days 00:00:00
Start Value                                       100.0
Min Value                                     45.944895
Max Value                            49532199635.866318
End Value                            43538788092.477097
Total Return [%]                     43538787992.477097
Benchmark Return [%]                          -20.26587
Total Time Exposure [%]                       99.561107
Max Gross Exposure [%]                            100.0
Max Drawdown [%]                              73.491664
Max Drawdown Duration                 875 days 00:00:00
Total Orders                                       1910
Total Fees Paid                                     0.0
Total Trades                                        959
Win Rate [%]                                  56.680585
Best Trade [%]                                65.372054
Worst Trade [%]                              -37.213453
Avg Winning Trade [%]                          9.067937
Avg Losing Trade [%]                          -5.788179
Avg Winning Trade Duration    7 days 18:17:54.033149171
Avg Losing Trade Duration     7 days 18:41:46.077348066
Profit Factor                                  3.598681
Expectancy                              47471405.655066
Sharpe Ratio                                   1.813416
Calmar Ratio                                   1.805824
Omega Ratio                                    1.368747
Sortino Ratio                                  3.026992
Name: group, dtype: object

Returns¶

We can go even further than that and do in-depth analysis of returns and other metrics.

In [ ]:
portfolio.returns_stats()
Out[ ]:
Start                        1999-04-07 00:00:00+00:00
End                          2022-10-18 00:00:00+00:00
Period                              5924 days 00:00:00
Total Return [%]                    43538787992.477104
Benchmark Return [%]                         -20.26587
Annualized Return [%]                       132.712975
Annualized Volatility [%]                    88.617529
Max Drawdown [%]                             73.491664
Max Drawdown Duration                875 days 00:00:00
Sharpe Ratio                                  1.813416
Calmar Ratio                                  1.805824
Omega Ratio                                   1.368747
Sortino Ratio                                 3.026992
Skew                                          1.516343
Kurtosis                                     16.768799
Tail Ratio                                    1.117592
Common Sense Ratio                            2.600781
Value at Risk                                 -0.06385
Alpha                                         3.985386
Beta                                         -0.107782
Name: group, dtype: object
Annual Returns¶

We can see the annual returns of the strategy, and the advantage of having the output in a simple pandas series is that we can then manipulate and display this data however we want to analyze it in different ways, for example by making a bar plot of the annual returns, in this specific case.

In [ ]:
# Annual returns
annual_returns = portfolio.annual_returns # can also type portfolio.get_annual_returns()
annual_returns
Out[ ]:
Date
1999-04-07 00:00:00+00:00    -0.225791
2000-04-06 00:00:00+00:00     4.087178
2001-04-06 00:00:00+00:00    -0.189695
2002-04-06 00:00:00+00:00     1.131165
2003-04-06 00:00:00+00:00     0.884160
2004-04-05 00:00:00+00:00    -0.289397
2005-04-05 00:00:00+00:00     0.356911
2006-04-05 00:00:00+00:00    -0.166240
2007-04-05 00:00:00+00:00     0.687942
2008-04-04 00:00:00+00:00     2.966231
2009-04-04 00:00:00+00:00     1.305499
2010-04-04 00:00:00+00:00     1.417747
2011-04-04 00:00:00+00:00     0.821203
2012-04-03 00:00:00+00:00     0.130452
2013-04-03 00:00:00+00:00     1.395278
2014-04-03 00:00:00+00:00     0.842624
2015-04-03 00:00:00+00:00     1.350014
2016-04-02 00:00:00+00:00     0.675128
2017-04-02 00:00:00+00:00     1.948730
2018-04-02 00:00:00+00:00     1.696418
2019-04-02 00:00:00+00:00    25.100971
2020-04-01 00:00:00+00:00     9.515804
2021-04-01 00:00:00+00:00     1.459666
2022-04-01 00:00:00+00:00     4.898051
Freq: 365D, Name: group, dtype: float64
In [ ]:
# Make an bar plot of the annual returns
annual_returns_plot = px.bar(annual_returns, x=annual_returns.index, y=annual_returns.values, labels={"x":"Year", "y":"Portfolio Return"}, title="Annual Returns")
annual_returns_plot.show()
In [ ]:
# Delete this when running the notebook yourself
annual_returns_plot.write_html("./html/annual_returns.html", default_width=800, default_height=500)
IFrame(src='./html/annual_returns.html', width=800, height=500)
Out[ ]:

Plots¶

Vectorbt.pro also has a lot of already built-in interactive plots that can be generated very easily. Below we'll plot all the available plots just for reference

In [ ]:
# Let's plot all the possible subplots just for reference. By default there's only the return curve.
all_portfolio_plots = portfolio.plot(settings=dict(bm_returns=True), subplots="all")
all_portfolio_plots
/Users/lorenzominghetti/Projects/vectorbt.pro-1.7.1/vectorbtpro/generic/plots_builder.py:396: UserWarning:

Subplot 'orders' does not support grouped data

/Users/lorenzominghetti/Projects/vectorbt.pro-1.7.1/vectorbtpro/generic/plots_builder.py:396: UserWarning:

Subplot 'trades' does not support grouped data

/Users/lorenzominghetti/Projects/vectorbt.pro-1.7.1/vectorbtpro/generic/plots_builder.py:396: UserWarning:

Subplot 'trade_pnl' does not support grouped data

/Users/lorenzominghetti/Projects/vectorbt.pro-1.7.1/vectorbtpro/generic/plots_builder.py:396: UserWarning:

Subplot 'asset_flow' does not support grouped data

/Users/lorenzominghetti/Projects/vectorbt.pro-1.7.1/vectorbtpro/generic/plots_builder.py:396: UserWarning:

Subplot 'assets' does not support grouped data

FigureWidget({
    'data': [{'legendgroup': '0',
              'line': {'color': '#2ca02c'},
              'mo…
In [ ]:
# Delete this this when running the notebook yourself
all_portfolio_plots.write_html("./html/all_portfolio_plots.html", default_width=800, default_height=1000)
IFrame(src='./html/all_portfolio_plots.html', width=800, height=1000)
Out[ ]:

It's also possible to specify custom subplots. The example below would create a cumulative returns and rolling drawdown plot. You can set the settings across all the subplots by using the global template mapping by adding template_context=dict(window=10) for example.

In [ ]:
# subplots = [
#     ('cumulative_returns', dict(
#         title='Cumulative Returns',
#         yaxis_kwargs=dict(title='Cumulative returns'),
#         plot_func='cumulative_returns.vbt.plot',
#         select_col_cumulative_returns=True,
#         pass_add_trace_kwargs=True
#     )),
#     ('rolling_drawdown', dict(
#         title='Rolling Drawdown',
#         yaxis_kwargs=dict(title='Rolling drawdown'),
#         plot_func=[
#             'returns_acc',  # returns accessor
#             (
#                 'rolling_max_drawdown',  # function name
#                 (vbt.Rep('window'),)),  # positional arguments
#             'vbt.plot'  # plotting function
#         ],
#         select_col_returns_acc=True,
#         pass_add_trace_kwargs=True,
#         trace_names=[vbt.Sub('rolling_drawdown(${window})')],  # add window to the trace name
#     ))
# ]

# portfolio.plot(subplots, template_context=dict(window=10))

Drawdowns¶

Like the returns, drawdowns can also be analyzed extensively.

In [ ]:
portfolio.get_drawdowns().stats()
Out[ ]:
Start                           1999-04-07 00:00:00+00:00
End                             2022-10-18 00:00:00+00:00
Period                                 5924 days 00:00:00
Coverage [%]                                     86.79946
Total Records                                         332
Total Recovered Drawdowns                             331
Total Active Drawdowns                                  1
Active Drawdown [%]                             12.100031
Active Duration                           2 days 00:00:00
Active Recovery [%]                                   0.0
Active Recovery Return [%]                            0.0
Active Recovery Duration                  0 days 00:00:00
Max Drawdown [%]                                73.491664
Avg Drawdown [%]                                 8.102684
Max Drawdown Duration                   875 days 00:00:00
Avg Drawdown Duration          15 days 12:41:19.758308157
Max Recovery Return [%]                        294.319213
Avg Recovery Return [%]                         14.915101
Max Recovery Duration                   249 days 00:00:00
Avg Recovery Duration           7 days 12:06:31.540785498
Avg Recovery Duration Ratio                      1.367338
Name: group, dtype: object
In [ ]:
drawdown_plot = portfolio.get_drawdowns().plot()
drawdown_plot
FigureWidget({
    'data': [{'line': {'color': '#1f77b4'},
              'mode': 'lines',
              'name'…
In [ ]:
# Delete this this when running the notebook yourself
drawdown_plot.write_html("./html/drawdown_plot.html", default_width=800, default_height=500)
IFrame(src='./html/all_portfolio_plots.html', width=800, height=500)
Out[ ]:
In [ ]:
underwater_plot = portfolio.plot_underwater()
underwater_plot
FigureWidget({
    'data': [{'fill': 'tozeroy',
              'fillcolor': 'rgba(220,57,18,0.3000)',
         …
In [ ]:
# Delete this this when running the notebook yourself
underwater_plot.write_html("./html/drawdown_plot.html", default_width=800, default_height=500)
IFrame(src='./html/drawdown_plot.html', width=800, height=500)
Out[ ]:
In [ ]:
drawdown_df = portfolio.get_drawdowns().records_readable
drawdown_df
Out[ ]:
Drawdown Id Column Peak Index Start Index Valley Index End Index Peak Value Valley Value End Value Status
0 0 group 1999-04-08 00:00:00+00:00 1999-04-09 00:00:00+00:00 1999-04-16 00:00:00+00:00 1999-04-19 00:00:00+00:00 1.009293e+02 8.917106e+01 1.017766e+02 Recovered
1 1 group 1999-04-20 00:00:00+00:00 1999-04-21 00:00:00+00:00 1999-10-28 00:00:00+00:00 2000-01-06 00:00:00+00:00 1.086694e+02 4.954582e+01 1.268284e+02 Recovered
2 2 group 2000-01-06 00:00:00+00:00 2000-01-07 00:00:00+00:00 2000-04-14 00:00:00+00:00 2000-08-02 00:00:00+00:00 1.268284e+02 4.594489e+01 1.304440e+02 Recovered
3 3 group 2000-08-02 00:00:00+00:00 2000-08-03 00:00:00+00:00 2000-08-07 00:00:00+00:00 2000-08-31 00:00:00+00:00 1.304440e+02 1.135298e+02 1.365279e+02 Recovered
4 4 group 2000-09-01 00:00:00+00:00 2000-09-05 00:00:00+00:00 2000-09-07 00:00:00+00:00 2000-09-26 00:00:00+00:00 1.404799e+02 1.139203e+02 1.433934e+02 Recovered
... ... ... ... ... ... ... ... ... ... ...
327 327 group 2022-09-21 00:00:00+00:00 2022-09-22 00:00:00+00:00 2022-09-26 00:00:00+00:00 2022-09-29 00:00:00+00:00 3.661402e+10 3.311646e+10 3.800744e+10 Recovered
328 328 group 2022-09-29 00:00:00+00:00 2022-09-30 00:00:00+00:00 2022-09-30 00:00:00+00:00 2022-10-03 00:00:00+00:00 3.800744e+10 3.601294e+10 3.851073e+10 Recovered
329 329 group 2022-10-03 00:00:00+00:00 2022-10-04 00:00:00+00:00 2022-10-05 00:00:00+00:00 2022-10-07 00:00:00+00:00 3.851073e+10 3.488746e+10 3.993029e+10 Recovered
330 330 group 2022-10-11 00:00:00+00:00 2022-10-12 00:00:00+00:00 2022-10-12 00:00:00+00:00 2022-10-13 00:00:00+00:00 4.272210e+10 4.262790e+10 4.547761e+10 Recovered
331 331 group 2022-10-14 00:00:00+00:00 2022-10-17 00:00:00+00:00 2022-10-18 00:00:00+00:00 2022-10-18 00:00:00+00:00 4.953220e+10 4.353879e+10 4.353879e+10 Active

332 rows × 10 columns

As another example of how it's possible to play with the data, let's create a dataframe that sorts the percentage drawdowns from biggest to smallest and shows the drawdown start and recovery date.

In [ ]:
drawdown_df["Drawdown PCT"] = ((drawdown_df["Peak Value"]-drawdown_df["Valley Value"])/drawdown_df["Peak Value"])*100
biggest_drawdowns = drawdown_df[["Peak Index", "Valley Index", "Drawdown PCT"]].sort_values(by=["Drawdown PCT"], ascending=False)
biggest_drawdowns["Peak Index"] = biggest_drawdowns["Peak Index"].dt.tz_localize(None)
biggest_drawdowns["Valley Index"] = biggest_drawdowns["Valley Index"].dt.tz_localize(None)
biggest_drawdowns
Out[ ]:
Peak Index Valley Index Drawdown PCT
17 2001-04-06 2001-09-21 73.491664
42 2004-01-16 2006-07-14 65.079365
2 2000-01-06 2000-04-14 63.773964
22 2001-12-07 2002-06-11 58.781410
1 1999-04-20 1999-10-28 54.406845
... ... ... ...
165 2016-03-14 2016-03-15 0.063342
300 2021-07-12 2021-07-13 0.053529
29 2003-07-09 2003-07-10 0.033248
207 2017-11-09 2017-11-10 0.029929
284 2020-12-29 2020-12-30 0.016628

332 rows × 3 columns

Analysis per single asset inside the portfolio¶

With group_by=False it's possible analize the contribution to returns, drawdowns and all the other metrics for each single ticker in the portfolio separately from the others, for really in-depth analysis of what's contributing to the performance of the portfolio.

In [ ]:
portfolio_not_grouped = vbt.Portfolio.from_orders(
    
    close=close_prices,
    #bm_close=put_your_benchmark_close_prices_here,
    size=allocations, # Here we pass the allocations we calculated earlier
    size_type="targetpercent",
    group_by=False,  
    cash_sharing=True,
    call_seq="auto",
    freq='D', # needed to calculate annualized returns and some metrics like sharpe, sortino etc in portfolio.stats()
    fees=0, # can specify a transaction fee here
    )

We can get a dataframe with all the stats for each single asset

In [ ]:
single_asset_stats = portfolio_not_grouped.stats(agg_func=None)
single_asset_stats
Out[ ]:
Start End Period Start Value Min Value Max Value End Value Total Return [%] Benchmark Return [%] Total Time Exposure [%] ... Avg Winning Trade [%] Avg Losing Trade [%] Avg Winning Trade Duration Avg Losing Trade Duration Profit Factor Expectancy Sharpe Ratio Calmar Ratio Omega Ratio Sortino Ratio
symbol
TQQQ TQQQ 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 100.0 74.270866 1.045371e+07 9.041877e+06 9.041777e+06 -75.795680 73.227549 ... 12.175537 -8.342310 12 days 00:54:54.915254237 13 days 09:48:10.140845070 2.303589 23184.044678 1.336266 0.822345 1.281332 2.092036
SQQQ SQQQ 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 100.0 35.563083 1.683875e+03 1.480125e+03 1.380125e+03 -99.999973 7.241729 ... 10.014137 -7.289226 2 days 08:39:35.257731958 2 days 10:04:26.666666666 2.682986 8.123802 0.617504 0.176093 1.382720 0.984780
UVXY UVXY 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 100.0 95.815675 1.891803e+04 1.891803e+04 1.881803e+04 -100.000000 3.190412 ... 13.570371 -4.542983 2 days 12:40:45.283018867 2 days 10:54:32.727272727 62.436518 247.605630 1.086683 0.718410 3.314147 3.587407
TLT TLT 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 100.0 91.964316 1.718486e+02 1.427350e+02 4.273496e+01 194.732172 15.749494 ... 1.293369 -1.165640 4 days 10:15:29.032258064 3 days 12:41:32.307692307 1.274152 0.183412 0.384537 0.089135 1.146790 0.568820

4 rows × 29 columns

In [ ]:
# Alternative way to grab the stats for a specific asset, in this case SQQQ
portfolio_not_grouped.stats(group_by=False, column=2)
Out[ ]:
Start                         1999-04-07 00:00:00+00:00
End                           2022-10-18 00:00:00+00:00
Period                               5924 days 00:00:00
Start Value                                       100.0
Min Value                                     95.815675
Max Value                                  18918.027881
End Value                                  18918.027881
Total Return [%]                           18818.027881
Benchmark Return [%]                             -100.0
Total Time Exposure [%]                        3.190412
Max Gross Exposure [%]                            100.0
Max Drawdown [%]                              34.706887
Max Drawdown Duration                 323 days 00:00:00
Total Orders                                        152
Total Fees Paid                                     0.0
Total Trades                                         76
Win Rate [%]                                  69.736842
Best Trade [%]                                65.372054
Worst Trade [%]                              -20.203265
Avg Winning Trade [%]                         13.570371
Avg Losing Trade [%]                          -4.542983
Avg Winning Trade Duration    2 days 12:40:45.283018867
Avg Losing Trade Duration     2 days 10:54:32.727272727
Profit Factor                                 62.436518
Expectancy                                    247.60563
Sharpe Ratio                                   1.086683
Calmar Ratio                                    0.71841
Omega Ratio                                    3.314147
Sortino Ratio                                  3.587407
Name: 2, dtype: object

We can get even more detailed returns stats for each single asset, and then potentially analyzing or plotting them in many different ways.

In [ ]:
# Even more detailed returns stats for each single asset
single_asset_returns = portfolio_not_grouped.returns_stats(agg_func=None)
single_asset_returns
Out[ ]:
Start End Period Total Return [%] Benchmark Return [%] Annualized Return [%] Annualized Volatility [%] Max Drawdown [%] Max Drawdown Duration Sharpe Ratio Calmar Ratio Omega Ratio Sortino Ratio Skew Kurtosis Tail Ratio Common Sense Ratio Value at Risk Alpha Beta
symbol
TQQQ TQQQ 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 9.041777e+06 -75.795680 62.349412 71.354278 75.819009 875 days 1.336266 0.822345 1.281332 2.092036 1.141272 17.109108 1.024298 1.662942 -0.054903 1.098805 0.513265
SQQQ SQQQ 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 1.380125e+03 -99.999973 12.122497 39.486874 68.841582 3217 days 0.617504 0.176093 1.382720 0.984780 1.195679 61.734717 inf inf 0.000000 0.363114 0.156942
UVXY UVXY 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 1.881803e+04 -100.000000 24.933761 34.343159 34.706887 323 days 1.086683 0.718410 3.314147 3.587407 16.572080 401.969725 inf inf 0.000000 0.470850 0.013698
TLT TLT 1999-04-07 00:00:00+00:00 2022-10-18 00:00:00+00:00 5924 days 4.273496e+01 194.732172 1.522337 6.200627 17.079081 1869 days 0.384537 0.089135 1.146790 0.568820 0.613140 27.380370 1.359131 1.379821 -0.002743 0.012374 0.144512
In [ ]:
single_asset_annual_returns = portfolio_not_grouped.get_annual_returns()
single_asset_annual_returns.plot()
# It's also possible to do a yearly bar chart with this data
Out[ ]:
<AxesSubplot: xlabel='Date'>

Remember the huge number of plots we generated earlier? There's the ability to do the same thing for each of the assets in the portfolioto plot a single asset of the portfolio with orders, trade pnl and returns. It's possible to specify additional subplots. Let's try see all the possibilities.

In [ ]:
# We plot all the available plots for column 3 aka TLT
portfolio_not_grouped_plot = portfolio_not_grouped.plot(column=3, subplots="all") # we're plotting all the possible subplots just for reference
portfolio_not_grouped_plot
FigureWidget({
    'data': [{'legendgroup': '0',
              'line': {'color': '#1f77b4'},
              'mo…
In [ ]:
# Delete this this when running the notebook yourself
portfolio_not_grouped_plot.write_html("./html/portfolio_not_grouped.html", default_width=800, default_height=500)
IFrame(src='./html/portfolio_not_grouped.html', width=800, height=500)
Out[ ]:

There is really so much more that can be done, once you get familiar with how things work. Feel free to contact me on discord ( Raekon#7854 ) if you need any help running this demo