Overall Statistics
Total Orders
111
Average Win
0.98%
Average Loss
-0.93%
Compounding Annual Return
15.351%
Drawdown
6.500%
Expectancy
0.747
Start Equity
100000
End Equity
144736.50
Net Profit
44.737%
Sharpe Ratio
0.71
Sortino Ratio
0.789
Probabilistic Sharpe Ratio
78.832%
Loss Rate
15%
Win Rate
85%
Profit-Loss Ratio
1.04
Alpha
0
Beta
0
Annual Standard Deviation
0.07
Annual Variance
0.005
Information Ratio
1.496
Tracking Error
0.07
Treynor Ratio
0
Total Fees
$194.00
Estimated Strategy Capacity
$820000.00
Lowest Capacity Asset
GLD T3SKPOF94JFP
Portfolio Turnover
2.33%
Drawdown Recovery
180
# region imports
from AlgorithmImports import *
import numpy as np
import pandas as pd
# endregion

class MacroFlowAdaptiveVolatility(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2023, 6, 1) 
        self.SetEndDate(2026, 1, 1)
        self.SetCash(100000)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        # 1. Asset Universe
        self.symbols = {
            "xly": self.AddEquity("XLY", Resolution.Daily).Symbol,
            "xlp": self.AddEquity("XLP", Resolution.Daily).Symbol,
            "hyg": self.AddEquity("HYG", Resolution.Daily).Symbol,
            "tlt": self.AddEquity("TLT", Resolution.Daily).Symbol,
            "cop": self.AddEquity("COPX", Resolution.Daily).Symbol,
            "gld": self.AddEquity("GLD", Resolution.Daily).Symbol,
            "tech": self.AddEquity("XLK", Resolution.Daily).Symbol,
            "energy": self.AddEquity("XLE", Resolution.Daily).Symbol,
            "market": self.AddEquity("RSP", Resolution.Daily).Symbol,
            "cash": self.AddEquity("BIL", Resolution.Daily).Symbol
        }
        self.vix = self.AddData(CBOE, "VIX").Symbol
        
        # 2. Risk & Quantitative Parameters
        self.target_vol = 0.10        
        self.asset_highs = {}         
        self.stopped_out_this_month = False 
        self.vix_sma = self.SMA(self.vix, 5, Resolution.Daily)
        self.lookback = 60
        self.SetWarmUp(self.lookback + 20)

        # 3. Custom Risk Dashboard
        riskPlot = Chart('Strategy Risk Dashboard')
        riskPlot.AddSeries(Series('Realized Volatility', SeriesType.Line, '%', Color.Orange))
        riskPlot.AddSeries(Series('Target Volatility', SeriesType.Line, '%', Color.Blue))
        riskPlot.AddSeries(Series('VIX Level', SeriesType.Line, '', Color.Gray))
        self.AddChart(riskPlot)

        self.Schedule.On(self.DateRules.MonthStart(self.symbols["tech"]), 
                         self.TimeRules.AfterMarketOpen(self.symbols["tech"], 30), 
                         self.Rebalance)
        
        self.Schedule.On(self.DateRules.EveryDay(self.symbols["tech"]),
                         self.TimeRules.BeforeMarketClose(self.symbols["tech"], 15),
                         self.RiskManagement)

    def Rebalance(self):
        if self.IsWarmingUp: return
        self.stopped_out_this_month = False 
        
        # FIX: Ensure ALL symbols are requested in history
        h = self.History(list(self.symbols.values()), self.lookback + 10, Resolution.Daily)
        if h.empty: return
        
        # Unstack for easier access
        prices = h.unstack(level=0)['close']

        # I. Triple-Gate Validation
        consumer_ok = self.GetSmoothedROC(self.symbols["xly"], h) > self.GetSmoothedROC(self.symbols["xlp"], h)
        credit_ok = self.GetSmoothedROC(self.symbols["hyg"], h) > self.GetSmoothedROC(self.symbols["tlt"], h)

        # II. Volatility Analytics
        # Extract returns for growth assets
        growth_rets = prices[[self.symbols["tech"], self.symbols["energy"]]].pct_change().dropna()
        vol_t = growth_rets[self.symbols["tech"]].std() * np.sqrt(252)
        vol_e = growth_rets[self.symbols["energy"]].std() * np.sqrt(252)
        corr = growth_rets[self.symbols["tech"]].corr(growth_rets[self.symbols["energy"]])

        if consumer_ok and credit_ok:
            # III. Growth Allocation (Inverse Volatility / Risk Parity)
            w_t = (1/vol_t) / ((1/vol_t) + (1/vol_e))
            w_e = 1 - w_t
            est_port_vol = np.sqrt((w_t**2 * vol_t**2) + (w_e**2 * vol_e**2) + (2*w_t*w_e*vol_t*vol_e*corr))
            
            exposure = np.clip(self.target_vol / est_port_vol, 0, 1.2)
            
            self.SetHoldings(self.symbols["tech"], w_t * exposure)
            self.SetHoldings(self.symbols["energy"], w_e * exposure)
            self.SetHoldings(self.symbols["market"], 0.10)
            self.Liquidate(self.symbols["gld"])
            
            self.PlotRisk(est_port_vol)
        else:
            # IV. Defensive State (Gold + Cash)
            # FIX: Pull Gold volatility safely from the prices dataframe
            gold_rets = prices[self.symbols["gld"]].pct_change().dropna()
            vol_g = gold_rets.std() * np.sqrt(252)
            
            self.SetHoldings(self.symbols["gld"], 0.40)
            self.SetHoldings(self.symbols["cash"], 0.60)
            self.Liquidate(self.symbols["tech"])
            self.Liquidate(self.symbols["energy"])
            
            self.PlotRisk(vol_g)

        # Update Highs
        for symbol in self.Portfolio.Keys:
            if self.Portfolio[symbol].Invested:
                self.asset_highs[symbol] = self.Securities[symbol].Price

    def RiskManagement(self):
        vix_price = self.Securities[self.vix].Price
        dynamic_stop = 0.12 if vix_price < 15 else (0.04 if vix_price > 25 else 0.08)

        for symbol in list(self.asset_highs.keys()):
            if not self.Portfolio[symbol].Invested: continue
            
            price = self.Securities[symbol].Price
            if price > self.asset_highs[symbol]: self.asset_highs[symbol] = price
            
            if price < self.asset_highs[symbol] * (1 - dynamic_stop):
                self.Log(f"STOP TRIGGERED: {symbol}. Moving to Cash.")
                self.Liquidate(symbol)
                self.stopped_out_this_month = True
                self.SetHoldings(self.symbols["cash"], 0.95)

        if self.stopped_out_this_month and vix_price < self.vix_sma.Current.Value * 0.90:
            self.Log("RE-ENTRY: Fear collapsed. Restarting Engine.")
            self.Rebalance()

    def GetSmoothedROC(self, symbol, history):
        prices = history.loc[symbol]['close']
        roc = (prices / prices.shift(21)) - 1
        return roc.rolling(5).mean().iloc[-1]

    def PlotRisk(self, current_vol):
        self.Plot('Strategy Risk Dashboard', 'Realized Volatility', current_vol * 100)
        self.Plot('Strategy Risk Dashboard', 'Target Volatility', self.target_vol * 100)
        self.Plot('Strategy Risk Dashboard', 'VIX Level', self.Securities[self.vix].Price)