| Overall Statistics |
|
Total Orders 198 Average Win 5.10% Average Loss -1.46% Compounding Annual Return 95.199% Drawdown 25.500% Expectancy 2.425 Start Equity 100000 End Equity 2837392.52 Net Profit 2737.393% Sharpe Ratio 2.141 Sortino Ratio 2.636 Probabilistic Sharpe Ratio 98.628% Loss Rate 24% Win Rate 76% Profit-Loss Ratio 3.49 Alpha 0.564 Beta 0.683 Annual Standard Deviation 0.284 Annual Variance 0.08 Information Ratio 2.01 Tracking Error 0.27 Treynor Ratio 0.889 Total Fees $1150.78 Estimated Strategy Capacity $1300000000.00 Lowest Capacity Asset IGW S6BDJ8ONH2ZP Portfolio Turnover 2.41% Drawdown Recovery 212 |
# Big Tech + AI hardware throttle rotation strategy.
# Version 20: VDE in defensive + inverse-vol weighting for Sharpe.
from AlgorithmImports import *
from datetime import timedelta
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"
]
# Deduplicated list preserving order
seen = set()
self._all_tickers = []
for t in self._tech_tickers + self._defensive_tickers:
if t not in seen:
seen.add(t)
self._all_tickers.append(t)
self._symbols = {}
for ticker in self._all_tickers:
security = self.add_equity(ticker, Resolution.DAILY)
security.set_data_normalization_mode(DataNormalizationMode.ADJUSTED)
self._symbols[ticker] = security.symbol
self._market = self._symbols["QQQ"]
self._risk_check = self._symbols["FNGS"]
self._lookback_days = 252
self._target_exposure = 1.0
self._minimum_rebalance_change = 0.10
self._last_regime = "neutral"
self._cooldown_months = 0
self._last_target_by_ticker = {}
for ticker in self._all_tickers:
self._last_target_by_ticker[ticker] = 0
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 on_data(self, data):
pass
def _rebalance(self):
if self.is_warming_up:
return
history = self.history(
list(self._symbols.values()),
self._lookback_days,
Resolution.DAILY
)
if history.empty:
target_weights = self._single_asset_weights("BIL")
self._apply_targets(target_weights)
return
regime = self._market_regime(history)
tech_scores = self._score_candidates(history, self._tech_tickers)
defensive_scores = self._score_candidates(history, self._defensive_tickers)
# Volatility scaling: reduce exposure in high-vol regimes
vol_scale = self._volatility_scale(history)
if regime == "bull":
target_weights = self._bull_market_weights(
history, tech_scores, vol_scale
)
elif regime == "neutral":
target_weights = self._neutral_market_weights(
history, tech_scores, defensive_scores, vol_scale
)
else:
target_weights = self._bear_market_weights(
history, defensive_scores
)
if self._should_skip_rebalance(target_weights):
return
self._apply_targets(target_weights)
def _market_regime(self, history):
qqq_close = self._get_close_series(history, self._market)
fngs_close = self._get_close_series(history, self._risk_check)
if qqq_close is None or qqq_close.size < 200:
return "neutral"
qqq_now = qqq_close.iloc[-1]
qqq_sma_50 = qqq_close.tail(50).mean()
qqq_sma_100 = qqq_close.tail(100).mean()
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:
fngs_now = fngs_close.iloc[-1]
fngs_sma_100 = fngs_close.tail(100).mean()
if qqq_now < qqq_sma_100 and fngs_now < fngs_sma_100:
return "bear"
if qqq_now < qqq_sma_50 and qqq_close.size >= 21:
one_month = qqq_now / qqq_close.iloc[-21] - 1
if one_month < -0.08:
return "bear"
if qqq_now > qqq_sma_200 * 1.03:
return "bull"
return "neutral"
def _bull_market_weights(self, history, tech_scores, vol_scale):
"""Bull: 3 best tech with strong momentum, score-weighted for max Sharpe."""
target_weights = self._zero_weights()
# Only select tech with positive momentum > 5% annualized
min_momentum = 0.10
strong_tech = {t: s for t, s in tech_scores.items() if s > min_momentum}
if len(strong_tech) >= 3:
# Enough strong candidates, pick top 3
selected_tickers = sorted(strong_tech.keys(), key=lambda t: strong_tech[t], reverse=True)[:3]
elif len(strong_tech) > 0:
# Some strong candidates, supplement with QQQ
selected_tickers = list(strong_tech.keys())
if "QQQ" not in selected_tickers:
selected_tickers.append("QQQ")
else:
# No strong momentum, default to QQQ
selected_tickers = ["QQQ"]
weighted_targets = self._score_weights(
tech_scores, selected_tickers, self._target_exposure * vol_scale
)
for ticker in weighted_targets:
target_weights[ticker] = weighted_targets[ticker]
return target_weights
def _neutral_market_weights(self, history, tech_scores, defensive_scores, vol_scale):
"""Neutral: 2 tech + 3 defensive, score-weighted for better selection."""
target_weights = self._zero_weights()
# Only select tech with positive momentum > 10% in neutral markets
min_momentum = 0.10
strong_tech = {t: s for t, s in tech_scores.items() if s > min_momentum}
if len(strong_tech) >= 2:
selected_tech = sorted(strong_tech.keys(), key=lambda t: strong_tech[t], reverse=True)[:2]
elif len(strong_tech) > 0:
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 len(selected_tech) == 0:
selected_tech = ["QQQ"]
if len(selected_defensive) == 0:
selected_defensive = ["BIL"]
tech_weight = self._target_exposure * 0.50 * vol_scale
defensive_weight = self._target_exposure * 0.50
tech_targets = self._score_weights(tech_scores, selected_tech, tech_weight)
defensive_targets = self._score_weights(
defensive_scores, selected_defensive, defensive_weight
)
target_weights = self._merge_weights(target_weights, tech_targets)
target_weights = self._merge_weights(target_weights, defensive_targets)
return target_weights
def _bear_market_weights(self, history, defensive_scores):
"""Bear: 3 defensive, inverse-vol weighted for stability."""
target_weights = self._zero_weights()
selected_defensive = self._select_top_tickers(defensive_scores, 3)
if len(selected_defensive) == 0:
selected_defensive = ["BIL"]
weighted_targets = self._inv_vol_weights(
history, selected_defensive, self._target_exposure
)
for ticker in weighted_targets:
target_weights[ticker] = weighted_targets[ticker]
return target_weights
def _score_weights(self, scores, tickers, total_weight):
"""Weight proportional to score magnitude for max Sharpe.
Scores are already vol-adjusted (momentum/vol + trend + dd),
so higher-score assets naturally have better risk-return."""
valid = {t: max(scores.get(t, 0), 0) for t in tickers}
total_score = sum(valid.values())
if total_score <= 0:
return self._equal_weights(tickers, total_weight)
return {t: total_weight * valid[t] / total_score for t in valid}
def _inv_vol_weights(self, history, tickers, total_weight):
"""Inverse-vol weighting for defensive positions."""
vols = {}
for ticker in tickers:
symbol = self._symbols[ticker]
close = self._get_close_series(history, symbol)
if close is None or close.size < 21:
continue
returns = close.pct_change().dropna()
vol = returns.tail(63).std()
if vol and vol > 0:
vols[ticker] = vol
if len(vols) == 0:
return self._equal_weights(tickers, total_weight)
inv_vols = {t: 1.0 / v for t, v in vols.items()}
total_inv = sum(inv_vols.values())
return {t: total_weight * inv_vols[t] / total_inv for t in inv_vols}
def _volatility_scale(self, history):
"""Scale factor based on market volatility (0.5 to 1.0).
Reduces exposure when QQQ vol is above historical average."""
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
# Scale down when current vol exceeds average
vol_ratio = current_vol / avg_vol
scale = min(1.0, max(0.5, 1.0 / vol_ratio))
return scale
def _score_candidates(self, history, candidate_tickers):
score_by_ticker = {}
for ticker in candidate_tickers:
symbol = self._symbols[ticker]
close = self._get_close_series(history, symbol)
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
returns = close.pct_change().dropna()
recent_returns = returns.tail(126)
volatility = recent_returns.std() * (252 ** 0.5)
if volatility <= 0:
continue
sma_100 = close.tail(100).mean()
trend = price_now / sma_100 - 1
recent_high = close.tail(63).max()
drawdown = price_now / recent_high - 1
score = momentum / volatility + trend + drawdown
score_by_ticker[ticker] = score
return score_by_ticker
def _equal_weights(self, tickers, total_weight):
weight_per_ticker = total_weight / len(tickers)
return {ticker: weight_per_ticker 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 _single_asset_weights(self, ticker):
target_weights = self._zero_weights()
target_weights[ticker] = self._target_exposure
return target_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:
symbol = self._symbols[ticker]
weight = target_weights.get(ticker, 0)
targets.append(PortfolioTarget(symbol, weight))
self._last_target_by_ticker[ticker] = weight
self.set_holdings(targets)
def _should_skip_rebalance(self, target_weights):
max_change = 0
for ticker in self._all_tickers:
old_weight = self._last_target_by_ticker[ticker]
new_weight = target_weights.get(ticker, 0)
change = abs(new_weight - old_weight)
max_change = max(max_change, change)
return max_change < self._minimum_rebalance_change
def _select_positive_top_tickers(self, score_by_ticker, count):
ranked_tickers = self._select_top_tickers(score_by_ticker, count)
return [t for t in ranked_tickers if score_by_ticker[t] > 0]
def _select_top_tickers(self, score_by_ticker, count):
ranked_tickers = sorted(
score_by_ticker,
key=score_by_ticker.get,
reverse=True
)
return ranked_tickers[:count]
def _get_close_series(self, history, symbol):
symbols_in_history = history.index.get_level_values(0)
if symbol not in symbols_in_history:
return None
return history.loc[symbol]["close"].dropna()