| Overall Statistics |
|
Total Orders 197 Average Win 5.04% Average Loss -1.30% Compounding Annual Return 93.902% Drawdown 25.500% Expectancy 2.725 Start Equity 100000 End Equity 2734362.34 Net Profit 2634.362% Sharpe Ratio 2.05 Sortino Ratio 2.418 Probabilistic Sharpe Ratio 97.479% Loss Rate 24% Win Rate 76% Profit-Loss Ratio 3.89 Alpha 0.567 Beta 0.729 Annual Standard Deviation 0.296 Annual Variance 0.088 Information Ratio 1.968 Tracking Error 0.28 Treynor Ratio 0.834 Total Fees $1141.22 Estimated Strategy Capacity $640000000.00 Lowest Capacity Asset IGW S6BDJ8ONH2ZP Portfolio Turnover 2.37% Drawdown Recovery 212 |
# Big Tech + AI hardware throttle rotation strategy.
# Version 20: VDE in defensive + inverse-vol weighting for Sharpe.
# region imports
from AlgorithmImports import *
# endregion
class BigTechThrottleRotation(QCAlgorithm):
def initialize(self):
self.set_cash(100000)
self.set_start_date(self.end_date - timedelta(5 * 365))
self._tech_tickers = ["FNGS", "QQQ", "IYW", "SMH", "TSM", "STX", "EWY", "VDE", "NVDA", "SOXX", "XLK", "VUG", "IWF", "PLTR", "WDC"]
self._defensive_tickers = ["BIL", "SHY", "IEF", "GLD", "TLT", "XLE", "XLV", "VDE", "XON"]
# Deduplicate while preserving order, since VDE appears in both sleeves.
self._all_tickers = []
for ticker in self._tech_tickers + self._defensive_tickers:
if ticker not in self._all_tickers:
self._all_tickers.append(ticker)
self._symbol_by_ticker = {ticker: self.add_equity(ticker, Resolution.DAILY).symbol for ticker in self._all_tickers}
self._market = self._symbol_by_ticker["QQQ"]
self._risk_check = self._symbol_by_ticker["FNGS"]
self._lookback_days = 252
self._target_exposure = 1.0
self._last_target_by_ticker = {ticker: 0 for ticker in self._all_tickers}
self.set_warm_up(self._lookback_days + 5, Resolution.DAILY)
self.schedule.on(self.date_rules.month_start(self._market), self.time_rules.at(8, 0), self._rebalance)
def on_warmup_finished(self):
self._rebalance()
def _rebalance(self):
if self.is_warming_up:
return
history = self.history(list(self._symbol_by_ticker.values()), self._lookback_days, Resolution.DAILY)
if history.empty:
bil_only = self._zero_weights()
bil_only["BIL"] = self._target_exposure
self._apply_targets(bil_only)
return
regime = self._market_regime(history)
tech_scores = self._score_candidates(history, self._tech_tickers)
defensive_scores = self._score_candidates(history, self._defensive_tickers)
vol_scale = self._volatility_scale(history)
if regime == "bull":
target_weights = self._bull_market_weights(tech_scores, vol_scale)
elif regime == "neutral":
target_weights = self._neutral_market_weights(tech_scores, defensive_scores, vol_scale)
else:
target_weights = self._bear_market_weights(history, defensive_scores)
# Skip the rebalance when no single target moved more than the minimum change threshold.
if max(abs(target_weights.get(ticker, 0) - self._last_target_by_ticker[ticker]) for ticker in self._all_tickers) < 0.10:
return
self._apply_targets(target_weights)
def _market_regime(self, history):
qqq_close = self._get_close_series(history, self._market)
if qqq_close is None or qqq_close.size < 200:
return "neutral"
fngs_close = self._get_close_series(history, self._risk_check)
qqq_now = qqq_close.iloc[-1]
qqq_sma_200 = qqq_close.tail(200).mean()
if qqq_now < qqq_sma_200:
return "bear"
if fngs_close is not None and fngs_close.size >= 100 and qqq_now < qqq_close.tail(100).mean() and fngs_close.iloc[-1] < fngs_close.tail(100).mean():
return "bear"
if qqq_now < qqq_close.tail(50).mean() and qqq_close.size >= 21 and qqq_now / qqq_close.iloc[-21] - 1 < -0.08:
return "bear"
if qqq_now > qqq_sma_200 * 1.03:
return "bull"
return "neutral"
def _bull_market_weights(self, tech_scores, vol_scale):
# Bull: score-weight the three strongest tech names, supplementing with QQQ if needed.
strong_tech = {ticker: score for ticker, score in tech_scores.items() if score > 0.10}
if len(strong_tech) >= 3:
selected = sorted(strong_tech, key=lambda t: strong_tech[t])[-3:]
elif strong_tech:
selected = list(strong_tech.keys())
if "QQQ" not in selected:
selected.append("QQQ")
else:
selected = ["QQQ"]
target_weights = self._zero_weights()
for ticker, weight in self._score_weights(tech_scores, selected, self._target_exposure * vol_scale).items():
target_weights[ticker] = weight
return target_weights
def _neutral_market_weights(self, tech_scores, defensive_scores, vol_scale):
# Neutral: split exposure evenly between two tech names and three defensive names.
strong_tech = {ticker: score for ticker, score in tech_scores.items() if score > 0.10}
if len(strong_tech) >= 2:
selected_tech = sorted(strong_tech, key=lambda t: strong_tech[t])[-2:]
elif strong_tech:
selected_tech = list(strong_tech.keys())
if "QQQ" not in selected_tech and len(selected_tech) < 2:
selected_tech.append("QQQ")
else:
selected_tech = ["QQQ"]
selected_defensive = self._select_top_tickers(defensive_scores, 3)
if not selected_defensive:
selected_defensive = ["BIL"]
target_weights = self._zero_weights()
target_weights = self._merge_weights(target_weights, self._score_weights(tech_scores, selected_tech, self._target_exposure * 0.50 * vol_scale))
target_weights = self._merge_weights(target_weights, self._score_weights(defensive_scores, selected_defensive, self._target_exposure * 0.50))
return target_weights
def _bear_market_weights(self, history, defensive_scores):
# Bear: inverse-vol weight the three strongest defensive names for stability.
selected_defensive = self._select_top_tickers(defensive_scores, 3)
if not selected_defensive:
selected_defensive = ["BIL"]
target_weights = self._zero_weights()
for ticker, weight in self._inv_vol_weights(history, selected_defensive, self._target_exposure).items():
target_weights[ticker] = weight
return target_weights
def _score_weights(self, scores, tickers, total_weight):
valid = {ticker: max(scores.get(ticker, 0), 0) for ticker in tickers}
total_score = sum(valid.values())
if total_score <= 0:
return self._equal_weights(tickers, total_weight)
return {ticker: total_weight * valid[ticker] / total_score for ticker in valid}
def _inv_vol_weights(self, history, tickers, total_weight):
vols = {}
for ticker in tickers:
close = self._get_close_series(history, self._symbol_by_ticker[ticker])
if close is None or close.size < 21:
continue
vol = close.pct_change().dropna().tail(63).std()
if vol and vol > 0:
vols[ticker] = vol
if not vols:
return self._equal_weights(tickers, total_weight)
inv_vols = {ticker: 1.0 / vol for ticker, vol in vols.items()}
total_inv = sum(inv_vols.values())
return {ticker: total_weight * inv_vols[ticker] / total_inv for ticker in inv_vols}
def _volatility_scale(self, history):
qqq_close = self._get_close_series(history, self._market)
if qqq_close is None or len(qqq_close) < 252:
return 1.0
returns = qqq_close.pct_change().dropna()
current_vol = returns.tail(21).std() * (252 ** 0.5)
avg_vol = returns.tail(252).std() * (252 ** 0.5)
if avg_vol <= 0:
return 1.0
return min(1.0, max(0.5, avg_vol / current_vol))
def _score_candidates(self, history, candidate_tickers):
# Score by vol-adjusted blended momentum, a trend term, and a recent drawdown term.
score_by_ticker = {}
for ticker in candidate_tickers:
close = self._get_close_series(history, self._symbol_by_ticker[ticker])
if close is None or close.size < 126:
continue
price_now = close.iloc[-1]
if price_now <= 0:
continue
one_month = price_now / close.iloc[-21] - 1 if close.size >= 21 else 0
three_month = price_now / close.iloc[-63] - 1 if close.size >= 63 else 0
six_month = price_now / close.iloc[-126] - 1
momentum = one_month * 0.3 + three_month * 0.4 + six_month * 0.3
volatility = close.pct_change().dropna().tail(126).std() * (252 ** 0.5)
if volatility <= 0:
continue
trend = price_now / close.tail(100).mean() - 1
drawdown = price_now / close.tail(63).max() - 1
score_by_ticker[ticker] = momentum / volatility + trend + drawdown
return score_by_ticker
def _equal_weights(self, tickers, total_weight):
return {ticker: total_weight / len(tickers) for ticker in tickers}
def _merge_weights(self, base_weights, new_weights):
for ticker in new_weights:
base_weights[ticker] = base_weights.get(ticker, 0) + new_weights[ticker]
return base_weights
def _zero_weights(self):
return {ticker: 0 for ticker in self._all_tickers}
def _apply_targets(self, target_weights):
targets = []
for ticker in self._all_tickers:
weight = target_weights.get(ticker, 0)
targets.append(PortfolioTarget(self._symbol_by_ticker[ticker], weight))
self._last_target_by_ticker[ticker] = weight
self.set_holdings(targets)
def _select_top_tickers(self, score_by_ticker, count):
return sorted(score_by_ticker, key=score_by_ticker.get)[-count:]
def _get_close_series(self, history, symbol):
if symbol not in history.index.get_level_values(0):
return None
return history.loc[symbol]["close"].dropna()