| Overall Statistics |
|
Total Orders 42 Average Win 2.69% Average Loss -2.45% Compounding Annual Return 4.365% Drawdown 48.900% Expectancy 0.101 Start Equity 100000.0 End Equity 104377.52 Net Profit 4.378% Sharpe Ratio 0.273 Sortino Ratio 0.255 Probabilistic Sharpe Ratio 19.514% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.10 Alpha -0.095 Beta 1.239 Annual Standard Deviation 0.385 Annual Variance 0.148 Information Ratio -0.307 Tracking Error 0.184 Treynor Ratio 0.085 Total Fees $56.21 Estimated Strategy Capacity $1800000.00 Lowest Capacity Asset BND TRO5ZARLX6JP Portfolio Turnover 4.85% Drawdown Recovery 14 |
# region imports
from AlgorithmImports import *
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import StandardScaler
# endregion
class StockBondRotationAlgorithm(QCAlgorithm):
"""
Machine Learning based dynamic allocation between equities and bonds.
Based on:
https://www.quantconnect.com/research/18049/reimagining-the-60-40-portfolio-in-an-era-of-ai-and-falling-rates/p1
"""
def initialize(self):
# keep in mind data snooping bias:
# always leave some (typically more recent) time period for testing!
#self.set_start_date(2017, 1, 1)
#self.set_end_date(2022, 12, 31)
self.set_start_date(2020, 1, 1)
self.set_end_date(2020, 12, 31)
# key settings
tickers = ['SPY', 'GLD', 'BND']
ticker_crypto = "BTCUSD"
fred_factors = ['VIXCLS', 'T10Y3M', 'DFF']
self._lookback_years = 4
self._max_bitcoin_weight = 0.25
tree_depth = 12
self.settings.daily_precise_end_time = False
self._equities = [self.add_equity(ticker).symbol for ticker in tickers]
self._bitcoin = self.add_crypto(ticker_crypto, market=Market.BITFINEX, leverage=2).symbol
self._factors = [self.add_data(Fred, ticker, Resolution.DAILY).symbol for ticker in fred_factors]
self._model = DecisionTreeRegressor(max_depth=tree_depth, random_state=1)
self._scaler = StandardScaler()
self.schedule.on(self.date_rules.month_start(self._equities[0]), self.time_rules.after_market_open(self._equities[0], 1), self.rebalance)
def rebalance(self):
# economic factors history
factors = self.history(self._factors, timedelta(self._lookback_years*365), Resolution.DAILY)['value'].unstack(0).dropna()
# monthly returns history
label = self.history(self._equities + [self._bitcoin], timedelta(self._lookback_years*365), Resolution.DAILY, data_normalization_mode=DataNormalizationMode.TOTAL_RETURN)['close'].unstack(0).dropna().pct_change(21).shift(-21).dropna()
prediction_by_symbol = pd.Series()
for symbol in self._equities + [self._bitcoin]:
asset_labels = label[symbol].dropna()
idx = factors.index.intersection(asset_labels.index)
self._model.fit(self._scaler.fit_transform(factors.loc[idx]), asset_labels.loc[idx])
prediction = self._model.predict(self._scaler.transform([factors.iloc[-1]]))[0]
if prediction > 0:
prediction_by_symbol.loc[symbol] = prediction
weight_by_symbol = 1.5 * prediction_by_symbol / prediction_by_symbol.sum()
# respect the limit of cryptocurrency exposure
if self._bitcoin in weight_by_symbol and weight_by_symbol.loc[self._bitcoin] > self._max_bitcoin_weight:
weight_by_symbol.loc[self._bitcoin] = self._max_bitcoin_weight
if len(weight_by_symbol) > 1:
equities = [symbol for symbol in self._equities if symbol in weight_by_symbol]
weight_by_symbol.loc[equities] = 1.5 * weight_by_symbol.loc[equities] / weight_by_symbol.loc[equities].sum()
# adjust portfolio holdings
self.set_holdings([PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items()], True)