Overall Statistics
Total Trades
402
Average Win
0.71%
Average Loss
-0.66%
Compounding Annual Return
9.604%
Drawdown
21.000%
Expectancy
0.640
Net Profit
150.284%
Sharpe Ratio
0.872
Probabilistic Sharpe Ratio
28.369%
Loss Rate
21%
Win Rate
79%
Profit-Loss Ratio
1.06
Alpha
0.041
Beta
0.276
Annual Standard Deviation
0.077
Annual Variance
0.006
Information Ratio
-0.228
Tracking Error
0.124
Treynor Ratio
0.245
Total Fees
$966.31
Estimated Strategy Capacity
$1700000.00
Lowest Capacity Asset
BIL TT1EBZ21QWKL
Portfolio Turnover
1.92%
from AlgorithmImports import *
import numpy as np


class MyAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2012, 12, 31)  
        self.SetEndDate(2022, 12, 31)  
        self.SetCash(100000)
        self.canary = self.AddEquity("TIP", Resolution.Minute).Symbol
        self.cash = self.AddEquity("BIL", Resolution.Minute).Symbol
        self.ief = self.AddEquity("IEF", Resolution.Minute).Symbol
        self.offensive_tickers = ['SPY', 'IWM', 'VWO', 'VEA', 'VNQ', 'DBC', 'IEF', 'TLT']
        self.offensive_assets = [self.AddEquity(symbol, Resolution.Minute).Symbol for symbol in self.offensive_tickers]

        self.month = -1
        self.SetWarmUp(252)
        self.Schedule.On(self.DateRules.MonthEnd("SPY"), self.TimeRules.AfterMarketOpen("SPY", 1), self.Rebalance)

    def momentum(self, asset):
        # Get historical price data for the asset
        history = self.History(asset, 365, Resolution.Daily)
        # Check if history is empty
        if history.empty or len(history) < 252:
            return 0.0
        prices = history['close']
        
        # Calculate total returns for each period
        returns_1m = (prices[-1] / prices[-21]) - 1
        returns_3m = (prices[-1] / prices[-63]) - 1
        returns_6m = (prices[-1] / prices[-126]) - 1
        returns_12m = (prices[-1] / prices[-252]) - 1
        
        # Calculate average momentum
        momentum = np.mean([returns_1m, returns_3m, returns_6m, returns_12m])
        
        return momentum
    
    def Rebalance(self):
        canary_momentum = self.momentum(self.canary)
        offensive_assets = self.offensive_assets
        momentums = [self.momentum(asset) for asset in offensive_assets]
        cash_momentum = self.momentum(self.cash)
        ief_momentum = self.momentum(self.ief)

        sorted_assets = [x for _, x in sorted(zip(momentums, offensive_assets), reverse=True)]
        
        # self.Debug(f"{self.Time.strftime('%Y-%m-%d')}");
        # self.Debug(f"TIPS momentum {canary_momentum}");

        if canary_momentum < 0:
            # self.Debug(f"Going DEFENSIVE on {self.Time.strftime('%Y-%m-%d')}");
            if ief_momentum > cash_momentum:
                self.SetHoldings(self.ief, 1, True)
            else:
                self.SetHoldings(self.cash, 1, True)                  
        else:
            top_four_assets = sorted_assets[:4]
            rest_assets = sorted_assets[4:]

            # there's probably a more efficient way to do this to avoid more transactions than needed
            for asset in rest_assets:
                self.Liquidate(asset)
            self.Liquidate(self.ief)
            self.Liquidate(self.cash)

            count_neg_momentum = 0
            for asset in top_four_assets:
                momentum = self.momentum(asset)
                self.Debug(f"{asset} momentum {momentum} {self.Time.strftime('%Y-%m-%d')}");
                if momentum >= 0:
                    self.SetHoldings(asset, 0.25)
                else:
                    count_neg_momentum = count_neg_momentum + 1
                    if ief_momentum > cash_momentum:
                        self.SetHoldings(self.ief, 0.25 * count_neg_momentum)
                    else:
                        self.SetHoldings(self.cash, 0.25 * count_neg_momentum)