Overall Statistics
Total Trades
41
Average Win
3.21%
Average Loss
-1.61%
Compounding Annual Return
22.679%
Drawdown
10.100%
Expectancy
0.651
Net Profit
20.612%
Sharpe Ratio
1.185
Probabilistic Sharpe Ratio
54.717%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
2.00
Alpha
0.167
Beta
0.057
Annual Standard Deviation
0.137
Annual Variance
0.019
Information Ratio
1.034
Tracking Error
0.237
Treynor Ratio
2.866
Total Fees
$268.02
Estimated Strategy Capacity
$150000.00
Lowest Capacity Asset
USDU VMIMJSS4X2SL
Portfolio Turnover
4.87%
from AlgorithmImports import *
from AlgorithmImports import *
from datetime import timedelta

class MyAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2022, 1, 1)
        self.SetEndDate(2022, 12, 1)
        self.SetCash(100000)

        self.signals = ["VIXM", "BND", "BIL", "TLT", "SPY"]
        self.risk_on_no_inflation = ["TECL", "TQQQ", "UPRO", "TMF"]
        self.risk_on_falling_rates = ["UPRO", "TMF"]
        self.risk_off_market_crash = ["SHY"]
        self.risk_off_rising_rates = ["USDU", "QID", "TBF"]
        self.refined_risk_off = ["IEI", "GLD", "TIP", "BSV"]

        for symbol in self.signals + self.risk_on_no_inflation + self.risk_on_falling_rates + self.risk_off_market_crash + self.risk_off_rising_rates + self.refined_risk_off:
            self.AddEquity(symbol)
        
        self.current_condition = None
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen("SPY", 10), self.CheckConditions)

    def OnData(self, data):
        pass

    def CheckConditions(self):
        history = self.History(self.signals + self.risk_on_no_inflation + self.risk_on_falling_rates + self.risk_off_market_crash + self.risk_off_rising_rates + self.refined_risk_off, 60, Resolution.Daily)
        
        if history.empty:
            return
        
        last_prices = history['close'].unstack(level=0).iloc[-1]
        prev_prices = history['close'].unstack(level=0).iloc[-60]
        
        vixm_momentum = self.CalculateMomentum(history['close']['VIXM'], 40)
        
        if vixm_momentum > 0.69:
            new_condition = "RiskOffMarketCrash"
        else:
            bnd_return_60 = self.CalculateReturn(last_prices['BND'], prev_prices['BND'], 60)
            bil_return_60 = self.CalculateReturn(last_prices['BIL'], prev_prices['BIL'], 60)
            
            if bnd_return_60 > bil_return_60:
                new_condition = "RiskOnNormal"
            elif bnd_return_60 <= bil_return_60 and self.CheckTLTCondition(last_prices, prev_prices):
                new_condition = "RiskOffRisingRates"
            else:
                spy_drawdown = self.CalculateMaxDrawdown(history['close']['SPY'], 5)
                
                if spy_drawdown < 0.05:
                    new_condition = "RiskOnFallingRates"
                else:
                    new_condition = "RefinedRiskOff"
        
        if new_condition != self.current_condition:
            self.current_condition = new_condition
            self.Rebalance(self.current_condition, history, last_prices)
    
    def CheckTLTCondition(self, last_prices, prev_prices):
        tlt_return_20 = self.CalculateCumulativeReturn(last_prices['TLT'], prev_prices['TLT'], 20)
        bil_return_20 = self.CalculateCumulativeReturn(last_prices['BIL'], prev_prices['BIL'], 20)
        return tlt_return_20 < bil_return_20
    
    def Rebalance(self, condition, history, last_prices):
        self.Log(f"Rebalancing for condition: {condition}")
        self.Liquidate()
        if condition == "RiskOffMarketCrash":
            self.SetHoldings("SHY", 1)
        elif condition == "RiskOnNormal":
            self.RiskOnNormal(history, last_prices)
        elif condition == "RiskOffRisingRates":
            self.RiskOffRisingRates(history)
        elif condition == "RiskOnFallingRates":
            self.SetHoldings("UPRO", 0.55)
            self.SetHoldings("TMF", 0.45)
        elif condition == "RefinedRiskOff":
            self.RefinedRiskOff()
    
    def RiskOnNormal(self, history, last_prices):
        relative_strength = {asset: self.CalculateMomentum(history['close'][asset], 10) for asset in self.risk_on_no_inflation}
        top_assets = sorted(relative_strength, key=lambda x: relative_strength[x], reverse=True)[:3]
        weight = 0.33
        for asset in top_assets:
            self.SetHoldings(asset, weight)
    
    def RiskOffRisingRates(self, history):
        self.SetHoldings("USDU", 0.5)
        qid_tbf_momentum = {asset: self.CalculateMomentum(history['close'][asset], 20) for asset in self.risk_off_rising_rates}
        top_asset = max(qid_tbf_momentum, key=qid_tbf_momentum.get)
        self.SetHoldings(top_asset, 0.5)
    
    def RefinedRiskOff(self):
        weight = 0.25
        for asset in self.refined_risk_off:
            self.SetHoldings(asset, weight)
    
    def CalculateReturn(self, current_price, previous_price, days):
        return (current_price - previous_price) / previous_price
    
    def CalculateCumulativeReturn(self, current_price, previous_price, days):
        return (current_price / previous_price) - 1
    
    def CalculateMaxDrawdown(self, prices, days):
        max_drawdown = 0
        for i in range(1, len(prices) - days + 1):
            drawdown = (prices[i:i+days].min() - prices[i-1]) / prices[i-1]
            max_drawdown = min(max_drawdown, drawdown)
        return max_drawdown
    
    def CalculateMomentum(self, prices, days):
        return prices.pct_change(periods=days).mean()