| 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)