Overall Statistics
Total Orders
175
Average Win
1.91%
Average Loss
-1.28%
Compounding Annual Return
10.716%
Drawdown
39.000%
Expectancy
0.562
Start Equity
100000
End Equity
184186.16
Net Profit
84.186%
Sharpe Ratio
0.407
Sortino Ratio
0.377
Probabilistic Sharpe Ratio
6.208%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.50
Alpha
0.004
Beta
0.948
Annual Standard Deviation
0.187
Annual Variance
0.035
Information Ratio
0.003
Tracking Error
0.102
Treynor Ratio
0.08
Total Fees
$473.49
Estimated Strategy Capacity
$16000000.00
Lowest Capacity Asset
BND TRO5ZARLX6JP
Portfolio Turnover
4.38%
Drawdown Recovery
507
# 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)
        
        # key settings
        tickers = ['SPY', 'GLD', 'BND']
        ticker_crypto = "BTCUSD"
        fred_factors = ['VIXCLS', 'T10Y3M', 'DFF']
        self._lookback_years = 4
        tree_depth = 12
        
        self.settings.daily_precise_end_time = False
        
        self._equities = [self.add_equity(ticker).symbol for ticker in tickers]
        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, 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:
            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()
        
        # adjust portfolio holdings
        self.set_holdings([PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items()], True)