Overall Statistics
Total Orders
500
Average Win
0.23%
Average Loss
-0.20%
Compounding Annual Return
0.735%
Drawdown
6.500%
Expectancy
0.037
Start Equity
100000
End Equity
101849.75
Net Profit
1.850%
Sharpe Ratio
-1.475
Sortino Ratio
-1.679
Probabilistic Sharpe Ratio
5.779%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.14
Alpha
-0.055
Beta
0.062
Annual Standard Deviation
0.033
Annual Variance
0.001
Information Ratio
-1.226
Tracking Error
0.129
Treynor Ratio
-0.777
Total Fees
$494.33
Estimated Strategy Capacity
$25000000.00
Lowest Capacity Asset
NB R735QTJ8XC9X
Portfolio Turnover
1.52%
Drawdown Recovery
293
from AlgorithmImports import *

class AdaptiveMomentumVolatility(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_cash(100000)
        
        # universe selection
        self.add_universe(self.coarse_selection_function, self.fine_selection_function)
        
        self.data = {}
        self.num_coarse = 50   # take 50 liquid stocks
        self.num_fine = 20     # filter down to 20
        self.max_positions = 10
        
        # ATR stop-loss
        self.atr_period = 14
        self.stop_loss_atr = 3  

        # VIX proxy (VIXY ETF since true VIX not tradable)
        self.vix = self.add_equity("VIXY", Resolution.DAILY).symbol

        # rebalance monthly
        self.schedule.on(
            self.date_rules.month_start("SPY"),
            self.time_rules.after_market_open("SPY"),
            self.rebalance
        )

    def coarse_selection_function(self, coarse):
        # top 50 by dollar volume
        selected = [x.symbol for x in sorted(coarse, key=lambda c: c.dollar_volume, reverse=True) if x.has_fundamental_data]
        return selected[:self.num_coarse]

    def fine_selection_function(self, fine):
        # pick top 20 stocks with positive PE
        sorted_by_pe = sorted(
            fine,
            key=lambda f: f.valuation_ratios.pe_ratio if f.valuation_ratios.pe_ratio > 0 else 9999
        )
        return [x.symbol for x in sorted_by_pe[:self.num_fine]]

    def on_securities_changed(self, changes):
        for security in changes.added_securities:
            # attach ATR indicator for each stock
            atr_indicator = self.atr(security.symbol, self.atr_period, MovingAverageType.SIMPLE, Resolution.DAILY)
            self.data[security.symbol] = atr_indicator

    def get_vix_adjustment(self):
        if not self.securities[self.vix].has_data:
            return 1.0
        
        vix_price = self.securities[self.vix].price
        if vix_price < 20:
            return 1.0   # normal risk
        elif vix_price < 30:
            return 0.5   # cut size in half
        else:
            return 0.25  # very defensive

    def rebalance(self):
        # liquidate everything first
        self.liquidate()
        
        selected = [s for s in self.data if self.securities.contains_key(s) and self.securities[s].has_data]
        
        # momentum filter (20d > 50d)
        final = []
        for symbol in selected:
            hist = self.history(symbol, 60, Resolution.DAILY)
            if len(hist) < 60: 
                continue
            closes = hist['close'].values
            ma20 = closes[-20:].mean()
            ma50 = closes[-50:].mean()
            if ma20 > ma50:
                final.append(symbol)
        
        # take up to 10 stocks
        final = final[:self.max_positions]
        if not final: 
            return
        
        # adjust weights by volatility regime
        vix_adj = self.get_vix_adjustment()
        weight = (1.0 / len(final)) * vix_adj
        
        for symbol in final:
            self.set_holdings(symbol, weight)

    def on_data(self, data):
        # ATR stop-loss check
        for kvp in self.portfolio:
            holding = kvp.value
            symbol = holding.symbol
            if not holding.invested: 
                continue
            if symbol not in self.data: 
                continue
            
            atr = self.data[symbol].current.value
            if atr == 0: 
                continue
            stop_price = holding.average_price - self.stop_loss_atr * atr
            if self.securities[symbol].price < stop_price:
                self.liquidate(symbol)