From 6abc658d4531b149139ab097d5f9011612d112e7 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:05:20 +0800 Subject: [PATCH] Publish IBKR QQQ/TQQQ research follow-ups --- research/README.md | 8 + ...est_video_qqq_tqqq_growth_optimizations.py | 572 ++++++++++++++++++ ...acktest_video_qqq_tqqq_position_scaling.py | 476 +++++++++++++++ ...q_tqqq_growth_optimizations_comparison.csv | 67 ++ ...q_growth_optimizations_recommendation.json | 77 +++ ...o_qqq_tqqq_growth_optimizations_summary.md | 54 ++ ...o_qqq_tqqq_position_scaling_comparison.csv | 13 + ..._tqqq_position_scaling_recommendation.json | 24 + ...video_qqq_tqqq_position_scaling_summary.md | 30 + 9 files changed, 1321 insertions(+) create mode 100644 research/backtest_video_qqq_tqqq_growth_optimizations.py create mode 100644 research/backtest_video_qqq_tqqq_position_scaling.py create mode 100644 research/results/video_qqq_tqqq_growth_optimizations_comparison.csv create mode 100644 research/results/video_qqq_tqqq_growth_optimizations_recommendation.json create mode 100644 research/results/video_qqq_tqqq_growth_optimizations_summary.md create mode 100644 research/results/video_qqq_tqqq_position_scaling_comparison.csv create mode 100644 research/results/video_qqq_tqqq_position_scaling_recommendation.json create mode 100644 research/results/video_qqq_tqqq_position_scaling_summary.md diff --git a/research/README.md b/research/README.md index 8d65f5c..49f19e7 100644 --- a/research/README.md +++ b/research/README.md @@ -15,6 +15,14 @@ Current retained research: - `results/video_qqq_tqqq_dual_drive_comparison.csv` - `results/video_qqq_tqqq_dual_drive_recommendation.json` - `results/video_qqq_tqqq_dual_drive_summary.md` +- `backtest_video_qqq_tqqq_position_scaling.py` +- `results/video_qqq_tqqq_position_scaling_comparison.csv` +- `results/video_qqq_tqqq_position_scaling_recommendation.json` +- `results/video_qqq_tqqq_position_scaling_summary.md` +- `backtest_video_qqq_tqqq_growth_optimizations.py` +- `results/video_qqq_tqqq_growth_optimizations_comparison.csv` +- `results/video_qqq_tqqq_growth_optimizations_recommendation.json` +- `results/video_qqq_tqqq_growth_optimizations_summary.md` Use `UsEquitySnapshotPipelines` outputs when reviewing snapshot-backed live strategy performance. diff --git a/research/backtest_video_qqq_tqqq_growth_optimizations.py b/research/backtest_video_qqq_tqqq_growth_optimizations.py new file mode 100644 index 0000000..a4f472e --- /dev/null +++ b/research/backtest_video_qqq_tqqq_growth_optimizations.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python3 +"""Research-only growth optimization study for the QQQ/TQQQ dual-drive idea. + +This script keeps the next-close execution model and explores growth-oriented +changes around three themes: + +- higher TQQQ share during risk-on trends; +- partial QQQ exposure while the baseline would sit in cash; +- cleaner or more aggressive pullback entries. + +It does not change live strategy code. +""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path + +import pandas as pd + +from backtest_video_qqq_tqqq_dual_drive import ( + CASH_SYMBOL, + build_buy_hold_run, + build_summary, + frame_to_markdown_table, + normalize_weights, +) +from backtest_video_qqq_tqqq_position_scaling import compute_turnover, load_market_data + + +CURRENT_DIR = Path(__file__).resolve().parent +DEFAULT_RESULTS_DIR = CURRENT_DIR / "results" +DEFAULT_DOWNLOAD_START = "2016-01-01" +DEFAULT_PERIOD_START = "2017-01-03" +DEFAULT_PERIOD_END = None +DEFAULT_COSTS_BPS = (0.0, 5.0, 10.0) + + +@dataclass(frozen=True) +class OptimizationConfig: + name: str + theme: str + description: str + bull_qqq_weight: float = 0.45 + bull_tqqq_weight: float = 0.45 + bull_cash_weight: float = 0.10 + strong_qqq_weight: float | None = None + strong_tqqq_weight: float | None = None + strong_cash_weight: float = 0.10 + pullback_mode: str = "base" + pullback_qqq_weight: float = 0.45 + pullback_tqqq_weight: float = 0.45 + pullback_cash_weight: float = 0.10 + idle_qqq_weight: float = 0.0 + idle_condition: str = "always" + + +@dataclass +class StrategyRun: + strategy_name: str + display_name: str + gross_returns: pd.Series + weights_history: pd.DataFrame + turnover_history: pd.Series + metadata: dict[str, object] + + +CONFIGS = ( + OptimizationConfig( + name="baseline_pullback_45_45", + theme="baseline", + description="Current retained pullback reconstruction: 45% QQQ + 45% TQQQ + 10% cash.", + ), + OptimizationConfig( + name="attack_40_50", + theme="attack_weight", + description="Replace 5 points of QQQ with TQQQ during both trend and pullback risk-on states.", + bull_qqq_weight=0.40, + bull_tqqq_weight=0.50, + pullback_qqq_weight=0.40, + pullback_tqqq_weight=0.50, + ), + OptimizationConfig( + name="attack_35_55", + theme="attack_weight", + description="Replace 10 points of QQQ with TQQQ during both trend and pullback risk-on states.", + bull_qqq_weight=0.35, + bull_tqqq_weight=0.55, + pullback_qqq_weight=0.35, + pullback_tqqq_weight=0.55, + ), + OptimizationConfig( + name="attack_30_60", + theme="attack_weight", + description="Replace 15 points of QQQ with TQQQ during both trend and pullback risk-on states.", + bull_qqq_weight=0.30, + bull_tqqq_weight=0.60, + pullback_qqq_weight=0.30, + pullback_tqqq_weight=0.60, + ), + OptimizationConfig( + name="attack_25_65", + theme="attack_weight", + description="Replace 20 points of QQQ with TQQQ during both trend and pullback risk-on states.", + bull_qqq_weight=0.25, + bull_tqqq_weight=0.65, + pullback_qqq_weight=0.25, + pullback_tqqq_weight=0.65, + ), + OptimizationConfig( + name="strong_trend_35_55", + theme="attack_weight", + description="Use 35% QQQ + 55% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.", + strong_qqq_weight=0.35, + strong_tqqq_weight=0.55, + ), + OptimizationConfig( + name="strong_trend_30_60", + theme="attack_weight", + description="Use 30% QQQ + 60% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.", + strong_qqq_weight=0.30, + strong_tqqq_weight=0.60, + ), + OptimizationConfig( + name="idle_25qqq", + theme="idle_exposure", + description="Keep 25% QQQ / 75% cash while the baseline is idle.", + idle_qqq_weight=0.25, + ), + OptimizationConfig( + name="idle_50qqq", + theme="idle_exposure", + description="Keep 50% QQQ / 50% cash while the baseline is idle.", + idle_qqq_weight=0.50, + ), + OptimizationConfig( + name="idle_25qqq_positive_ma20", + theme="idle_exposure", + description="Keep 25% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.", + idle_qqq_weight=0.25, + idle_condition="positive_ma20", + ), + OptimizationConfig( + name="idle_50qqq_positive_ma20", + theme="idle_exposure", + description="Keep 50% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.", + idle_qqq_weight=0.50, + idle_condition="positive_ma20", + ), + OptimizationConfig( + name="pullback_quality_45_45", + theme="pullback_quality", + description="Require below-MA200 pullback entries to clear a volatility-scaled rebound threshold.", + pullback_mode="quality_rebound", + ), + OptimizationConfig( + name="pullback_quality_35_55", + theme="pullback_quality", + description="Quality rebound pullback gate, then use 35% QQQ + 55% TQQQ.", + pullback_mode="quality_rebound", + pullback_qqq_weight=0.35, + pullback_tqqq_weight=0.55, + ), + OptimizationConfig( + name="pullback_quality_30_60", + theme="pullback_quality", + description="Quality rebound pullback gate, then use 30% QQQ + 60% TQQQ.", + pullback_mode="quality_rebound", + pullback_qqq_weight=0.30, + pullback_tqqq_weight=0.60, + ), + OptimizationConfig( + name="pullback_aggressive_35_55", + theme="pullback_quality", + description="Keep the current pullback gate but shift pullback weight to 35% QQQ + 55% TQQQ.", + pullback_qqq_weight=0.35, + pullback_tqqq_weight=0.55, + ), + OptimizationConfig( + name="pullback_aggressive_30_60", + theme="pullback_quality", + description="Keep the current pullback gate but shift pullback weight to 30% QQQ + 60% TQQQ.", + pullback_qqq_weight=0.30, + pullback_tqqq_weight=0.60, + ), + OptimizationConfig( + name="strong_trend_35_55_quality_pullback_35_55", + theme="combo", + description="Use stronger TQQQ in strong trends and only take quality rebound pullbacks at 35%/55%.", + strong_qqq_weight=0.35, + strong_tqqq_weight=0.55, + pullback_mode="quality_rebound", + pullback_qqq_weight=0.35, + pullback_tqqq_weight=0.55, + ), + OptimizationConfig( + name="attack_40_50_idle_25qqq", + theme="combo", + description="Use 40%/50% risk-on weights and keep 25% QQQ while idle.", + bull_qqq_weight=0.40, + bull_tqqq_weight=0.50, + pullback_qqq_weight=0.40, + pullback_tqqq_weight=0.50, + idle_qqq_weight=0.25, + ), + OptimizationConfig( + name="strong_trend_35_55_idle_25qqq", + theme="combo", + description="Use 35%/55% only in strong trend stacks and keep 25% QQQ while idle.", + strong_qqq_weight=0.35, + strong_tqqq_weight=0.55, + idle_qqq_weight=0.25, + ), + OptimizationConfig( + name="attack_40_50_quality_pullback_35_55_idle_25qqq", + theme="combo", + description="Moderately higher trend TQQQ, quality rebound pullbacks at 35%/55%, and 25% QQQ idle.", + bull_qqq_weight=0.40, + bull_tqqq_weight=0.50, + pullback_mode="quality_rebound", + pullback_qqq_weight=0.35, + pullback_tqqq_weight=0.55, + idle_qqq_weight=0.25, + ), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--results-dir", default=str(DEFAULT_RESULTS_DIR)) + parser.add_argument("--download-start", default=DEFAULT_DOWNLOAD_START) + parser.add_argument("--period-start", default=DEFAULT_PERIOD_START) + parser.add_argument("--period-end", default=DEFAULT_PERIOD_END) + parser.add_argument("--end", default=None, help="Inclusive Nasdaq download end date, YYYY-MM-DD.") + parser.add_argument("--cost-bps", nargs="*", type=float, default=list(DEFAULT_COSTS_BPS)) + return parser.parse_args() + + +def enrich_indicators(indicators: pd.DataFrame) -> pd.DataFrame: + frame = indicators.copy() + close = frame["qqq_close"] + frame["qqq_ma10"] = close.rolling(10).mean() + frame["qqq_ma10_slope"] = frame["qqq_ma10"].diff() + frame["qqq_return_5d"] = close.pct_change(5, fill_method=None) + frame["qqq_pullback_low20"] = close.rolling(20).min() + frame["qqq_pullback_rebound20"] = close / frame["qqq_pullback_low20"] - 1.0 + frame["qqq_rolling_vol20"] = close.pct_change(fill_method=None).rolling(20).std() + return frame + + +def _is_strong_trend(row: pd.Series) -> bool: + close = float(row["qqq_close"]) + ma20 = float(row["qqq_ma20"]) if pd.notna(row["qqq_ma20"]) else float("nan") + ma60 = float(row["qqq_ma60"]) if pd.notna(row["qqq_ma60"]) else float("nan") + ma200 = float(row["qqq_ma200"]) if pd.notna(row["qqq_ma200"]) else float("nan") + ma20_slope = float(row["qqq_ma20_slope"]) if pd.notna(row["qqq_ma20_slope"]) else float("nan") + return ( + pd.notna(ma20) + and pd.notna(ma60) + and pd.notna(ma200) + and pd.notna(ma20_slope) + and close > ma20 + and ma20 > ma60 + and close > ma200 + and ma20_slope > 0.0 + ) + + +def _idle_qqq_weight(config: OptimizationConfig, row: pd.Series) -> float: + if config.idle_qqq_weight <= 0.0: + return 0.0 + if config.idle_condition == "always": + return float(config.idle_qqq_weight) + if config.idle_condition == "positive_ma20": + close = float(row["qqq_close"]) + ma20 = float(row["qqq_ma20"]) if pd.notna(row["qqq_ma20"]) else float("nan") + ma20_slope = float(row["qqq_ma20_slope"]) if pd.notna(row["qqq_ma20_slope"]) else float("nan") + if pd.notna(ma20) and pd.notna(ma20_slope) and close > ma20 and ma20_slope > 0.0: + return float(config.idle_qqq_weight) + return 0.0 + raise KeyError(f"Unknown idle condition: {config.idle_condition}") + + +def _pullback_risk_on(config: OptimizationConfig, row: pd.Series, *, above_ma200: bool) -> bool: + if config.pullback_mode == "none": + return False + + ma200 = float(row["qqq_ma200"]) if pd.notna(row["qqq_ma200"]) else float("nan") + if pd.isna(ma200): + return False + + close = float(row["qqq_close"]) + ma20 = float(row["qqq_ma20"]) if pd.notna(row["qqq_ma20"]) else float("nan") + ma20_slope = float(row["qqq_ma20_slope"]) if pd.notna(row["qqq_ma20_slope"]) else float("nan") + positive_ma20_slope = pd.notna(ma20_slope) and ma20_slope > 0.0 + base = (not above_ma200) and pd.notna(ma20) and close > ma20 and positive_ma20_slope + if config.pullback_mode == "base": + return base + if config.pullback_mode == "quality_rebound": + rebound = float(row["qqq_pullback_rebound20"]) if pd.notna(row["qqq_pullback_rebound20"]) else float("nan") + rolling_vol = float(row["qqq_rolling_vol20"]) if pd.notna(row["qqq_rolling_vol20"]) else float("nan") + threshold = max(0.02, rolling_vol * 2.0) if pd.notna(rolling_vol) else 0.02 + return base and pd.notna(rebound) and rebound > threshold + raise KeyError(f"Unknown pullback mode: {config.pullback_mode}") + + +def decide_weights( + config: OptimizationConfig, + row: pd.Series, + *, + risk_active: bool, +) -> tuple[dict[str, float], bool]: + close = float(row["qqq_close"]) + ma200 = float(row["qqq_ma200"]) if pd.notna(row["qqq_ma200"]) else float("nan") + ma20_slope = float(row["qqq_ma20_slope"]) if pd.notna(row["qqq_ma20_slope"]) else float("nan") + has_long_history = pd.notna(ma200) + above_ma200 = has_long_history and close > ma200 + positive_ma20_slope = pd.notna(ma20_slope) and ma20_slope > 0.0 + + next_risk_active = risk_active + if risk_active and has_long_history and not above_ma200: + next_risk_active = False + elif not risk_active and above_ma200 and positive_ma20_slope: + next_risk_active = True + + if next_risk_active: + if config.strong_qqq_weight is not None and config.strong_tqqq_weight is not None and _is_strong_trend(row): + return normalize_weights( + { + "QQQ": config.strong_qqq_weight, + "TQQQ": config.strong_tqqq_weight, + CASH_SYMBOL: config.strong_cash_weight, + } + ), next_risk_active + return normalize_weights( + { + "QQQ": config.bull_qqq_weight, + "TQQQ": config.bull_tqqq_weight, + CASH_SYMBOL: config.bull_cash_weight, + } + ), next_risk_active + + if _pullback_risk_on(config, row, above_ma200=above_ma200): + return normalize_weights( + { + "QQQ": config.pullback_qqq_weight, + "TQQQ": config.pullback_tqqq_weight, + CASH_SYMBOL: config.pullback_cash_weight, + } + ), next_risk_active + + idle_qqq = _idle_qqq_weight(config, row) + return normalize_weights({"QQQ": idle_qqq, CASH_SYMBOL: max(0.0, 1.0 - idle_qqq)}), next_risk_active + + +def run_backtest(config: OptimizationConfig, returns_matrix: pd.DataFrame, indicators: pd.DataFrame) -> StrategyRun: + index = returns_matrix.index.intersection(indicators.index) + asset_columns = ("QQQ", "TQQQ", CASH_SYMBOL) + weights_history = pd.DataFrame(0.0, index=index, columns=asset_columns) + portfolio_returns = pd.Series(0.0, index=index, name=config.name) + turnover_history = pd.Series(0.0, index=index, name="turnover") + + current_weights = {CASH_SYMBOL: 1.0} + risk_active = False + for idx in range(len(index) - 1): + date = index[idx] + next_date = index[idx + 1] + target_weights, risk_active = decide_weights(config, indicators.loc[date], risk_active=risk_active) + if target_weights != current_weights: + turnover_history.at[next_date] = compute_turnover(current_weights, target_weights) + current_weights = target_weights + for symbol, weight in current_weights.items(): + weights_history.at[date, symbol] = weight + + row_returns = returns_matrix.loc[next_date] + portfolio_returns.at[next_date] = sum( + weight * float(row_returns.get(symbol, 0.0)) + for symbol, weight in current_weights.items() + if symbol != CASH_SYMBOL + ) + + for symbol, weight in current_weights.items(): + weights_history.at[index[-1], symbol] = weight + + return StrategyRun( + strategy_name=config.name, + display_name=config.name, + gross_returns=portfolio_returns, + weights_history=weights_history, + turnover_history=turnover_history, + metadata={ + "family": "video_qqq_tqqq_growth_optimizations", + "execution_mode": "next_close", + "theme": config.theme, + "description": config.description, + "known_limitation": "Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.", + }, + ) + + +def choose_recommendation(summary: pd.DataFrame) -> dict[str, object]: + focus = summary.loc[ + (summary["cost_bps_one_way"] == 5.0) + & (summary["family"] == "video_qqq_tqqq_growth_optimizations") + ].copy() + baseline = focus.loc[focus["strategy"] == "baseline_pullback_45_45"].iloc[0] + candidates = focus.loc[focus["strategy"] != "baseline_pullback_45_45"].copy() + baseline_maxdd = float(baseline["Max Drawdown"]) + near_drawdown_floor = baseline_maxdd - 0.05 + near_drawdown = candidates.loc[candidates["Max Drawdown"] >= near_drawdown_floor].copy() + if near_drawdown.empty: + near_drawdown = candidates.copy() + + best_cagr = candidates.sort_values("CAGR", ascending=False).iloc[0] + best_near_drawdown = near_drawdown.sort_values("CAGR", ascending=False).iloc[0] + best_2023 = candidates.sort_values("2023+ CAGR", ascending=False).iloc[0] + + by_theme: dict[str, dict[str, object]] = {} + for theme, rows in candidates.groupby("theme"): + best = rows.sort_values("CAGR", ascending=False).iloc[0] + by_theme[str(theme)] = { + "strategy": str(best["strategy"]), + "cagr": float(best["CAGR"]), + "max_drawdown": float(best["Max Drawdown"]), + "turnover_per_year": float(best["Turnover/Year"]), + "cagr_delta_vs_baseline": float(best["CAGR"] - baseline["CAGR"]), + } + + findings = [ + ( + f"Baseline: {baseline['CAGR']:.2%} CAGR / {baseline['Max Drawdown']:.2%} MaxDD / " + f"{baseline['2023+ CAGR']:.2%} 2023+ CAGR." + ), + ( + f"Best CAGR candidate: `{best_cagr['strategy']}` at {best_cagr['CAGR']:.2%} CAGR / " + f"{best_cagr['Max Drawdown']:.2%} MaxDD." + ), + ( + f"Best candidate within 5pp worse MaxDD than baseline: `{best_near_drawdown['strategy']}` at " + f"{best_near_drawdown['CAGR']:.2%} CAGR / {best_near_drawdown['Max Drawdown']:.2%} MaxDD." + ), + ( + f"Best 2023+ candidate: `{best_2023['strategy']}` at {best_2023['2023+ CAGR']:.2%} 2023+ CAGR / " + f"{best_2023['Max Drawdown']:.2%} MaxDD." + ), + ] + + verdict = ( + "The cleanest growth lever is shifting a moderate amount of QQQ into TQQQ during risk-on states. " + "Idle QQQ exposure helps less, and stricter pullback quality gates mostly give up too much upside." + ) + findings.append(verdict) + + return { + "baseline": { + "strategy": str(baseline["strategy"]), + "cagr": float(baseline["CAGR"]), + "max_drawdown": baseline_maxdd, + "turnover_per_year": float(baseline["Turnover/Year"]), + "cagr_2023_plus": float(baseline["2023+ CAGR"]), + }, + "best_cagr": _row_summary(best_cagr), + "best_near_baseline_drawdown": _row_summary(best_near_drawdown), + "best_2023_plus": _row_summary(best_2023), + "best_by_theme": by_theme, + "findings": findings, + "verdict": verdict, + } + + +def _row_summary(row: pd.Series) -> dict[str, object]: + return { + "strategy": str(row["strategy"]), + "theme": str(row["theme"]), + "cagr": float(row["CAGR"]), + "max_drawdown": float(row["Max Drawdown"]), + "turnover_per_year": float(row["Turnover/Year"]), + "cagr_2023_plus": float(row["2023+ CAGR"]), + "average_qqq_weight": float(row["Average QQQ Weight"]), + "average_tqqq_weight": float(row["Average TQQQ Weight"]), + } + + +def build_markdown(summary: pd.DataFrame, recommendation: dict[str, object]) -> str: + focus = summary.loc[ + (summary["cost_bps_one_way"] == 5.0) + & (summary["family"] == "video_qqq_tqqq_growth_optimizations") + ].copy() + compact_columns = [ + "strategy", + "theme", + "CAGR", + "Max Drawdown", + "2020 Return", + "2022 Return", + "2023 Return", + "2023+ CAGR", + "Turnover/Year", + "Average QQQ Weight", + "Average TQQQ Weight", + "Average Cash Weight", + ] + top_cagr = focus.sort_values("CAGR", ascending=False).head(10) + top_2023 = focus.sort_values("2023+ CAGR", ascending=False).head(10) + theme_best = focus.loc[focus["strategy"].isin(item["strategy"] for item in recommendation["best_by_theme"].values())] + theme_best = theme_best.sort_values("CAGR", ascending=False) + return "\n".join( + [ + "# Video QQQ/TQQQ Growth Optimization Follow-up", + "", + "## Setup", + "- Data: Nasdaq daily close/OHLC, not dividend-adjusted.", + "- Signal timing: next-close implementation; no same-close lookahead.", + "- Baseline: retained pullback reconstruction, 45% QQQ + 45% TQQQ + 10% cash.", + "- Goal: improve growth without treating lower drawdown as the primary objective.", + "", + "## Top CAGR Candidates", + frame_to_markdown_table(top_cagr[compact_columns]), + "", + "## Top 2023+ CAGR Candidates", + frame_to_markdown_table(top_2023[compact_columns]), + "", + "## Best By Theme", + frame_to_markdown_table(theme_best[compact_columns]), + "", + "## Findings", + *[f"- {item}" for item in recommendation["findings"]], + "", + "## Caveats", + "- Nasdaq close data is not adjusted for dividends, so absolute CAGR should be compared mainly within this study.", + "- This is a research-only experiment; no live allocation code was changed.", + ] + ) + "\n" + + +def write_outputs(summary: pd.DataFrame, recommendation: dict[str, object], results_dir: Path) -> None: + comparison_path = results_dir / "video_qqq_tqqq_growth_optimizations_comparison.csv" + summary_path = results_dir / "video_qqq_tqqq_growth_optimizations_summary.md" + recommendation_path = results_dir / "video_qqq_tqqq_growth_optimizations_recommendation.json" + summary.to_csv(comparison_path, index=False) + summary_path.write_text(build_markdown(summary, recommendation), encoding="utf-8") + recommendation_path.write_text(json.dumps(recommendation, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"wrote {comparison_path}") + print(f"wrote {summary_path}") + print(f"wrote {recommendation_path}") + + +def main() -> None: + args = parse_args() + results_dir = Path(args.results_dir).expanduser().resolve() + results_dir.mkdir(parents=True, exist_ok=True) + + _qqq_ohlc, returns_matrix, indicators = load_market_data(start=args.download_start, end=args.end) + indicators = enrich_indicators(indicators) + runs = [run_backtest(config, returns_matrix, indicators) for config in CONFIGS] + reference_runs = [build_buy_hold_run("QQQ", returns_matrix), build_buy_hold_run("TQQQ", returns_matrix)] + + summary = build_summary( + [*runs, *reference_runs], + returns_matrix["QQQ"], + costs_bps=args.cost_bps, + period_start=args.period_start, + period_end=args.period_end, + ) + recommendation = choose_recommendation(summary) + write_outputs(summary, recommendation, results_dir) + + +if __name__ == "__main__": + main() diff --git a/research/backtest_video_qqq_tqqq_position_scaling.py b/research/backtest_video_qqq_tqqq_position_scaling.py new file mode 100644 index 0000000..6867de4 --- /dev/null +++ b/research/backtest_video_qqq_tqqq_position_scaling.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +"""Research-only position-scaling follow-up for the QQQ/TQQQ dual-drive idea. + +This script does not change the live strategy. It tests whether changing only +the in-position TQQQ sleeve size improves the retained dual-drive reconstruction. +Signals are formed on today's close and applied to the next close-to-close +return. +""" + +from __future__ import annotations + +import argparse +import json +import math +from dataclasses import dataclass +from pathlib import Path + +import pandas as pd +import requests + +from backtest_video_qqq_tqqq_dual_drive import ( + CASH_SYMBOL, + VideoConfig, + build_buy_hold_run, + build_summary, + decide_video_weights, + frame_to_markdown_table, + normalize_weights, +) + + +CURRENT_DIR = Path(__file__).resolve().parent +DEFAULT_RESULTS_DIR = CURRENT_DIR / "results" +DEFAULT_PERIOD_START = "2017-01-03" +DEFAULT_DOWNLOAD_START = "2016-01-01" +DEFAULT_PERIOD_END = None +DEFAULT_COSTS_BPS = (0.0, 5.0, 10.0) +NASDAQ_ENDPOINT = "https://api.nasdaq.com/api/quote/{symbol}/historical" + + +@dataclass(frozen=True) +class ScalingConfig: + name: str + description: str + + +@dataclass +class StrategyRun: + strategy_name: str + display_name: str + gross_returns: pd.Series + weights_history: pd.DataFrame + turnover_history: pd.Series + metadata: dict[str, object] + scale_history: pd.Series + + +SCALING_CONFIGS = ( + ScalingConfig( + name="baseline", + description="No in-position scaling; keep 45% QQQ + 45% TQQQ + 10% cash while risk-on.", + ), + ScalingConfig( + name="ma60_half", + description="Cut TQQQ by 50% while risk-on when QQQ closes below MA60; move freed weight to cash.", + ), + ScalingConfig( + name="ma20_gap_trim_only", + description="Scale TQQQ to 50% when QQQ is 2%+ below MA20, 75% when below MA20, otherwise baseline.", + ), + ScalingConfig( + name="ma20_gap_trim_boost", + description="Same MA20 trim, plus boost TQQQ to 115% of baseline when QQQ is 3%+ above MA20.", + ), + ScalingConfig( + name="trend_score_4", + description="Use close>MA20, MA20>MA60, positive MA20 slope, close>MA200 to scale TQQQ in steps.", + ), +) + + +SIGNAL_CONFIGS = ( + VideoConfig( + name="trend_only", + description="Above-MA200 dual-drive state machine without below-MA200 pullback entry.", + execution_mode="next_close", + ), + VideoConfig( + name="pullback", + description="Above-MA200 dual-drive plus the retained below-MA200 pullback state.", + execution_mode="next_close", + allow_below_ma200_pullback=True, + ), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--results-dir", default=str(DEFAULT_RESULTS_DIR)) + parser.add_argument("--download-start", default=DEFAULT_DOWNLOAD_START) + parser.add_argument("--period-start", default=DEFAULT_PERIOD_START) + parser.add_argument("--period-end", default=DEFAULT_PERIOD_END) + parser.add_argument("--end", default=None, help="Inclusive Nasdaq download end date, YYYY-MM-DD.") + parser.add_argument("--cost-bps", nargs="*", type=float, default=list(DEFAULT_COSTS_BPS)) + return parser.parse_args() + + +def _clean_price(value: object) -> float: + text = str(value).replace("$", "").replace(",", "").strip() + return float(text) + + +def download_nasdaq_ohlcv(symbol: str, *, start: str, end: str | None) -> pd.DataFrame: + params = { + "assetclass": "etf", + "fromdate": start, + "limit": "9999", + } + if end: + params["todate"] = end + headers = { + "User-Agent": ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0 Safari/537.36" + ), + "Accept": "application/json,text/plain,*/*", + "Origin": "https://www.nasdaq.com", + "Referer": f"https://www.nasdaq.com/market-activity/etf/{symbol.lower()}/historical", + } + response = requests.get( + NASDAQ_ENDPOINT.format(symbol=symbol.upper()), + params=params, + headers=headers, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + rows = (((payload.get("data") or {}).get("tradesTable") or {}).get("rows") or []) + if not rows: + raise RuntimeError(f"No Nasdaq historical rows returned for {symbol}") + + frame = pd.DataFrame(rows) + frame["date"] = pd.to_datetime(frame["date"]).dt.tz_localize(None).dt.normalize() + frame = frame.rename(columns={"close": "close", "open": "open", "high": "high", "low": "low", "volume": "volume"}) + for column in ("open", "high", "low", "close"): + frame[column] = frame[column].map(_clean_price) + frame["volume"] = frame["volume"].map(lambda value: int(str(value).replace(",", "").strip())) + frame = frame.set_index("date").sort_index() + return frame[["open", "high", "low", "close", "volume"]] + + +def load_market_data(*, start: str, end: str | None) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + qqq_ohlc = download_nasdaq_ohlcv("QQQ", start=start, end=end) + tqqq_ohlc = download_nasdaq_ohlcv("TQQQ", start=start, end=end) + index = qqq_ohlc.index.intersection(tqqq_ohlc.index) + qqq_ohlc = qqq_ohlc.reindex(index).dropna() + tqqq_ohlc = tqqq_ohlc.reindex(index).dropna() + close = pd.DataFrame({"QQQ": qqq_ohlc["close"], "TQQQ": tqqq_ohlc["close"]}, index=index) + returns_matrix = close.pct_change(fill_method=None).replace([float("inf"), float("-inf")], pd.NA).fillna(0.0) + returns_matrix[CASH_SYMBOL] = 0.0 + indicators = build_indicator_frame(qqq_ohlc, tqqq_ohlc) + return qqq_ohlc, returns_matrix, indicators + + +def build_indicator_frame(qqq_ohlc: pd.DataFrame, tqqq_ohlc: pd.DataFrame) -> pd.DataFrame: + index = qqq_ohlc.index.intersection(tqqq_ohlc.index) + qqq_close = qqq_ohlc["close"].reindex(index) + tqqq_close = tqqq_ohlc["close"].reindex(index) + frame = pd.DataFrame(index=index) + frame["qqq_close"] = qqq_close + frame["tqqq_close"] = tqqq_close + frame["qqq_ma20"] = qqq_close.rolling(20).mean() + frame["qqq_ma60"] = qqq_close.rolling(60).mean() + frame["qqq_ma200"] = qqq_close.rolling(200).mean() + frame["qqq_ma20_slope"] = frame["qqq_ma20"].diff() + frame["qqq_ma20_gap"] = qqq_close / frame["qqq_ma20"] - 1.0 + frame["tqqq_ma200"] = tqqq_close.rolling(200).mean() + frame["tqqq_overheat_ratio"] = tqqq_close / frame["tqqq_ma200"] + return frame + + +def compute_turnover(old_weights: dict[str, float], new_weights: dict[str, float]) -> float: + symbols = set(old_weights) | set(new_weights) + return 0.5 * sum(abs(float(new_weights.get(symbol, 0.0)) - float(old_weights.get(symbol, 0.0))) for symbol in symbols) + + +def scaling_multiplier(config_name: str, row: pd.Series) -> float: + close = float(row["qqq_close"]) + ma20 = float(row["qqq_ma20"]) if pd.notna(row["qqq_ma20"]) else float("nan") + ma60 = float(row["qqq_ma60"]) if pd.notna(row["qqq_ma60"]) else float("nan") + ma200 = float(row["qqq_ma200"]) if pd.notna(row["qqq_ma200"]) else float("nan") + ma20_slope = float(row["qqq_ma20_slope"]) if pd.notna(row["qqq_ma20_slope"]) else float("nan") + ma20_gap = float(row["qqq_ma20_gap"]) if pd.notna(row["qqq_ma20_gap"]) else float("nan") + + if config_name == "baseline": + return 1.0 + if config_name == "ma60_half": + return 0.5 if pd.notna(ma60) and close < ma60 else 1.0 + if config_name == "ma20_gap_trim_only": + if pd.notna(ma20_gap) and ma20_gap <= -0.02: + return 0.5 + if pd.notna(ma20_gap) and ma20_gap < 0.0: + return 0.75 + return 1.0 + if config_name == "ma20_gap_trim_boost": + if pd.notna(ma20_gap) and ma20_gap <= -0.02: + return 0.5 + if pd.notna(ma20_gap) and ma20_gap < 0.0: + return 0.75 + if pd.notna(ma20_gap) and ma20_gap >= 0.03: + return 1.15 + return 1.0 + if config_name == "trend_score_4": + score = sum( + [ + pd.notna(ma20) and close > ma20, + pd.notna(ma20) and pd.notna(ma60) and ma20 > ma60, + pd.notna(ma20_slope) and ma20_slope > 0.0, + pd.notna(ma200) and close > ma200, + ] + ) + if score >= 4: + return 1.10 + if score == 3: + return 1.0 + if score == 2: + return 0.75 + return 0.5 + raise KeyError(f"Unknown scaling config: {config_name}") + + +def apply_tqqq_scaling(weights: dict[str, float], scale: float) -> dict[str, float]: + base_tqqq = float(weights.get("TQQQ", 0.0)) + if base_tqqq <= 1e-12: + return normalize_weights(weights) + + target_tqqq = max(0.0, base_tqqq * float(scale)) + delta = target_tqqq - base_tqqq + adjusted = dict(weights) + adjusted["TQQQ"] = target_tqqq + + if delta < 0.0: + adjusted[CASH_SYMBOL] = float(adjusted.get(CASH_SYMBOL, 0.0)) + abs(delta) + elif delta > 0.0: + cash = float(adjusted.get(CASH_SYMBOL, 0.0)) + cash_used = min(cash, delta) + adjusted[CASH_SYMBOL] = cash - cash_used + remaining = delta - cash_used + if remaining > 1e-12: + adjusted["QQQ"] = max(0.0, float(adjusted.get("QQQ", 0.0)) - remaining) + + return normalize_weights(adjusted) + + +def run_scaling_backtest( + signal_config: VideoConfig, + scaling_config: ScalingConfig, + returns_matrix: pd.DataFrame, + indicators: pd.DataFrame, +) -> StrategyRun: + index = returns_matrix.index.intersection(indicators.index) + asset_columns = ("QQQ", "TQQQ", CASH_SYMBOL) + weights_history = pd.DataFrame(0.0, index=index, columns=asset_columns) + portfolio_returns = pd.Series(0.0, index=index, name=f"{signal_config.name}_{scaling_config.name}") + turnover_history = pd.Series(0.0, index=index, name="turnover") + scale_history = pd.Series(0.0, index=index, name="tqqq_scale") + + current_weights = {CASH_SYMBOL: 1.0} + risk_active = False + for idx in range(len(index) - 1): + date = index[idx] + next_date = index[idx + 1] + base_weights, risk_active = decide_video_weights(signal_config, indicators.loc[date], risk_active=risk_active) + scale = scaling_multiplier(scaling_config.name, indicators.loc[date]) if base_weights.get("TQQQ", 0.0) > 0.0 else 0.0 + target_weights = apply_tqqq_scaling(base_weights, scale) + scale_history.at[date] = scale + + if target_weights != current_weights: + turnover_history.at[next_date] = compute_turnover(current_weights, target_weights) + current_weights = target_weights + for symbol, weight in current_weights.items(): + weights_history.at[date, symbol] = weight + + row_returns = returns_matrix.loc[next_date] + portfolio_returns.at[next_date] = sum( + weight * float(row_returns.get(symbol, 0.0)) + for symbol, weight in current_weights.items() + if symbol != CASH_SYMBOL + ) + + for symbol, weight in current_weights.items(): + weights_history.at[index[-1], symbol] = weight + + return StrategyRun( + strategy_name=f"{signal_config.name}_{scaling_config.name}", + display_name=f"{signal_config.name}_{scaling_config.name}", + gross_returns=portfolio_returns, + weights_history=weights_history, + turnover_history=turnover_history, + scale_history=scale_history, + metadata={ + "family": "video_qqq_tqqq_position_scaling", + "execution_mode": "next_close", + "signal_mode": signal_config.name, + "scaling": scaling_config.name, + "description": f"{signal_config.description} {scaling_config.description}", + "known_limitation": "Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.", + }, + ) + + +def enrich_summary(summary: pd.DataFrame, runs: list[StrategyRun]) -> pd.DataFrame: + scale_by_strategy = { + run.strategy_name: run.scale_history.reindex(run.gross_returns.index).fillna(0.0) + for run in runs + } + rows: list[dict[str, object]] = [] + for row in summary.to_dict("records"): + strategy = str(row["strategy"]) + scale = scale_by_strategy.get(strategy) + if scale is None: + row["Average Scale While Invested"] = math.nan + else: + start = row.get("Start") + end = row.get("End") + sliced_scale = scale.copy() + if start: + sliced_scale = sliced_scale.loc[str(start):] + if end: + sliced_scale = sliced_scale.loc[: str(end)] + invested = sliced_scale[sliced_scale > 0.0] + row["Average Scale While Invested"] = float(invested.mean()) if not invested.empty else 0.0 + rows.append(row) + return pd.DataFrame(rows) + + +def choose_recommendation(summary: pd.DataFrame) -> dict[str, object]: + focus = summary.loc[summary["cost_bps_one_way"] == 5.0].copy() + scaled = focus.loc[focus["family"] == "video_qqq_tqqq_position_scaling"].copy() + pullback = scaled.loc[scaled["signal_mode"] == "pullback"].copy() + baseline = pullback.loc[pullback["scaling"] == "baseline"].iloc[0] + candidates = pullback.loc[pullback["scaling"] != "baseline"].copy() + candidates["score"] = ( + candidates["CAGR"].fillna(-999.0) * 3.0 + + candidates["Information Ratio vs QQQ"].fillna(-999.0) + - candidates["Turnover/Year"].fillna(999.0) * 0.02 + + (candidates["Max Drawdown"].fillna(-999.0) - baseline["Max Drawdown"]) * 0.5 + ) + best = candidates.sort_values("score", ascending=False).iloc[0] + improves_cagr = float(best["CAGR"]) > float(baseline["CAGR"]) + improves_drawdown = float(best["Max Drawdown"]) > float(baseline["Max Drawdown"]) + if improves_cagr and improves_drawdown: + verdict = "The best scaled pullback variant improves both CAGR and MaxDD in this run, but should still be validated with adjusted data before live use." + elif improves_cagr: + verdict = "The best scaled pullback variant raises CAGR, but it does not improve drawdown enough to justify a live change by itself." + elif improves_drawdown: + verdict = "Scaling smooths drawdown, but it lowers CAGR and adds turnover; this is not a clear upgrade." + else: + verdict = "No tested in-position scaling variant clearly improves the pullback baseline." + + findings = [ + ( + f"Pullback baseline: {baseline['CAGR']:.2%} CAGR / " + f"{baseline['Max Drawdown']:.2%} MaxDD / turnover {baseline['Turnover/Year']:.2f}/yr." + ), + ( + f"Best scaled pullback candidate: `{best['scaling']}` at {best['CAGR']:.2%} CAGR / " + f"{best['Max Drawdown']:.2%} MaxDD / turnover {best['Turnover/Year']:.2f}/yr." + ), + verdict, + ] + return { + "baseline_pullback": { + "strategy": str(baseline["strategy"]), + "cagr": float(baseline["CAGR"]), + "max_drawdown": float(baseline["Max Drawdown"]), + "turnover_per_year": float(baseline["Turnover/Year"]), + "average_tqqq_weight": float(baseline["Average TQQQ Weight"]), + }, + "best_scaled_pullback_candidate": { + "strategy": str(best["strategy"]), + "scaling": str(best["scaling"]), + "cagr": float(best["CAGR"]), + "max_drawdown": float(best["Max Drawdown"]), + "turnover_per_year": float(best["Turnover/Year"]), + "average_tqqq_weight": float(best["Average TQQQ Weight"]), + "average_scale_while_invested": float(best["Average Scale While Invested"]), + }, + "findings": findings, + "verdict": verdict, + } + + +def build_markdown(summary: pd.DataFrame, recommendation: dict[str, object]) -> str: + focus = summary.loc[summary["cost_bps_one_way"] == 5.0].copy() + focus = focus.loc[focus["family"] == "video_qqq_tqqq_position_scaling"].sort_values( + ["signal_mode", "CAGR"], + ascending=[True, False], + ) + compact_columns = [ + "strategy", + "signal_mode", + "scaling", + "CAGR", + "Max Drawdown", + "2020 Return", + "2022 Return", + "2023 Return", + "2023+ CAGR", + "Turnover/Year", + "Average QQQ Weight", + "Average TQQQ Weight", + "Average Cash Weight", + "Average Scale While Invested", + ] + return "\n".join( + [ + "# Video QQQ/TQQQ Position-Scaling Follow-up", + "", + "## Setup", + "- Data: Nasdaq daily close/OHLC, not dividend-adjusted.", + "- Signal timing: next-close implementation; no same-close lookahead.", + "- Baseline risk-on sleeve: 45% QQQ + 45% TQQQ + 10% cash.", + "- Scaling changes only the TQQQ sleeve while already in a risk-on or pullback-risk-on state.", + "", + "## 5 bps Comparison", + frame_to_markdown_table(focus[compact_columns]), + "", + "## Findings", + *[f"- {item}" for item in recommendation["findings"]], + "", + "## Caveats", + "- Nasdaq close data is not adjusted for dividends, so absolute CAGR is not a replacement for the retained Yahoo adjusted reference.", + "- This is a research-only experiment; no live allocation code was changed.", + ] + ) + "\n" + + +def write_scaling_outputs(summary: pd.DataFrame, recommendation: dict[str, object], results_dir: Path) -> None: + comparison_path = results_dir / "video_qqq_tqqq_position_scaling_comparison.csv" + summary_path = results_dir / "video_qqq_tqqq_position_scaling_summary.md" + recommendation_path = results_dir / "video_qqq_tqqq_position_scaling_recommendation.json" + summary.to_csv(comparison_path, index=False) + summary_path.write_text(build_markdown(summary, recommendation), encoding="utf-8") + recommendation_path.write_text(json.dumps(recommendation, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"wrote {comparison_path}") + print(f"wrote {summary_path}") + print(f"wrote {recommendation_path}") + + +def main() -> None: + args = parse_args() + results_dir = Path(args.results_dir).expanduser().resolve() + results_dir.mkdir(parents=True, exist_ok=True) + + _qqq_ohlc, returns_matrix, indicators = load_market_data(start=args.download_start, end=args.end) + runs: list[StrategyRun] = [] + for signal_config in SIGNAL_CONFIGS: + for scaling_config in SCALING_CONFIGS: + runs.append(run_scaling_backtest(signal_config, scaling_config, returns_matrix, indicators)) + + reference_runs = [build_buy_hold_run("QQQ", returns_matrix), build_buy_hold_run("TQQQ", returns_matrix)] + summary = build_summary( + [*runs, *reference_runs], + returns_matrix["QQQ"], + costs_bps=args.cost_bps, + period_start=args.period_start, + period_end=args.period_end, + ) + summary = enrich_summary(summary, runs) + recommendation = choose_recommendation(summary) + write_scaling_outputs(summary, recommendation, results_dir) + + +if __name__ == "__main__": + main() diff --git a/research/results/video_qqq_tqqq_growth_optimizations_comparison.csv b/research/results/video_qqq_tqqq_growth_optimizations_comparison.csv new file mode 100644 index 0000000..2cd33b9 --- /dev/null +++ b/research/results/video_qqq_tqqq_growth_optimizations_comparison.csv @@ -0,0 +1,67 @@ +strategy,display_name,cost_bps_one_way,Start,End,Total Return,CAGR,Max Drawdown,Volatility,Sharpe,Beta vs QQQ,Information Ratio vs QQQ,Turnover/Year,2020 Return,2022 Return,2023 Return,2023+ CAGR,Average QQQ Weight,Average TQQQ Weight,Average Cash Weight,TQQQ Days Share,family,execution_mode,theme,description,known_limitation +baseline_pullback_45_45,baseline_pullback_45_45,0.0,2017-01-03,2026-04-17,14.575282787683719,0.34412348879944776,-0.31384528507408893,0.30556926657125144,1.1239460498639473,1.0043309957618087,0.661624204815186,3.9745576526098505,0.7973426831777359,-0.11058227306900204,0.651853924545222,0.4204141809004738,0.38408993576017136,0.38408993576017136,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,baseline,Current retained pullback reconstruction: 45% QQQ + 45% TQQQ + 10% cash.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +baseline_pullback_45_45,baseline_pullback_45_45,5.0,2017-01-03,2026-04-17,14.29050022544996,0.3414544893929359,-0.314771504640206,0.3055774809882369,1.1173997222836356,1.0044703935950823,0.6518531322179977,3.9745576526098505,0.7941106531980329,-0.11733016502096338,0.649611972331724,0.41905202815209286,0.38408993576017136,0.38408993576017136,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,baseline,Current retained pullback reconstruction: 45% QQQ + 45% TQQQ + 10% cash.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +baseline_pullback_45_45,baseline_pullback_45_45,10.0,2017-01-03,2026-04-17,14.010799935209327,0.3387895911783203,-0.3156968903139078,0.3055885758576715,1.1108432758307718,1.0046097914283554,0.642065972903271,3.9745576526098505,0.7908829841194385,-0.12402985252148091,0.6473720494509809,0.41769056883769484,0.38408993576017136,0.38408993576017136,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,baseline,Current retained pullback reconstruction: 45% QQQ + 45% TQQQ + 10% cash.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50,attack_40_50,0.0,2017-01-03,2026-04-17,16.492382954581718,0.36103482838701506,-0.3291110284309372,0.32241782559757626,1.1206760200972843,1.059662102266416,0.709388153168163,3.9745576526098505,0.8451272611432037,-0.11847244562965353,0.6892316640366154,0.44076766337115414,0.34141327623126333,0.42676659528907923,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 5 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50,attack_40_50,5.0,2017-01-03,2026-04-17,16.1725457963202,0.3583322328517877,-0.330016664539309,0.32242588515850057,1.1144723932207699,1.0598015000996892,0.7001466457677271,3.9745576526098505,0.8418093320271154,-0.1251588475375569,0.686938344190853,0.43938597865145224,0.34141327623126333,0.42676659528907923,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 5 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50,attack_40_50,10.0,2017-01-03,2026-04-17,15.858416527695741,0.3556337901163522,-0.330921485265949,0.3224366746677491,1.1082596931845514,1.0599408979329623,0.690889954146724,3.9745576526098505,0.8384958796752349,-0.131797495991966,0.6846471007325463,0.43800499730854825,0.34141327623126333,0.42676659528907923,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 5 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_35_55,attack_35_55,0.0,2017-01-03,2026-04-17,18.59262394018528,0.37775928981450835,-0.3441500677483763,0.33926704321665885,1.117728611303624,1.1149932087710233,0.7500322297543938,3.9745576526098505,0.8929344563119572,-0.1264768837405088,0.7269600242428,0.461010721552207,0.29873661670235546,0.4694432548179871,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 10 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_35_55,attack_35_55,5.0,2017-01-03,2026-04-17,18.234382530106323,0.37502346406683373,-0.3450354253784521,0.33927496328656126,1.1118335985990262,1.1151326066042968,0.7412905502704964,3.9745576526098505,0.8895305888277898,-0.13310097102810248,0.7246148297160797,0.45960960951560104,0.29873661670235546,0.4694432548179871,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 10 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_35_55,attack_35_55,10.0,2017-01-03,2026-04-17,17.882534423309437,0.3722918421692769,-0.3459199858638383,0.33928547774197537,1.1059304044266065,1.1152720044375697,0.732534692166734,3.9745576526098505,0.8861313140218958,-0.13967776144546395,0.7222717591171073,0.4582092107453615,0.29873661670235546,0.4694432548179871,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 10 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_30_60,attack_30_60,0.0,2017-01-03,2026-04-17,20.88593895279986,0.394284251781849,-0.3589632474975395,0.35611682594705907,1.1150583393417233,1.1703243152756309,0.7846485296047271,3.9745576526098505,0.9406968480684841,-0.13459109522104107,0.7650251680057072,0.4811303830232587,0.2560599571734475,0.512119914346895,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 15 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_30_60,attack_30_60,5.0,2017-01-03,2026-04-17,20.48576173324544,0.3915155866222344,-0.35982863051214653,0.35612461971293263,1.1094426977288603,1.1704637131089033,0.7763741183203257,3.9745576526098505,0.9372071242176716,-0.14115207757044024,0.7626276099606086,0.479709960730206,0.2560599571734475,0.512119914346895,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 15 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_30_60,attack_30_60,10.0,2017-01-03,2026-04-17,20.092726277618315,0.38875117576744556,-0.3606932343464486,0.35613488512378794,1.103819641131613,1.1706031109421775,0.7680865609511786,3.9745576526098505,0.9337221088465966,-0.1476662249722519,0.7602322238530859,0.4782902615335145,0.2560599571734475,0.512119914346895,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 15 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_25_65,attack_25_65,0.0,2017-01-03,2026-04-17,23.381758679477624,0.41059707499874554,-0.3735514692050125,0.3729670971975547,1.1126278836572514,1.2256554217802382,0.8142009498916196,3.9745576526098505,0.9883449395519883,-0.14281056958198135,0.8034126612579984,0.5011135784200216,0.21338329764453962,0.5547965738758031,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 20 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_25_65,attack_25_65,5.0,2017-01-03,2026-04-17,22.93594133579832,0.4077959861515288,-0.37439718027321944,0.37297477606132146,1.1072663375783134,1.225794819613511,0.8063606460634908,3.9745576526098505,0.9847695662983973,-0.14930769103366526,0.8009622698964263,0.49967397543082726,0.21338329764453962,0.5547965738758031,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 20 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_25_65,attack_25_65,10.0,2017-01-03,2026-04-17,22.498080352236606,0.4049992014301238,-0.3752421298539046,0.37298481491529295,1.1018980401699645,1.225934217446784,0.79850820724267,3.9745576526098505,0.9811990170447971,-0.15575844455342125,0.7985140989350104,0.4982351053020626,0.21338329764453962,0.5547965738758031,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Replace 20 points of QQQ with TQQQ during both trend and pullback risk-on states.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55,strong_trend_35_55,0.0,2017-01-03,2026-04-17,15.779331772960305,0.3549473676390229,-0.32349534889535336,0.31855405239739637,1.11630165267625,1.0454628438613849,0.6902022171454092,5.9564508994396945,0.8757426571018547,-0.11702609239270112,0.7010935605540578,0.4229575638274814,0.3353961456102784,0.43278372591006425,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 35% QQQ + 55% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55,strong_trend_35_55,5.0,2017-01-03,2026-04-17,15.322183236643962,0.35092199569336113,-0.3244440487054211,0.3185547300790788,1.1069317556449767,1.045579300782557,0.6761789725065237,5.9564508994396945,0.8705237257389693,-0.12376897778809914,0.697771803704418,0.41997486779044046,0.3353961456102784,0.43278372591006425,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 35% QQQ + 55% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55,strong_trend_35_55,10.0,2017-01-03,2026-04-17,14.877350382531311,0.34690731058330404,-0.3253918304492567,0.3185582654551577,1.0975520525684779,1.0456957577037285,0.662138586137283,5.9564508994396945,0.8653177129818894,-0.13046334182426766,0.6944554410776997,0.4169977308964956,0.3353961456102784,0.43278372591006425,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 35% QQQ + 55% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_30_60,strong_trend_30_60,0.0,2017-01-03,2026-04-17,16.389245787564505,0.36016817972533555,-0.32829157397669173,0.32534344891391537,1.1116927016142393,1.0660287679111728,0.7016569397929733,6.947397522854615,0.9153131307599514,-0.1202480017151416,0.725911011705507,0.4239642843874305,0.3110492505353319,0.4571306209850107,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 30% QQQ + 60% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_30_60,strong_trend_30_60,5.0,2017-01-03,2026-04-17,15.838119264174225,0.35545789291619845,-0.32925117519697467,0.3253403359380798,1.101005267988503,1.0661337543762945,0.68568817335251,6.947397522854615,0.9090433505272277,-0.12698814365229982,0.7220274957618011,0.4201705531965645,0.3110492505353319,0.4571306209850107,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 30% QQQ + 60% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_30_60,strong_trend_30_60,10.0,2017-01-03,2026-04-17,15.304307620489617,0.3507625591858645,-0.330209817248981,0.32534017265489,1.0903077444869427,1.0662387408414151,0.6697013164257021,6.947397522854615,0.9027923443593213,-0.13367960942241064,0.7181515472636522,0.4163861360319647,0.3110492505353319,0.4571306209850107,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,attack_weight,Use 30% QQQ + 60% TQQQ only when QQQ trend stack is strong; otherwise baseline weights.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq,idle_25qqq,0.0,2017-01-03,2026-04-17,14.721303488163157,0.3454751582116544,-0.3334617141843791,0.3078807577978407,1.1210884217182342,1.113677929867269,0.7704991176107562,2.8705138602182245,0.824334946910217,-0.17861568020139662,0.6993620868839905,0.4357062192110819,0.42070663811563175,0.38408993576017136,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ / 75% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq,idle_25qqq,5.0,2017-01-03,2026-04-17,14.513026567726193,0.3435437609672789,-0.3336754298908864,0.30788978246699245,1.1163848221196007,1.1137786060801886,0.7623830126564166,2.8705138602182245,0.8219762859239315,-0.1831347722792277,0.6976971424940936,0.43471266521562524,0.42070663811563175,0.38408993576017136,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ / 75% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq,idle_25qqq,10.0,2017-01-03,2026-04-17,14.307442465834285,0.3416145089015501,-0.33388914559739347,0.3079002981602525,1.1116761148719898,1.1138792822931083,0.7542544213992469,2.8705138602182245,0.8196199127686403,-0.18763045668644918,0.6960332858547504,0.43371947616785334,0.42070663811563175,0.38408993576017136,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ / 75% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq,idle_50qqq,0.0,2017-01-03,2026-04-17,14.660981922615928,0.34491814336234206,-0.35464839531725245,0.31472826487664846,1.1021562861135508,1.2230248639727292,0.8892169395344033,1.9872788263049252,0.8444792396716747,-0.2452769691737291,0.7479230760174578,0.45011905835577437,0.45732334047109213,0.38408993576017136,0.15858672376873661,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ / 50% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq,idle_50qqq,5.0,2017-01-03,2026-04-17,14.516944295355659,0.3435803038687615,-0.3547897509789736,0.3147365398856031,1.0989640631241306,1.2230945628893657,0.8828035941523756,1.9872788263049252,0.8428356323063704,-0.24816295953759826,0.7467380895958933,0.449424884812053,0.45732334047109213,0.38408993576017136,0.15858672376873661,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ / 50% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq,idle_50qqq,10.0,2017-01-03,2026-04-17,14.374199382798183,0.34224349395018816,-0.3549311066406945,0.31474551389083016,1.095769574465932,1.2231642618060021,0.876381600382361,1.9872788263049252,0.8411931236020271,-0.2510385612397863,0.7455536388160904,0.44873088762851765,0.45732334047109213,0.38408993576017136,0.15858672376873661,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ / 50% cash while the baseline is idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq_positive_ma20,idle_25qqq_positive_ma20,0.0,2017-01-03,2026-04-17,14.737206237999121,0.345621687985985,-0.31384528507408915,0.30556880330471864,1.127605119763383,1.0045259321355189,0.6673186403846212,4.001485549985256,0.7973426831777359,-0.11058227306900204,0.651853924545222,0.4204141809004738,0.3861241970021414,0.38408993576017136,0.22978586723768735,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq_positive_ma20,idle_25qqq_positive_ma20,5.0,2017-01-03,2026-04-17,14.447512796963794,0.34293145288457616,-0.3147715046402061,0.3055775503151648,1.1210125828904924,1.0046636061399619,0.6574749910229757,4.001485549985256,0.7941106531980329,-0.11733016502096338,0.649611972331724,0.41905202815209286,0.3861241970021414,0.38408993576017136,0.22978586723768735,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_25qqq_positive_ma20,idle_25qqq_positive_ma20,10.0,2017-01-03,2026-04-17,14.163027018167762,0.34024540552047444,-0.3156968903139079,0.3055891538664832,1.1144100062096003,1.0048012801444044,0.6476153472619521,4.001485549985256,0.7908829841194385,-0.12402985252148091,0.6473720494509809,0.41769056883769484,0.3861241970021414,0.38408993576017136,0.22978586723768735,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 25% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq_positive_ma20,idle_50qqq_positive_ma20,0.0,2017-01-03,2026-04-17,14.90040621669132,0.3471178443507208,-0.31384528507408904,0.3055773824159613,1.1312307253801002,1.0047208685092293,0.6729704673906923,4.033799026835743,0.7973426831777359,-0.11058227306900204,0.651853924545222,0.4204141809004738,0.3881584582441114,0.38408993576017136,0.22775160599571734,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq_positive_ma20,idle_50qqq_positive_ma20,5.0,2017-01-03,2026-04-17,14.605357195223036,0.3444028003599169,-0.31477150464020587,0.30558646953373725,1.1245840431464864,1.0048565038231223,0.6630421325123178,4.033799026835743,0.7941106531980329,-0.11733016502096338,0.649611972331724,0.41905202815209286,0.3881584582441114,0.38408993576017136,0.22775160599571734,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +idle_50qqq_positive_ma20,idle_50qqq_positive_ma20,10.0,2017-01-03,2026-04-17,14.315656187244405,0.34169203065177056,-0.3156968903139079,0.3055984259244495,1.11792725984957,1.0049921391370145,0.6530977005790692,4.033799026835743,0.7908829841194385,-0.12402985252148091,0.6473720494509809,0.41769056883769484,0.3881584582441114,0.38408993576017136,0.22775160599571734,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,idle_exposure,Keep 50% QQQ while idle only when QQQ is above MA20 with positive MA20 slope.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_45_45,pullback_quality_45_45,0.0,2017-01-03,2026-04-17,14.796703905975845,0.34616873708389395,-0.31384528507408893,0.3055278059163805,1.1290498283812047,1.0040912526296712,0.6691081517969379,3.7806767915069313,0.7973426831777359,-0.09793814516387989,0.651853924545222,0.4204141809004738,0.38389721627408996,0.38389721627408996,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Require below-MA200 pullback entries to clear a volatility-scaled rebound threshold.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_45_45,pullback_quality_45_45,5.0,2017-01-03,2026-04-17,14.5219386770246,0.3436268774071549,-0.314771504640206,0.3055333283062063,1.122830270688342,1.0041908363776464,0.6597981332036301,3.7806767915069313,0.7941106531980329,-0.10396999126293394,0.649611972331724,0.41905202815209286,0.38389721627408996,0.38389721627408996,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Require below-MA200 pullback entries to clear a volatility-scaled rebound threshold.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_45_45,pullback_quality_45_45,10.0,2017-01-03,2026-04-17,14.251832169123691,0.34108867606326365,-0.3156968903139078,0.3055415935589026,1.1166009139485271,1.004290420125621,0.6504731920143915,3.7806767915069313,0.7908829841194385,-0.10996417698118077,0.6473720494509809,0.41769056883769484,0.38389721627408996,0.38389721627408996,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Require below-MA200 pullback entries to clear a volatility-scaled rebound threshold.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_35_55,pullback_quality_35_55,0.0,2017-01-03,2026-04-17,15.393509506117365,0.3515566230232474,-0.31395121286197125,0.30898626776444865,1.1327617348291679,1.0149360749251384,0.6857818274036637,3.8991595399587147,0.797065211449633,-0.0887648103273283,0.6690329264842296,0.4307050041637204,0.37918629550321203,0.38860813704496794,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 35% QQQ + 55% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_35_55,pullback_quality_35_55,5.0,2017-01-03,2026-04-17,15.099505014192268,0.3489246630338778,-0.3148773371145298,0.3089915980009964,1.1264203242160762,1.0150358579437067,0.6762947514861108,3.8991595399587147,0.7938335556007745,-0.09504102668553649,0.6665226982490062,0.42924811724920486,0.37918629550321203,0.38860813704496794,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 35% QQQ + 55% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_35_55,pullback_quality_35_55,10.0,2017-01-03,2026-04-17,14.810647898441598,0.34629667863904046,-0.3158026275175737,0.3089996470831744,1.1200692769601903,1.015135640962275,0.6667926963118298,3.8991595399587147,0.7906062603165596,-0.10127672027885781,0.6640152080932209,0.4277920923206533,0.37918629550321203,0.38860813704496794,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 35% QQQ + 55% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_30_60,pullback_quality_30_60,0.0,2017-01-03,2026-04-17,15.69407228907178,0.3542041098467461,-0.3140041767559124,0.3108356049855403,1.134148298955269,1.0203584860728723,0.6933231461518927,3.9584009141846064,0.7969264755855816,-0.08437876995653182,0.677640669041486,0.4358524911293351,0.37683083511777304,0.3909635974304069,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 30% QQQ + 60% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_30_60,pullback_quality_30_60,5.0,2017-01-03,2026-04-17,15.390165639935173,0.35152692598370705,-0.3149302533516919,0.3108408829060965,1.127749303309001,1.020458368726737,0.6837577042782873,3.9584009141846064,0.7936950068021431,-0.09077742936706257,0.6749945642760478,0.4343478221159167,0.37683083511777304,0.3909635974304069,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 30% QQQ + 60% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_quality_30_60,pullback_quality_30_60,10.0,2017-01-03,2026-04-17,15.09166330109111,0.3488538777546897,-0.31585549611940644,0.31084887394285904,1.121340737716818,1.0205582513806026,0.6741772725060873,3.9584009141846064,0.7904678984151212,-0.097134101002604,0.6723515749655453,0.4328441008538617,0.37683083511777304,0.3909635974304069,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,"Quality rebound pullback gate, then use 30% QQQ + 60% TQQQ.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_35_55,pullback_aggressive_35_55,0.0,2017-01-03,2026-04-17,15.1369670622271,0.34926240007409093,-0.31395121286197125,0.3090366050590408,1.127112214647079,1.0152037333412163,0.677503602112014,4.093040401061634,0.797065211449633,-0.1030247528021333,0.6690329264842296,0.4307050041637204,0.3793361884368308,0.38884368308351186,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 35% QQQ + 55% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_35_55,pullback_aggressive_35_55,5.0,2017-01-03,2026-04-17,14.833190533353362,0.3465033032849061,-0.3148773371145298,0.3090448539093554,1.120447056868861,1.0153433304450823,0.6675602278198306,4.093040401061634,0.7938335556007745,-0.11001065953737277,0.6665226982490062,0.42924811724920486,0.3793361884368308,0.38884368308351186,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 35% QQQ + 55% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_35_55,pullback_aggressive_35_55,10.0,2017-01-03,2026-04-17,14.535002990068875,0.34374864135171257,-0.3158026275175737,0.30905595741614983,1.1137719672162878,1.0154829275489485,0.6576007671614983,4.093040401061634,0.7906062603165596,-0.11694517976782925,0.6640152080932209,0.4277920923206533,0.3793361884368308,0.38884368308351186,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 35% QQQ + 55% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_30_60,pullback_aggressive_30_60,0.0,2017-01-03,2026-04-17,15.419203080467597,0.3517846287655775,-0.3140041767559124,0.31089064041726744,1.1282318212301274,1.020640102130919,0.6846610501929371,4.152281775287527,0.7969264755855816,-0.09945454526918829,0.677640669041486,0.4358524911293351,0.3769593147751606,0.391220556745182,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 30% QQQ + 60% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_30_60,pullback_aggressive_30_60,5.0,2017-01-03,2026-04-17,15.105673887472463,0.3489803263690696,-0.3149302533516919,0.3108989470673225,1.1215107114309693,1.0207797988700826,0.6746420495584761,4.152281775287527,0.7936950068021431,-0.10655922975764365,0.6749945642760478,0.4343478221159167,0.3769593147751606,0.391220556745182,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 30% QQQ + 60% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +pullback_aggressive_30_60,pullback_aggressive_30_60,10.0,2017-01-03,2026-04-17,14.797999299439565,0.3461806270679004,-0.31585549611940644,0.31091010171417777,1.1147797490815483,1.0209194956092449,0.6646069840150284,4.152281775287527,0.7904678984151212,-0.11361090690472175,0.6723515749655453,0.4328441008538617,0.3769593147751606,0.391220556745182,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,pullback_quality,Keep the current pullback gate but shift pullback weight to 30% QQQ + 60% TQQQ.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_quality_pullback_35_55,strong_trend_35_55_quality_pullback_35_55,0.0,2017-01-03,2026-04-17,16.66081159976356,0.3624403589703782,-0.32359978691731117,0.3218322355219969,1.1253281237013988,1.056067923024715,0.713346675340051,5.794883515187261,0.8754530820745572,-0.0953666968715835,0.7187846463946148,0.43326681376130227,0.3304925053533191,0.4373019271948609,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,Use stronger TQQQ in strong trends and only take quality rebound pullbacks at 35%/55%.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_quality_pullback_35_55,strong_trend_35_55_quality_pullback_35_55,5.0,2017-01-03,2026-04-17,16.19253988710452,0.3585024917941819,-0.3245483872703727,0.3218322989491125,1.1163072835802346,1.0561512113220461,0.6998342245588494,5.794883515187261,0.8702348262616191,-0.1016424320584175,0.7153410303252596,0.43021941184644796,0.3304925053533191,0.4373019271948609,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,Use stronger TQQQ in strong trends and only take quality rebound pullbacks at 35%/55%.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_quality_pullback_35_55,strong_trend_35_55_quality_pullback_35_55,10.0,2017-01-03,2026-04-17,15.736544264725662,0.35457478570349443,-0.3254960696093727,0.3218350635542324,1.107277153503461,1.0562344996193778,0.6863061727152567,5.794883515187261,0.8650294876675104,-0.1078773180536724,0.7119032040872693,0.4271777890178854,0.3304925053533191,0.4373019271948609,0.23220556745182014,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,Use stronger TQQQ in strong trends and only take quality rebound pullbacks at 35%/55%.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_idle_25qqq,attack_40_50_idle_25qqq,0.0,2017-01-03,2026-04-17,16.656376767528897,0.36240350407924793,-0.34824071734308804,0.32460898748986106,1.1184043643427004,1.169009036371877,0.811225942839927,2.8705138602182245,0.8728371475877383,-0.185902317093812,0.7378148292489639,0.4562788252570422,0.3780299785867237,0.42676659528907923,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 40%/50% risk-on weights and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_idle_25qqq,attack_40_50_idle_25qqq,5.0,2017-01-03,2026-04-17,16.42246251948958,0.3604477953106715,-0.34844969437941076,0.32461774393388054,1.1139441458937618,1.1691097125847962,0.8036744893844568,2.8705138602182245,0.87041579949291,-0.1903802313102868,0.7361117372877073,0.4552710246362204,0.3780299785867237,0.42676659528907923,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 40%/50% risk-on weights and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_idle_25qqq,attack_40_50_idle_25qqq,10.0,2017-01-03,2026-04-17,16.191572570678584,0.3584942587148925,-0.34865867141573403,0.3246279145859968,1.109479334710249,1.1692103887977159,0.7961118008053084,2.8705138602182245,0.8679968000134464,-0.19483495683454,0.7344097582991931,0.45426359419588525,0.3780299785867237,0.42676659528907923,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 40%/50% risk-on weights and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_idle_25qqq,strong_trend_35_55_idle_25qqq,0.0,2017-01-03,2026-04-17,15.936640620090973,0.35630992169543885,-0.34283589300661665,0.3207717301179099,1.1139404265488089,1.154809777966846,0.7921004050005466,4.85240710704807,0.90391232166757,-0.18456659841658007,0.7500178799671615,0.4382769839466363,0.3720128479657388,0.43278372591006425,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 35%/55% only in strong trend stacks and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_idle_25qqq,strong_trend_35_55_idle_25qqq,5.0,2017-01-03,2026-04-17,15.559717512116297,0.35302595628593125,-0.3430811300474912,0.32077331721342517,1.1063564680437217,1.1548875132676633,0.7791399730897604,4.85240710704807,0.8995761852701623,-0.18909356009415546,0.7472606178379197,0.43564564293932584,0.3720128479657388,0.43278372591006425,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 35%/55% only in strong trend stacks and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +strong_trend_35_55_idle_25qqq,strong_trend_35_55_idle_25qqq,10.0,2017-01-03,2026-04-17,15.19110510891619,0.34974924468806967,-0.3433263449400966,0.32077644296973296,1.0987673141282994,1.154965248568482,0.7661660268624874,4.85240710704807,0.8952490387588283,-0.19359683668451477,0.7445070894411376,0.4330187121376021,0.3720128479657388,0.43278372591006425,0.195203426124197,0.8535331905781585,video_qqq_tqqq_growth_optimizations,next_close,combo,Use 35%/55% only in strong trend stacks and keep 25% QQQ while idle.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_quality_pullback_35_55_idle_25qqq,attack_40_50_quality_pullback_35_55_idle_25qqq,0.0,2017-01-03,2026-04-17,17.221519013096106,0.3670347946630763,-0.34824071734308804,0.32624185468200073,1.124842634959509,1.1742094479550604,0.8259889806588473,2.789730168092009,0.8726925731891118,-0.17106445363776124,0.7468110273544104,0.46153583817996235,0.375610278372591,0.4289079229122056,0.19548179871520344,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,"Moderately higher trend TQQQ, quality rebound pullbacks at 35%/55%, and 25% QQQ idle.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_quality_pullback_35_55_idle_25qqq,attack_40_50_quality_pullback_35_55_idle_25qqq,5.0,2017-01-03,2026-04-17,16.987000986588747,0.36512871704565764,-0.348449694379411,0.32624770847601936,1.1205385853871463,1.1742814691861172,0.8186925096500844,2.789730168092009,0.8702713650165357,-0.17516556190555033,0.7449712814691465,0.4604811240572244,0.375610278372591,0.4289079229122056,0.19548179871520344,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,"Moderately higher trend TQQQ, quality rebound pullbacks at 35%/55%, and 25% QQQ idle.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +attack_40_50_quality_pullback_35_55_idle_25qqq,attack_40_50_quality_pullback_35_55_idle_25qqq,10.0,2017-01-03,2026-04-17,16.75542793659606,0.36322469041196404,-0.34865867141573403,0.32625490334534857,1.1162301019671306,1.1743534904171733,0.811385512475574,2.789730168092009,0.8678525053690185,-0.1792476737983788,0.7431329107598819,0.45942684153702396,0.375610278372591,0.4289079229122056,0.19548179871520344,0.8531049250535332,video_qqq_tqqq_growth_optimizations,next_close,combo,"Moderately higher trend TQQQ, quality rebound pullbacks at 35%/55%, and 25% QQQ idle.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate. +QQQ_buy_hold,QQQ_buy_hold,0.0,2017-01-03,2026-04-17,4.476451721809586,0.20100531542336664,-0.35617218247976434,0.22810019577654167,0.9191176708957306,1.0000000000000004,,0.0,0.47565965852970193,-0.33070252607766726,0.5379299984978212,0.3113964491841523,1.0,0.0,0.0,0.0,reference,buy_hold,,Buy-and-hold QQQ.,Reference only. +QQQ_buy_hold,QQQ_buy_hold,5.0,2017-01-03,2026-04-17,4.476451721809586,0.20100531542336664,-0.35617218247976434,0.22810019577654167,0.9191176708957306,1.0000000000000004,,0.0,0.47565965852970193,-0.33070252607766726,0.5379299984978212,0.3113964491841523,1.0,0.0,0.0,0.0,reference,buy_hold,,Buy-and-hold QQQ.,Reference only. +QQQ_buy_hold,QQQ_buy_hold,10.0,2017-01-03,2026-04-17,4.476451721809586,0.20100531542336664,-0.35617218247976434,0.22810019577654167,0.9191176708957306,1.0000000000000004,,0.0,0.47565965852970193,-0.33070252607766726,0.5379299984978212,0.3113964491841523,1.0,0.0,0.0,0.0,reference,buy_hold,,Buy-and-hold QQQ.,Reference only. +TQQQ_buy_hold,TQQQ_buy_hold,0.0,2017-01-03,2026-04-17,21.10442918584464,0.3957768884847217,-0.8175454442813596,0.6749421732850229,0.8370716293635412,2.9557905559716007,0.7945309583245843,0.0,1.1005102229452417,-0.7919797991943727,1.9306358381502853,0.7900987920186318,0.0,1.0,0.0,1.0,reference,buy_hold,,Buy-and-hold TQQQ.,Reference only. +TQQQ_buy_hold,TQQQ_buy_hold,5.0,2017-01-03,2026-04-17,21.10442918584464,0.3957768884847217,-0.8175454442813596,0.6749421732850229,0.8370716293635412,2.9557905559716007,0.7945309583245843,0.0,1.1005102229452417,-0.7919797991943727,1.9306358381502853,0.7900987920186318,0.0,1.0,0.0,1.0,reference,buy_hold,,Buy-and-hold TQQQ.,Reference only. +TQQQ_buy_hold,TQQQ_buy_hold,10.0,2017-01-03,2026-04-17,21.10442918584464,0.3957768884847217,-0.8175454442813596,0.6749421732850229,0.8370716293635412,2.9557905559716007,0.7945309583245843,0.0,1.1005102229452417,-0.7919797991943727,1.9306358381502853,0.7900987920186318,0.0,1.0,0.0,1.0,reference,buy_hold,,Buy-and-hold TQQQ.,Reference only. diff --git a/research/results/video_qqq_tqqq_growth_optimizations_recommendation.json b/research/results/video_qqq_tqqq_growth_optimizations_recommendation.json new file mode 100644 index 0000000..e92a1d6 --- /dev/null +++ b/research/results/video_qqq_tqqq_growth_optimizations_recommendation.json @@ -0,0 +1,77 @@ +{ + "baseline": { + "strategy": "baseline_pullback_45_45", + "cagr": 0.3414544893929359, + "max_drawdown": -0.314771504640206, + "turnover_per_year": 3.9745576526098505, + "cagr_2023_plus": 0.41905202815209286 + }, + "best_cagr": { + "strategy": "attack_25_65", + "theme": "attack_weight", + "cagr": 0.4077959861515288, + "max_drawdown": -0.37439718027321944, + "turnover_per_year": 3.9745576526098505, + "cagr_2023_plus": 0.49967397543082726, + "average_qqq_weight": 0.21338329764453962, + "average_tqqq_weight": 0.5547965738758031 + }, + "best_near_baseline_drawdown": { + "strategy": "attack_30_60", + "theme": "attack_weight", + "cagr": 0.3915155866222344, + "max_drawdown": -0.35982863051214653, + "turnover_per_year": 3.9745576526098505, + "cagr_2023_plus": 0.479709960730206, + "average_qqq_weight": 0.2560599571734475, + "average_tqqq_weight": 0.512119914346895 + }, + "best_2023_plus": { + "strategy": "attack_25_65", + "theme": "attack_weight", + "cagr": 0.4077959861515288, + "max_drawdown": -0.37439718027321944, + "turnover_per_year": 3.9745576526098505, + "cagr_2023_plus": 0.49967397543082726, + "average_qqq_weight": 0.21338329764453962, + "average_tqqq_weight": 0.5547965738758031 + }, + "best_by_theme": { + "attack_weight": { + "strategy": "attack_25_65", + "cagr": 0.4077959861515288, + "max_drawdown": -0.37439718027321944, + "turnover_per_year": 3.9745576526098505, + "cagr_delta_vs_baseline": 0.06634149675859291 + }, + "combo": { + "strategy": "attack_40_50_quality_pullback_35_55_idle_25qqq", + "cagr": 0.36512871704565764, + "max_drawdown": -0.348449694379411, + "turnover_per_year": 2.789730168092009, + "cagr_delta_vs_baseline": 0.023674227652721758 + }, + "idle_exposure": { + "strategy": "idle_50qqq_positive_ma20", + "cagr": 0.3444028003599169, + "max_drawdown": -0.31477150464020587, + "turnover_per_year": 4.033799026835743, + "cagr_delta_vs_baseline": 0.002948310966981005 + }, + "pullback_quality": { + "strategy": "pullback_quality_30_60", + "cagr": 0.35152692598370705, + "max_drawdown": -0.3149302533516919, + "turnover_per_year": 3.9584009141846064, + "cagr_delta_vs_baseline": 0.010072436590771172 + } + }, + "findings": [ + "Baseline: 34.15% CAGR / -31.48% MaxDD / 41.91% 2023+ CAGR.", + "Best CAGR candidate: `attack_25_65` at 40.78% CAGR / -37.44% MaxDD.", + "Best candidate within 5pp worse MaxDD than baseline: `attack_30_60` at 39.15% CAGR / -35.98% MaxDD.", + "Best 2023+ candidate: `attack_25_65` at 49.97% 2023+ CAGR / -37.44% MaxDD.", + "The cleanest growth lever is shifting a moderate amount of QQQ into TQQQ during risk-on states. Idle QQQ exposure helps less, and stricter pullback quality gates mostly give up too much upside." + ], + "verdict": "The cleanest growth lever is shifting a moderate amount of QQQ into TQQQ during risk-on states. Idle QQQ exposure helps less, and stricter pullback quality gates mostly give up too much upside." +} \ No newline at end of file diff --git a/research/results/video_qqq_tqqq_growth_optimizations_summary.md b/research/results/video_qqq_tqqq_growth_optimizations_summary.md new file mode 100644 index 0000000..a773276 --- /dev/null +++ b/research/results/video_qqq_tqqq_growth_optimizations_summary.md @@ -0,0 +1,54 @@ +# Video QQQ/TQQQ Growth Optimization Follow-up + +## Setup +- Data: Nasdaq daily close/OHLC, not dividend-adjusted. +- Signal timing: next-close implementation; no same-close lookahead. +- Baseline: retained pullback reconstruction, 45% QQQ + 45% TQQQ + 10% cash. +- Goal: improve growth without treating lower drawdown as the primary objective. + +## Top CAGR Candidates +| strategy | theme | CAGR | Max Drawdown | 2020 Return | 2022 Return | 2023 Return | 2023+ CAGR | Turnover/Year | Average QQQ Weight | Average TQQQ Weight | Average Cash Weight | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| attack_25_65 | attack_weight | 0.407796 | -0.374397 | 0.984770 | -0.149308 | 0.800962 | 0.499674 | 3.974558 | 0.213383 | 0.554797 | 0.231820 | +| attack_30_60 | attack_weight | 0.391516 | -0.359829 | 0.937207 | -0.141152 | 0.762628 | 0.479710 | 3.974558 | 0.256060 | 0.512120 | 0.231820 | +| attack_35_55 | attack_weight | 0.375023 | -0.345035 | 0.889531 | -0.133101 | 0.724615 | 0.459610 | 3.974558 | 0.298737 | 0.469443 | 0.231820 | +| attack_40_50_quality_pullback_35_55_idle_25qqq | combo | 0.365129 | -0.348450 | 0.870271 | -0.175166 | 0.744971 | 0.460481 | 2.789730 | 0.375610 | 0.428908 | 0.195482 | +| attack_40_50_idle_25qqq | combo | 0.360448 | -0.348450 | 0.870416 | -0.190380 | 0.736112 | 0.455271 | 2.870514 | 0.378030 | 0.426767 | 0.195203 | +| strong_trend_35_55_quality_pullback_35_55 | combo | 0.358502 | -0.324548 | 0.870235 | -0.101642 | 0.715341 | 0.430219 | 5.794884 | 0.330493 | 0.437302 | 0.232206 | +| attack_40_50 | attack_weight | 0.358332 | -0.330017 | 0.841809 | -0.125159 | 0.686938 | 0.439386 | 3.974558 | 0.341413 | 0.426767 | 0.231820 | +| strong_trend_30_60 | attack_weight | 0.355458 | -0.329251 | 0.909043 | -0.126988 | 0.722027 | 0.420171 | 6.947398 | 0.311049 | 0.457131 | 0.231820 | +| strong_trend_35_55_idle_25qqq | combo | 0.353026 | -0.343081 | 0.899576 | -0.189094 | 0.747261 | 0.435646 | 4.852407 | 0.372013 | 0.432784 | 0.195203 | +| pullback_quality_30_60 | pullback_quality | 0.351527 | -0.314930 | 0.793695 | -0.090777 | 0.674995 | 0.434348 | 3.958401 | 0.376831 | 0.390964 | 0.232206 | + +## Top 2023+ CAGR Candidates +| strategy | theme | CAGR | Max Drawdown | 2020 Return | 2022 Return | 2023 Return | 2023+ CAGR | Turnover/Year | Average QQQ Weight | Average TQQQ Weight | Average Cash Weight | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| attack_25_65 | attack_weight | 0.407796 | -0.374397 | 0.984770 | -0.149308 | 0.800962 | 0.499674 | 3.974558 | 0.213383 | 0.554797 | 0.231820 | +| attack_30_60 | attack_weight | 0.391516 | -0.359829 | 0.937207 | -0.141152 | 0.762628 | 0.479710 | 3.974558 | 0.256060 | 0.512120 | 0.231820 | +| attack_40_50_quality_pullback_35_55_idle_25qqq | combo | 0.365129 | -0.348450 | 0.870271 | -0.175166 | 0.744971 | 0.460481 | 2.789730 | 0.375610 | 0.428908 | 0.195482 | +| attack_35_55 | attack_weight | 0.375023 | -0.345035 | 0.889531 | -0.133101 | 0.724615 | 0.459610 | 3.974558 | 0.298737 | 0.469443 | 0.231820 | +| attack_40_50_idle_25qqq | combo | 0.360448 | -0.348450 | 0.870416 | -0.190380 | 0.736112 | 0.455271 | 2.870514 | 0.378030 | 0.426767 | 0.195203 | +| idle_50qqq | idle_exposure | 0.343580 | -0.354790 | 0.842836 | -0.248163 | 0.746738 | 0.449425 | 1.987279 | 0.457323 | 0.384090 | 0.158587 | +| attack_40_50 | attack_weight | 0.358332 | -0.330017 | 0.841809 | -0.125159 | 0.686938 | 0.439386 | 3.974558 | 0.341413 | 0.426767 | 0.231820 | +| strong_trend_35_55_idle_25qqq | combo | 0.353026 | -0.343081 | 0.899576 | -0.189094 | 0.747261 | 0.435646 | 4.852407 | 0.372013 | 0.432784 | 0.195203 | +| idle_25qqq | idle_exposure | 0.343544 | -0.333675 | 0.821976 | -0.183135 | 0.697697 | 0.434713 | 2.870514 | 0.420707 | 0.384090 | 0.195203 | +| pullback_quality_30_60 | pullback_quality | 0.351527 | -0.314930 | 0.793695 | -0.090777 | 0.674995 | 0.434348 | 3.958401 | 0.376831 | 0.390964 | 0.232206 | + +## Best By Theme +| strategy | theme | CAGR | Max Drawdown | 2020 Return | 2022 Return | 2023 Return | 2023+ CAGR | Turnover/Year | Average QQQ Weight | Average TQQQ Weight | Average Cash Weight | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| attack_25_65 | attack_weight | 0.407796 | -0.374397 | 0.984770 | -0.149308 | 0.800962 | 0.499674 | 3.974558 | 0.213383 | 0.554797 | 0.231820 | +| attack_40_50_quality_pullback_35_55_idle_25qqq | combo | 0.365129 | -0.348450 | 0.870271 | -0.175166 | 0.744971 | 0.460481 | 2.789730 | 0.375610 | 0.428908 | 0.195482 | +| pullback_quality_30_60 | pullback_quality | 0.351527 | -0.314930 | 0.793695 | -0.090777 | 0.674995 | 0.434348 | 3.958401 | 0.376831 | 0.390964 | 0.232206 | +| idle_50qqq_positive_ma20 | idle_exposure | 0.344403 | -0.314772 | 0.794111 | -0.117330 | 0.649612 | 0.419052 | 4.033799 | 0.388158 | 0.384090 | 0.227752 | + +## Findings +- Baseline: 34.15% CAGR / -31.48% MaxDD / 41.91% 2023+ CAGR. +- Best CAGR candidate: `attack_25_65` at 40.78% CAGR / -37.44% MaxDD. +- Best candidate within 5pp worse MaxDD than baseline: `attack_30_60` at 39.15% CAGR / -35.98% MaxDD. +- Best 2023+ candidate: `attack_25_65` at 49.97% 2023+ CAGR / -37.44% MaxDD. +- The cleanest growth lever is shifting a moderate amount of QQQ into TQQQ during risk-on states. Idle QQQ exposure helps less, and stricter pullback quality gates mostly give up too much upside. + +## Caveats +- Nasdaq close data is not adjusted for dividends, so absolute CAGR should be compared mainly within this study. +- This is a research-only experiment; no live allocation code was changed. diff --git a/research/results/video_qqq_tqqq_position_scaling_comparison.csv b/research/results/video_qqq_tqqq_position_scaling_comparison.csv new file mode 100644 index 0000000..92ff9cc --- /dev/null +++ b/research/results/video_qqq_tqqq_position_scaling_comparison.csv @@ -0,0 +1,13 @@ +strategy,display_name,cost_bps_one_way,Start,End,Total Return,CAGR,Max Drawdown,Volatility,Sharpe,Beta vs QQQ,Information Ratio vs QQQ,Turnover/Year,2020 Return,2022 Return,2023 Return,2023+ CAGR,Average QQQ Weight,Average TQQQ Weight,Average Cash Weight,TQQQ Days Share,family,execution_mode,signal_mode,scaling,description,known_limitation,Average Scale While Invested +trend_only_baseline,trend_only_baseline,5.0,2017-01-03,2026-04-17,9.55951260612942,0.28901600162263485,-0.3134259994470624,0.29025973910445996,1.022999615550462,0.9054620173031639,0.4256089114256638,2.617391624889413,0.7976335440545881,-0.20969222435072288,0.4943848430109121,0.32503696003179217,0.3626980728051392,0.3626980728051392,0.2746038543897216,0.8059957173447537,video_qqq_tqqq_position_scaling,next_close,trend_only,baseline,Above-MA200 dual-drive state machine without below-MA200 pullback entry. No in-position scaling; keep 45% QQQ + 45% TQQQ + 10% cash while risk-on.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,1.0 +trend_only_ma60_half,trend_only_ma60_half,5.0,2017-01-03,2026-04-17,6.521384408268618,0.24276046050756794,-0.2654190067226184,0.26195943456016346,0.9636226806315576,0.8019226793349825,0.22178500051541988,4.847021527572988,0.7224785084018832,-0.1755963438288578,0.43942032903957706,0.2914133927834208,0.3626980728051392,0.33417558886509646,0.30312633832976443,0.8059957173447537,video_qqq_tqqq_position_scaling,next_close,trend_only,ma60_half,Above-MA200 dual-drive state machine without below-MA200 pullback entry. Cut TQQQ by 50% while risk-on when QQQ closes below MA60; move freed weight to cash.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9213184476342371 +trend_only_ma20_gap_trim_only,trend_only_ma20_gap_trim_only,5.0,2017-01-03,2026-04-17,7.366520185586255,0.2570969526241875,-0.2960575461423124,0.25505327534564654,1.027576814186094,0.7818580103961671,0.2774340319393968,6.2284226629312895,0.8397355781708535,-0.181566049085519,0.4388618931066104,0.2682468440872907,0.3626980728051392,0.3292130620985011,0.30808886509635974,0.8059957173447537,video_qqq_tqqq_position_scaling,next_close,trend_only,ma20_gap_trim_only,"Above-MA200 dual-drive state machine without below-MA200 pullback entry. Scale TQQQ to 50% when QQQ is 2%+ below MA20, 75% when below MA20, otherwise baseline.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9076289207868156 +trend_only_ma20_gap_trim_boost,trend_only_ma20_gap_trim_boost,5.0,2017-01-03,2026-04-17,7.310136998391682,0.2561816908345118,-0.3034214845739437,0.2622263660313889,1.0038448391121833,0.8006827555730331,0.2768015604540526,7.631635395163668,0.8582701355423219,-0.18716778426702785,0.45642185571919436,0.2682725575166731,0.3626980728051392,0.339186295503212,0.29811563169164884,0.8059957173447537,video_qqq_tqqq_position_scaling,next_close,trend_only,ma20_gap_trim_boost,"Above-MA200 dual-drive state machine without below-MA200 pullback entry. Same MA20 trim, plus boost TQQQ to 115% of baseline when QQQ is 3%+ above MA20.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9350611376927166 +trend_only_trend_score_4,trend_only_trend_score_4,5.0,2017-01-03,2026-04-17,8.456217543550236,0.27378484629881283,-0.2879041009453024,0.27855444994600126,1.0111500228879813,0.8601045663720543,0.35950681019616065,5.394734960188736,0.8755857375768146,-0.18449830910598186,0.4684616168352005,0.282043873703407,0.3626980728051392,0.36191755888650967,0.27538436830835117,0.8059957173447537,video_qqq_tqqq_position_scaling,next_close,trend_only,trend_score_4,"Above-MA200 dual-drive state machine without below-MA200 pullback entry. Use close>MA20, MA20>MA60, positive MA20 slope, close>MA200 to scale TQQQ in steps.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.997846889952153 +pullback_baseline,pullback_baseline,5.0,2017-01-03,2026-04-17,14.29050022544996,0.3414544893929359,-0.314771504640206,0.3055774809882369,1.1173997222836356,1.0044703935950823,0.6518531322179977,3.9745576526098505,0.7941106531980329,-0.11733016502096338,0.649611972331724,0.41905202815209286,0.38408993576017136,0.38408993576017136,0.23182012847965736,0.8535331905781585,video_qqq_tqqq_position_scaling,next_close,pullback,baseline,Above-MA200 dual-drive plus the retained below-MA200 pullback state. No in-position scaling; keep 45% QQQ + 45% TQQQ + 10% cash while risk-on.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,1.0 +pullback_ma60_half,pullback_ma60_half,5.0,2017-01-03,2026-04-17,8.90717776292249,0.2801927187810227,-0.25688956596243384,0.2748360067090383,1.0392193767645084,0.8860551691306676,0.4039599630116453,6.107247124741964,0.7202453193632639,-0.1282114197386739,0.5607574102128505,0.36504314883010447,0.38408993576017136,0.35248394004282657,0.2634261241970022,0.8535331905781585,video_qqq_tqqq_position_scaling,next_close,pullback,ma60_half,Above-MA200 dual-drive plus the retained below-MA200 pullback state. Cut TQQQ by 50% while risk-on when QQQ closes below MA60; move freed weight to cash.,Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9176706827309237 +pullback_ma20_gap_trim_only,pullback_ma20_gap_trim_only,5.0,2017-01-03,2026-04-17,11.114979502906246,0.3082369408735841,-0.26239564028965345,0.2723809213135765,1.1256420659991158,0.8808663866880855,0.5215316944737974,7.585588690651726,0.8361301783560506,-0.08591692672967466,0.588321653890878,0.358232495081783,0.38408993576017136,0.35060492505353325,0.26530513918629556,0.8535331905781585,video_qqq_tqqq_position_scaling,next_close,pullback,ma20_gap_trim_only,"Above-MA200 dual-drive plus the retained below-MA200 pullback state. Scale TQQQ to 50% when QQQ is 2%+ below MA20, 75% when below MA20, otherwise baseline.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9127761044176707 +pullback_ma20_gap_trim_boost,pullback_ma20_gap_trim_boost,5.0,2017-01-03,2026-04-17,11.178052159855154,0.30896885593264645,-0.26503966898324416,0.28137580111303667,1.1005719589723935,0.9061577924222137,0.5206459350245751,9.163294197876732,0.8542587137364817,-0.09281829484805915,0.6197718425807883,0.36704458751949964,0.38408993576017136,0.3628329764453962,0.2530770877944325,0.8535331905781585,video_qqq_tqqq_position_scaling,next_close,pullback,ma20_gap_trim_boost,"Above-MA200 dual-drive plus the retained below-MA200 pullback state. Same MA20 trim, plus boost TQQQ to 115% of baseline when QQQ is 3%+ above MA20.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9445532128514057 +pullback_trend_score_4,pullback_trend_score_4,5.0,2017-01-03,2026-04-17,11.677674489328865,0.3146500075330574,-0.2856852755886097,0.29020976162701434,1.091021058690767,0.944218053719343,0.5487894967822978,6.654960557357713,0.8725320340504596,-0.12618638530426296,0.5922413733968928,0.35601129383979435,0.38408993576017136,0.3795032119914347,0.236406852248394,0.8535331905781585,video_qqq_tqqq_position_scaling,next_close,pullback,trend_score_4,"Above-MA200 dual-drive plus the retained below-MA200 pullback state. Use close>MA20, MA20>MA60, positive MA20 slope, close>MA200 to scale TQQQ in steps.",Nasdaq close data is not dividend-adjusted; exact live state machine remains approximate.,0.9880522088353413 +QQQ_buy_hold,QQQ_buy_hold,5.0,2017-01-03,2026-04-17,4.476451721809586,0.20100531542336664,-0.35617218247976434,0.22810019577654167,0.9191176708957306,1.0000000000000004,,0.0,0.47565965852970193,-0.33070252607766726,0.5379299984978212,0.3113964491841523,1.0,0.0,0.0,0.0,reference,buy_hold,,,Buy-and-hold QQQ.,Reference only., +TQQQ_buy_hold,TQQQ_buy_hold,5.0,2017-01-03,2026-04-17,21.10442918584464,0.3957768884847217,-0.8175454442813596,0.6749421732850229,0.8370716293635412,2.9557905559716007,0.7945309583245843,0.0,1.1005102229452417,-0.7919797991943727,1.9306358381502853,0.7900987920186318,0.0,1.0,0.0,1.0,reference,buy_hold,,,Buy-and-hold TQQQ.,Reference only., diff --git a/research/results/video_qqq_tqqq_position_scaling_recommendation.json b/research/results/video_qqq_tqqq_position_scaling_recommendation.json new file mode 100644 index 0000000..07d69ca --- /dev/null +++ b/research/results/video_qqq_tqqq_position_scaling_recommendation.json @@ -0,0 +1,24 @@ +{ + "baseline_pullback": { + "strategy": "pullback_baseline", + "cagr": 0.3414544893929359, + "max_drawdown": -0.314771504640206, + "turnover_per_year": 3.9745576526098505, + "average_tqqq_weight": 0.38408993576017136 + }, + "best_scaled_pullback_candidate": { + "strategy": "pullback_trend_score_4", + "scaling": "trend_score_4", + "cagr": 0.3146500075330574, + "max_drawdown": -0.2856852755886097, + "turnover_per_year": 6.654960557357713, + "average_tqqq_weight": 0.3795032119914347, + "average_scale_while_invested": 0.9880522088353413 + }, + "findings": [ + "Pullback baseline: 34.15% CAGR / -31.48% MaxDD / turnover 3.97/yr.", + "Best scaled pullback candidate: `trend_score_4` at 31.47% CAGR / -28.57% MaxDD / turnover 6.65/yr.", + "Scaling smooths drawdown, but it lowers CAGR and adds turnover; this is not a clear upgrade." + ], + "verdict": "Scaling smooths drawdown, but it lowers CAGR and adds turnover; this is not a clear upgrade." +} \ No newline at end of file diff --git a/research/results/video_qqq_tqqq_position_scaling_summary.md b/research/results/video_qqq_tqqq_position_scaling_summary.md new file mode 100644 index 0000000..1c2ecae --- /dev/null +++ b/research/results/video_qqq_tqqq_position_scaling_summary.md @@ -0,0 +1,30 @@ +# Video QQQ/TQQQ Position-Scaling Follow-up + +## Setup +- Data: Nasdaq daily close/OHLC, not dividend-adjusted. +- Signal timing: next-close implementation; no same-close lookahead. +- Baseline risk-on sleeve: 45% QQQ + 45% TQQQ + 10% cash. +- Scaling changes only the TQQQ sleeve while already in a risk-on or pullback-risk-on state. + +## 5 bps Comparison +| strategy | signal_mode | scaling | CAGR | Max Drawdown | 2020 Return | 2022 Return | 2023 Return | 2023+ CAGR | Turnover/Year | Average QQQ Weight | Average TQQQ Weight | Average Cash Weight | Average Scale While Invested | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| pullback_baseline | pullback | baseline | 0.341454 | -0.314772 | 0.794111 | -0.117330 | 0.649612 | 0.419052 | 3.974558 | 0.384090 | 0.384090 | 0.231820 | 1.000000 | +| pullback_trend_score_4 | pullback | trend_score_4 | 0.314650 | -0.285685 | 0.872532 | -0.126186 | 0.592241 | 0.356011 | 6.654961 | 0.384090 | 0.379503 | 0.236407 | 0.988052 | +| pullback_ma20_gap_trim_boost | pullback | ma20_gap_trim_boost | 0.308969 | -0.265040 | 0.854259 | -0.092818 | 0.619772 | 0.367045 | 9.163294 | 0.384090 | 0.362833 | 0.253077 | 0.944553 | +| pullback_ma20_gap_trim_only | pullback | ma20_gap_trim_only | 0.308237 | -0.262396 | 0.836130 | -0.085917 | 0.588322 | 0.358232 | 7.585589 | 0.384090 | 0.350605 | 0.265305 | 0.912776 | +| pullback_ma60_half | pullback | ma60_half | 0.280193 | -0.256890 | 0.720245 | -0.128211 | 0.560757 | 0.365043 | 6.107247 | 0.384090 | 0.352484 | 0.263426 | 0.917671 | +| trend_only_baseline | trend_only | baseline | 0.289016 | -0.313426 | 0.797634 | -0.209692 | 0.494385 | 0.325037 | 2.617392 | 0.362698 | 0.362698 | 0.274604 | 1.000000 | +| trend_only_trend_score_4 | trend_only | trend_score_4 | 0.273785 | -0.287904 | 0.875586 | -0.184498 | 0.468462 | 0.282044 | 5.394735 | 0.362698 | 0.361918 | 0.275384 | 0.997847 | +| trend_only_ma20_gap_trim_only | trend_only | ma20_gap_trim_only | 0.257097 | -0.296058 | 0.839736 | -0.181566 | 0.438862 | 0.268247 | 6.228423 | 0.362698 | 0.329213 | 0.308089 | 0.907629 | +| trend_only_ma20_gap_trim_boost | trend_only | ma20_gap_trim_boost | 0.256182 | -0.303421 | 0.858270 | -0.187168 | 0.456422 | 0.268273 | 7.631635 | 0.362698 | 0.339186 | 0.298116 | 0.935061 | +| trend_only_ma60_half | trend_only | ma60_half | 0.242760 | -0.265419 | 0.722479 | -0.175596 | 0.439420 | 0.291413 | 4.847022 | 0.362698 | 0.334176 | 0.303126 | 0.921318 | + +## Findings +- Pullback baseline: 34.15% CAGR / -31.48% MaxDD / turnover 3.97/yr. +- Best scaled pullback candidate: `trend_score_4` at 31.47% CAGR / -28.57% MaxDD / turnover 6.65/yr. +- Scaling smooths drawdown, but it lowers CAGR and adds turnover; this is not a clear upgrade. + +## Caveats +- Nasdaq close data is not adjusted for dividends, so absolute CAGR is not a replacement for the retained Yahoo adjusted reference. +- This is a research-only experiment; no live allocation code was changed.