| Overall Statistics |
|
Total Orders 234 Average Win 2.57% Average Loss -2.25% Compounding Annual Return 32.634% Drawdown 30.000% Expectancy 0.428 Start Equity 100000 End Equity 410676.39 Net Profit 310.676% Sharpe Ratio 0.958 Sortino Ratio 1.034 Probabilistic Sharpe Ratio 50.884% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 1.14 Alpha 0.168 Beta 0.564 Annual Standard Deviation 0.217 Annual Variance 0.047 Information Ratio 0.647 Tracking Error 0.211 Treynor Ratio 0.368 Total Fees $851.45 Estimated Strategy Capacity $6700000.00 Lowest Capacity Asset REMX UR58SK7J5PPH Portfolio Turnover 2.84% Drawdown Recovery 611 |
# region imports
from AlgorithmImports import *
from arch import arch_model
# endregion
class GarchVolRotation(QCAlgorithm):
def initialize(self):
# self.set_start_date(self.end_date - timedelta(60))
self.set_start_date(2021, 1, 1)
self.set_end_date(2026, 1, 1)
self.settings.seed_initial_prices = True
self.settings.automatic_indicator_warm_up = True
self.set_cash(100_000)
self._lookback = 252
self._horizon = 21
spy = self.add_equity("SPY", Resolution.DAILY)
spy.session.size = self._lookback
spy.momp = self.momp(spy, self._lookback)
self._spy = spy
etf_tickers = [
"XLK", "IYW",
"XLV", "VHT",
"XLF", "VFH",
"ERTH", "GRID", "PRN",
"REMX", "SLVP", "COPX",
"FALN", "INDS", "SRV",
"IXC", "IYE", "IGE",
"GLD", "IAU", "GDXJ",
"SLV", "SIVR", "AGQ",
]
self._securities = []
for ticker in etf_tickers:
security = self.add_equity(ticker, Resolution.DAILY)
security.session.size = self._lookback
security.momp = self.momp(security, self._lookback)
self._securities.append(security)
self._date_rule = self.date_rules.month_start("SPY")
self.set_warm_up(timedelta(365))
def on_warmup_finished(self):
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self._date_rule, time_rule, self._rebalance)
if self.live_mode:
self._rebalance()
else:
self.schedule.on(self.date_rules.today, time_rule, self._rebalance)
def _rebalance(self):
if not self._spy.momp.is_ready:
return
spy_momentum = self._spy.momp.current.value
acceleration_by_security = {}
for security in self._securities:
if not security.momp.is_ready or not security.session.is_ready:
continue
if security.momp.current.value <= spy_momentum:
continue
# Session iterates newest-first; reverse to oldest-first for np.diff.
closes = np.array([x.close for x in security.session])[::-1]
pct_returns = np.diff(closes) / closes[:-1]
if len(pct_returns) < 200:
continue
model = arch_model(pct_returns * 100, vol="Garch", p=1, q=1, dist="t")
res = model.fit(disp="off")
forecast = res.forecast(horizon=self._horizon)
forecast_vol = np.sqrt(forecast.variance.values[-1].mean())
realized_vol = np.std(pct_returns) * np.sqrt(252)
if forecast_vol <= realized_vol:
continue
acceleration_by_security[security] = forecast_vol / realized_vol
if len(acceleration_by_security) < 3:
return
top_securities = [s for s, _ in sorted(acceleration_by_security.items(), key=lambda x: x[1])[:3]]
targets = [PortfolioTarget(s, 1.0 / 3) for s in top_securities]
self.set_holdings(targets, True)