| Overall Statistics |
|
Total Orders 368 Average Win 1.62% Average Loss -0.92% Compounding Annual Return 32.163% Drawdown 14.800% Expectancy 0.861 Start Equity 100000 End Equity 402817.60 Net Profit 302.818% Sharpe Ratio 1.153 Sortino Ratio 1.36 Probabilistic Sharpe Ratio 77.082% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 1.75 Alpha 0.16 Beta 0.466 Annual Standard Deviation 0.161 Annual Variance 0.026 Information Ratio 0.786 Tracking Error 0.165 Treynor Ratio 0.399 Total Fees $534.77 Estimated Strategy Capacity $180000000.00 Lowest Capacity Asset SHY SGNKIKYGE9NP Portfolio Turnover 3.05% Drawdown Recovery 489 |
# region imports
from AlgorithmImports import *
# endregion
class VaaGoblinOmega(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5 * 365))
self.set_cash(100000)
growth_tickers = ["NVDA", "AVGO", "MSFT", "META", "LLY", "COST", "QQQ", "XLK", "SMH", "SPY"]
canary_tickers = ["SPY", "QQQ", "IWM", "HYG", "EFA"]
high_beta_tickers = ["NVDA", "AVGO", "META", "QQQ", "XLK", "SMH"]
tickers = ["NVDA", "AVGO", "MSFT", "META", "LLY", "COST", "QQQ", "XLK", "SMH", "SPY", "IWM", "HYG", "EFA", "GLD", "SHY", "IEF", "XLV", "XLP", "XLU"]
self._securities = []
self._growth_securities = []
self._canary_securities = []
self._security_by_ticker = {}
for ticker in tickers:
security = self.add_equity(ticker, Resolution.DAILY)
security.sma_50 = self.sma(security, 50)
security.sma_100 = self.sma(security, 100)
security.sma_200 = self.sma(security, 200)
security.rocp_21 = self.rocp(security, 21)
security.rocp_63 = self.rocp(security, 63)
security.rocp_126 = self.rocp(security, 126)
security.rocp_252 = self.rocp(security, 252)
security.std_63 = self.std(security, 63)
security.is_high_beta = ticker in high_beta_tickers
self._securities.append(security)
self._security_by_ticker[ticker] = security
if ticker in growth_tickers:
self._growth_securities.append(security)
if ticker in canary_tickers:
self._canary_securities.append(security)
self._spy = self._security_by_ticker["SPY"]
self._qqq = self._security_by_ticker["QQQ"]
self.set_benchmark(self._spy)
self.set_warm_up(300, Resolution.DAILY)
self.schedule.on(self.date_rules.month_start(self._spy, 5), self.time_rules.at(8, 0), self._rebalance)
def on_warmup_finished(self):
self._rebalance()
def _rebalance(self):
if self.is_warming_up:
return
# Skip until every indicator is ready and every security has a valid price.
for security in self._securities:
if not (security.sma_50.is_ready and security.sma_100.is_ready and security.sma_200.is_ready and security.rocp_21.is_ready and security.rocp_63.is_ready and security.rocp_126.is_ready and security.rocp_252.is_ready and security.std_63.is_ready):
return
if not security.has_data or security.price <= 0:
return
# Count canary assets that are both rising and above their long-term trend.
risk_score = sum(1 for security in self._canary_securities if self._vaa_score(security) > 0 and security.price > security.sma_200.current.value)
# Pick the allocation profile from the stress check first, then the canary risk score.
if (self._spy.rocp_21.current.value < -0.045 or self._qqq.rocp_21.current.value < -0.065) and self._spy.price < self._spy.sma_50.current.value and self._qqq.price < self._qqq.sma_50.current.value:
weights = self._risk_off_weights()
elif risk_score >= 4:
weights = self._strong_risk_on_weights()
elif risk_score >= 3:
weights = self._weak_risk_on_weights()
else:
weights = self._risk_off_weights()
targets = [PortfolioTarget(security, weights.get(security, 0)) for security in self._securities]
self.set_holdings(targets, liquidate_existing_holdings=True)
def _strong_risk_on_weights(self):
selected = self._select_top_growth_securities(4, 3)
weights = {}
for security, weight in zip(selected, [0.36, 0.25, 0.17, 0.10]):
self._add_weight(weights, security, weight)
self._add_weight(weights, self._security_by_ticker["GLD"], 0.08)
self._add_weight(weights, self._security_by_ticker["SHY"], 0.04)
return self._apply_cash_buffer(weights)
def _weak_risk_on_weights(self):
selected = self._select_top_growth_securities(2, 2)
weights = {}
if len(selected) >= 1:
self._add_weight(weights, selected[0], 0.25)
if len(selected) >= 2:
self._add_weight(weights, selected[1], 0.15)
self._add_weight(weights, self._security_by_ticker["GLD"], 0.30)
self._add_weight(weights, self._security_by_ticker["SHY"], 0.20)
self._add_weight(weights, self._security_by_ticker["XLV"], 0.10)
return self._apply_cash_buffer(weights)
def _risk_off_weights(self):
return self._apply_cash_buffer({self._security_by_ticker["SHY"]: 0.50, self._security_by_ticker["GLD"]: 0.38, self._security_by_ticker["IEF"]: 0.12})
def _select_top_growth_securities(self, count, max_high_beta):
# Score each growth name by VaA momentum plus a trend bonus, normalized by volatility.
score_by_security = {}
for security in self._growth_securities:
volatility = security.std_63.current.value
if security.price <= 0 or volatility <= 0:
score_by_security[security] = -100
continue
trend_bonus = 0
if security.price > security.sma_50.current.value:
trend_bonus += 0.03
if security.price > security.sma_100.current.value:
trend_bonus += 0.04
if security.price > security.sma_200.current.value:
trend_bonus += 0.06
score_by_security[security] = (self._vaa_score(security) + trend_bonus) / max(0.03, volatility / security.price)
# Take the highest scorers while capping how many high-beta names can be chosen.
selected = []
high_beta_count = 0
for security in sorted(score_by_security, key=lambda s: score_by_security[s], reverse=True):
if score_by_security[security] <= 0:
continue
if security.is_high_beta and high_beta_count >= max_high_beta:
continue
selected.append(security)
if security.is_high_beta:
high_beta_count += 1
if len(selected) == count:
break
if len(selected) == 0:
selected.append(self._spy)
return selected
def _vaa_score(self, security):
return 12 * security.rocp_21.current.value + 4 * security.rocp_63.current.value + 2 * security.rocp_126.current.value + security.rocp_252.current.value
def _add_weight(self, weights, security, weight):
weights[security] = weights.get(security, 0) + weight
def _apply_cash_buffer(self, weights):
return {security: weight * (1 - 0.02) for security, weight in weights.items()}