Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/us_equity_strategy_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ See the Chinese handbook for localized positioning text and default parameter ta
| `tech_communication_pullback_enhancement` | research_only | Underperforms `russell_top50_leader_rotation` on return and drawdown. |
| `QQQ` / `SPY` LEAPS growth overlay | research_only | Design: [`research/index_leaps_growth_overlay.md`](./research/index_leaps_growth_overlay.md). Proxy backtest module: `UsEquitySnapshotPipelines/docs/index-leaps-growth-overlay-research.md`. |
| `us_equity_combo_leveraged_shadow_352045` | shadow_candidate | TQQQ 35% / SOXL 20% / BOXX 45% with `hard_defense_risk_exposure=0.0`. Backtest evidence: 22.21% CAGR, -33.88% max drawdown over 2010-01-05 to 2026-07-02 with 5 bps cost. Runtime config: `src/us_equity_strategies/configs/us_equity_combo_leveraged_shadow_352045.json`. |
| `us_equity_combo_leveraged_shadow_402040` | shadow_candidate | TQQQ 40% / SOXL 20% / BOXX 40% with `hard_defense_risk_exposure=0.0`. Backtest evidence: 23.81% CAGR, -35.53% max drawdown over 2010-01-05 to 2026-07-02 with 5 bps cost. Runtime config: `src/us_equity_strategies/configs/us_equity_combo_leveraged_shadow_402040.json`. |
| `crisis_response_shadow` plugin | shadow candidate | Defense-only observation; no allocation impact. |

## Promotion principles
Expand Down
2 changes: 2 additions & 0 deletions docs/us_equity_strategy_status.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ _更新日期:2026-06-28_
| `global_etf_confidence_vol_gate` production-like 研究 | 2015-01-05 至 2026-05-06 | 14.77% | -23.35% | 相比同口径 Top2/SMA250 的 13.60% CAGR、-23.35% 回撤,收益和 Sharpe 改善;仍未跑赢 QQQ 长期 CAGR,因此只作为 Global ETF 自身增强候选。 | [`docs/research/global_etf_confidence_vol_gate.md`](./research/global_etf_confidence_vol_gate.md) |
| Crisis unified response historical research,含旧 5% 反弹袖子 | 1999-03-10 至 2026-04-16 | 23.89% | -56.04% | 相比合成 TQQQ 基线显著降低 2000/2008 级别灾难回撤;但该历史版本包含旧反弹袖子,不等于当前 defense-only shadow plugin。 | `UsEquitySnapshotPipelines/data/output/crisis_response_audit_trial/external_fragility10_severe10_fin_credit/summary.csv` |
| `us_equity_combo_leveraged_shadow_352045`,5 bps 单边成本 | 2010-01-05 至 2026-07-02 | 22.21% | -33.88% | `TQQQ 35% / SOXL 20% / BOXX 45%`,`hard_defense_risk_exposure=0`;最大回撤进入 35%-40% 可接受区间,但收益/风险仍弱于单腿最强 proxy,因此只进入 shadow 观察,不替换 live。 | `/tmp/us_combo_core_sweep_20260704/ranking.csv`;运行配置见 `src/us_equity_strategies/configs/us_equity_combo_leveraged_shadow_352045.json` |
| `us_equity_combo_leveraged_shadow_402040`,5 bps 单边成本 | 2010-01-05 至 2026-07-02 | 23.81% | -35.53% | `TQQQ 40% / SOXL 20% / BOXX 40%`,`hard_defense_risk_exposure=0`;在 35%-40% 回撤预算下收益、Sharpe、Calmar 均优于 35/20/45,适合作为下一轮 shadow 观察配置。 | `/tmp/us_combo_rule_compare_20260704/ranking_rule_compare.csv`;运行配置见 `src/us_equity_strategies/configs/us_equity_combo_leveraged_shadow_402040.json` |

暂时没有写进正式表的内容:

Expand All @@ -76,6 +77,7 @@ _更新日期:2026-06-28_
| --- | --- | --- |
| `tecl_xlk_trend_income` | `research_enabled` | 重叠窗口未跑赢 live TQQQ / SOXL(2024+ CAGR 24.8%,最大回撤 -46.0%);保留策略实现与回测入口,不进入 runtime。研究文档见 [`UsEquitySnapshotPipelines/docs/tecl-xlk-optimization-research.zh-CN.md`](../../UsEquitySnapshotPipelines/docs/tecl-xlk-optimization-research.zh-CN.md)。 |
| `us_equity_combo_leveraged_shadow_352045` | shadow candidate | 组合型杠杆候选:`TQQQ 35% / SOXL 20% / BOXX 45%`,risk-off 归零风险腿转 `BOXX`。当前仅提供受控 runtime_config 与测试保护,不改变默认 live;需要平台 dry-run / shadow 周期证据后才可重新评估 live。 |
| `us_equity_combo_leveraged_shadow_402040` | shadow candidate | 组合型杠杆候选:`TQQQ 40% / SOXL 20% / BOXX 40%`,risk-off 归零风险腿转 `BOXX`。这是 35%-40% 回撤预算下的下一轮 shadow 配置,不直接替换 live。 |
| `crisis_response_shadow` 插件 | 可作为 `tqqq_growth_income` 的 `shadow` 插件候选,只写信号、日志和通知上下文。 | 现在是 defense-only 黑天鹅观察流,不下单、不改 allocation;需要稳定 shadow 日志后再做 evidence review。 |
| 事件反弹 / MAGS 路线 | 保持 research-only,不作为运行策略 profile。 | 对 MAGS 的正贡献不稳定,且事件反弹预算不应该混进黑天鹅逃命插件。 |
| `QQQ` / `SPY` LEAPS 增长增强层 | 已有 option overlay 意图框架;组合型 live profile 默认带 `option_*` 设置,但 `spy_leaps_growth_v1` / `qqq_leaps_growth_v1` 等 recipe 仍是 research candidate,当前会以 `research_only_recipe` 跳过,不产生真实订单意图,研究设计见 [`docs/research/index_leaps_growth_overlay.zh-CN.md`](./research/index_leaps_growth_overlay.zh-CN.md)([English](./research/index_leaps_growth_overlay.md));代理回测见 [`UsEquitySnapshotPipelines/docs/index-leaps-growth-overlay-research.md`](../../UsEquitySnapshotPipelines/docs/index-leaps-growth-overlay-research.md)。 | 属于有限权利金预算的增长增强层,不是当前低回撤收入层的直接替代;需要真实期权链回测后才能把 recipe 晋级为 live。 |
Expand Down
5 changes: 4 additions & 1 deletion src/us_equity_strategies/combo_entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,11 @@ def evaluate_us_equity_combo_leveraged(ctx: StrategyContext) -> StrategyDecision
risk_flags: tuple[str, ...] = ()
if has_cash:
risk_flags += ("cash_parked",)
if not metadata.get("spy_above_ma200", True):
regime_state = str(metadata.get("regime_state") or "")
if regime_state == "hard_defense":
risk_flags += ("ma200_risk_off",)
elif regime_state == "soft_defense":
risk_flags += ("soft_defense",)
budgets = _build_budgets(diagnostics)
return StrategyDecision(
positions=_weights_to_positions(weights),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
},
"required_market_data": {
"market_data": {
"spy_above_ma200": "boolean"
"spy_above_ma200": "boolean",
"qqq_above_ma200": "boolean",
"soxx_above_ma200": "boolean",
"spy_ma20_slope_positive": "boolean",
"qqq_ma20_slope_positive": "boolean",
"soxx_ma20_slope_positive": "boolean"
}
},
"backtest_evidence": {
Expand All @@ -32,6 +37,7 @@
},
"notes": [
"This config does not change the strategy default or any live deployment.",
"Backtest dynamic mode uses SPY/QQQ/SOXX MA200 hard-defense inputs; runtime market_data must provide the same regime fields.",
"Risk-off behavior requires hard_defense_risk_exposure=0.0; otherwise the strategy retains the default 50% risk-leg exposure.",
"The backtest artifact is local research output and should be regenerated into a durable repo or CI artifact before live promotion."
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"role": "us_equity_combo_leveraged_shadow",
"status": "shadow_candidate",
"strategy_profile": "us_equity_combo_leveraged",
"name": "us_equity_combo_leveraged_shadow_402040",
"description": "TQQQ 40% + SOXL 20% + BOXX 40% dynamic shadow candidate with full BOXX hard defense.",
"runtime_config": {
"tqqq_weight": 0.4,
"soxl_weight": 0.2,
"boxx_weight": 0.4,
"dynamic": true,
"hard_defense_risk_exposure": 0.0
},
"required_market_data": {
"market_data": {
"spy_above_ma200": "boolean",
"qqq_above_ma200": "boolean",
"soxx_above_ma200": "boolean",
"spy_ma20_slope_positive": "boolean",
"qqq_ma20_slope_positive": "boolean",
"soxx_ma20_slope_positive": "boolean"
}
},
"backtest_evidence": {
"artifact": "/tmp/us_combo_rule_compare_20260704/ranking_rule_compare.csv",
"window": "2010-01-05 to 2026-07-02",
"turnover_cost_bps": 5,
"full_cagr": 0.238097,
"max_drawdown": -0.35531,
"sharpe": 0.911368,
"calmar": 0.67011,
"rolling_1y_worst_mdd": -0.35531,
"rolling_3y_worst_mdd": -0.35531
},
"promotion_state": {
"live_enable_candidate": false,
"shadow_ready": true,
"promotion_gate": "requires platform-level dry-run evidence before live"
},
"notes": [
"This config does not change the strategy default or any live deployment.",
"Backtest dynamic mode uses SPY/QQQ/SOXX MA200 hard-defense inputs; runtime market_data must provide the same regime fields.",
"Risk-off behavior requires hard_defense_risk_exposure=0.0; otherwise the strategy retains the default 50% risk-leg exposure.",
"Candidate assumes user accepts an approximate 35-40% historical max drawdown budget."
]
}
91 changes: 75 additions & 16 deletions src/us_equity_strategies/strategies/us_equity_combo_leveraged.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@

Dynamic mode
------------
SPY 200-day MA regime signal:
- SPY above MA200 (bull): normal weights
- SPY below MA200 (bear): risk legs retain 50% by default, BOXX receives the rest
Multi-asset 200-day MA regime signal:
- SPY/QQQ/SOXX all above MA200: normal weights
- any of SPY/QQQ/SOXX below MA200: risk legs retain 50% by default, BOXX receives the rest
- if only 20-day MA slope weakens, keep current weights and mark soft defense
"""

from __future__ import annotations
Expand All @@ -33,6 +34,8 @@
DEFAULT_BOXX_WEIGHT = 0.40
DEFAULT_REBALANCE_THRESHOLD = 0.05 # 5% drift triggers rebalance
DEFAULT_HARD_DEFENSE_RISK_EXPOSURE = 0.50
DEFAULT_SOFT_DEFENSE_RISK_EXPOSURE = 1.00
REGIME_SYMBOLS = ("SPY", "QQQ", "SOXX")


def _noop_logger(_message: str) -> None:
Expand Down Expand Up @@ -80,7 +83,13 @@ def load_runtime_parameters(
raise ValueError("Runtime strategy config runtime_config must be an object")

runtime_config = dict(raw_runtime_config)
for key in ("tqqq_weight", "soxl_weight", "boxx_weight", "hard_defense_risk_exposure"):
for key in (
"tqqq_weight",
"soxl_weight",
"boxx_weight",
"hard_defense_risk_exposure",
"soft_defense_risk_exposure",
):
if key not in runtime_config:
continue
value = float(runtime_config[key])
Expand All @@ -91,6 +100,8 @@ def load_runtime_parameters(
runtime_config[key] = value
if "hard_defense_risk_exposure" in runtime_config and float(runtime_config["hard_defense_risk_exposure"]) > 1.0:
raise ValueError("Runtime strategy config hard_defense_risk_exposure must be in [0, 1]")
if "soft_defense_risk_exposure" in runtime_config and float(runtime_config["soft_defense_risk_exposure"]) > 1.0:
raise ValueError("Runtime strategy config soft_defense_risk_exposure must be in [0, 1]")
if all(key in runtime_config for key in ("tqqq_weight", "soxl_weight", "boxx_weight")):
total = sum(float(runtime_config[key]) for key in ("tqqq_weight", "soxl_weight", "boxx_weight"))
if total <= 0.0:
Expand All @@ -103,6 +114,37 @@ def load_runtime_parameters(
return runtime_config


def _market_bool(market_data: dict[str, Any], key: str, default: bool) -> bool:
value = market_data.get(key, default)
return bool(value)


def _resolve_regime(market_data: dict[str, Any] | None) -> dict[str, object]:
raw = market_data if isinstance(market_data, dict) else {}
above_ma200 = {
symbol: _market_bool(raw, f"{symbol.lower()}_above_ma200", True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the new regime inputs before defaulting risk-on

When the platform sends the prior SPY-only market_data (the manifest/entrypoint still only require the generic market_data input), this True fallback marks any missing QQQ/SOXX MA200 field as above trend. For the new 352045/402040 shadow configs that claim the backtest used SPY/QQQ/SOXX hard-defense inputs, a dry-run can therefore remain risk-on during a QQQ or SOXX MA200 break instead of failing fast or entering hard defense, invalidating the shadow observation.

Useful? React with 👍 / 👎.

for symbol in REGIME_SYMBOLS
}
ma20_slope_positive = {
symbol: _market_bool(raw, f"{symbol.lower()}_ma20_slope_positive", True)
for symbol in REGIME_SYMBOLS
}

if not all(above_ma200.values()):
regime_state = "hard_defense"
elif not all(ma20_slope_positive.values()):
regime_state = "soft_defense"
else:
regime_state = "risk_on"

return {
"regime_state": regime_state,
"above_ma200": above_ma200,
"ma20_slope_positive": ma20_slope_positive,
"spy_above_ma200": above_ma200["SPY"],
}


def build_target_weights(
market_data: dict[str, Any] | None = None,
config: dict[str, Any] | None = None,
Expand All @@ -113,15 +155,19 @@ def build_target_weights(
Parameters
----------
market_data : dict or None
Contains price/regime info. If it holds a 'spy_above_ma200' key,
dynamic mode uses it.
Contains price/regime info. Dynamic mode uses SPY/QQQ/SOXX
'<symbol>_above_ma200' and '<symbol>_ma20_slope_positive' keys when
available. Missing non-SPY keys default to risk-on for backwards
compatibility.
config : dict or None
Override configuration. Accepted keys:
- tqqq_weight, soxl_weight, boxx_weight (overrides defaults)
- dynamic (bool, default True)
- hard_defense_risk_exposure (float, default 0.50): retained
TQQQ/SOXL fraction when SPY is below MA200. Use 0.0 for a
full BOXX hard-defense shadow.
- soft_defense_risk_exposure (float, default 1.00): retained
TQQQ/SOXL fraction when MA20 slope weakens while MA200 stays intact.

Returns
-------
Expand All @@ -137,15 +183,23 @@ def build_target_weights(
1.0,
max(0.0, float(cfg.get("hard_defense_risk_exposure", DEFAULT_HARD_DEFENSE_RISK_EXPOSURE))),
)
soft_defense_risk_exposure = min(
1.0,
max(0.0, float(cfg.get("soft_defense_risk_exposure", DEFAULT_SOFT_DEFENSE_RISK_EXPOSURE))),
)

# Dynamic mode: SPY MA200 risk-off
spy_above_ma200 = True
if isinstance(market_data, dict):
spy_above_ma200 = bool(market_data.get("spy_above_ma200", True))
regime = _resolve_regime(market_data)
regime_state = str(regime["regime_state"])

if dynamic and not spy_above_ma200:
tqqq_weight *= hard_defense_risk_exposure
soxl_weight *= hard_defense_risk_exposure
if dynamic and regime_state == "hard_defense":
risk_exposure = hard_defense_risk_exposure
tqqq_weight *= risk_exposure
soxl_weight *= risk_exposure
boxx_weight = 1.0 - tqqq_weight - soxl_weight
elif dynamic and regime_state == "soft_defense":
risk_exposure = soft_defense_risk_exposure
tqqq_weight *= risk_exposure
soxl_weight *= risk_exposure
boxx_weight = 1.0 - tqqq_weight - soxl_weight

# Normalize
Expand All @@ -165,14 +219,18 @@ def build_target_weights(
"profile_name": PROFILE_NAME,
"signal_source": SIGNAL_SOURCE,
"status_icon": STATUS_ICON,
"spy_above_ma200": spy_above_ma200,
"regime_state": regime_state if dynamic else "risk_on",
"spy_above_ma200": regime["spy_above_ma200"],
"above_ma200": regime["above_ma200"],
"ma20_slope_positive": regime["ma20_slope_positive"],
"effective_weights": {
"TQQQ": tqqq_weight,
"SOXL": soxl_weight,
"BOXX": boxx_weight,
},
"dynamic": dynamic,
"hard_defense_risk_exposure": hard_defense_risk_exposure,
"soft_defense_risk_exposure": soft_defense_risk_exposure,
"rebalance": compute_portfolio_drift(
weights,
holdings=cfg.get("current_holdings_quantities", {}),
Expand Down Expand Up @@ -202,13 +260,14 @@ def compute_signals(
**kwargs,
)
ew = metadata.get("effective_weights", {})
regime_state = str(metadata.get("regime_state") or "risk_on")
spy_status = "MA200_up" if metadata.get("spy_above_ma200", True) else "MA200_down"
signal_desc = (
f"leveraged combo {spy_status} "
f"leveraged combo {regime_state} {spy_status} "
f"TQQQ={ew.get('TQQQ', 0):.0%} SOXL={ew.get('SOXL', 0):.0%} BOXX={ew.get('BOXX', 0):.0%}"
)
status_desc = (
f"{spy_status} | "
f"{regime_state} {spy_status} | "
f"TQQQ={ew.get('TQQQ', 0):.0%} SOXL={ew.get('SOXL', 0):.0%} BOXX={ew.get('BOXX', 0):.0%}"
)
has_cash = ew.get("BOXX", 0) > 0.5
Expand Down
74 changes: 74 additions & 0 deletions tests/test_us_equity_combo.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,49 @@ def test_us_equity_combo_leveraged_supports_zero_hard_defense_shadow() -> None:
assert metadata["hard_defense_risk_exposure"] == 0.0


def test_us_equity_combo_leveraged_uses_multi_asset_hard_defense() -> None:
weights, metadata = us_equity_combo_leveraged.build_target_weights(
market_data={
"spy_above_ma200": True,
"qqq_above_ma200": False,
"soxx_above_ma200": True,
},
config={
"tqqq_weight": 0.35,
"soxl_weight": 0.20,
"boxx_weight": 0.45,
"hard_defense_risk_exposure": 0.0,
},
)

assert weights == {"TQQQ": 0.0, "SOXL": 0.0, "BOXX": 1.0}
assert metadata["regime_state"] == "hard_defense"
assert metadata["above_ma200"]["QQQ"] is False


def test_us_equity_combo_leveraged_marks_soft_defense_without_changing_shadow_weights() -> None:
weights, metadata = us_equity_combo_leveraged.build_target_weights(
market_data={
"spy_above_ma200": True,
"qqq_above_ma200": True,
"soxx_above_ma200": True,
"spy_ma20_slope_positive": True,
"qqq_ma20_slope_positive": False,
"soxx_ma20_slope_positive": True,
},
config={
"tqqq_weight": 0.35,
"soxl_weight": 0.20,
"boxx_weight": 0.45,
"hard_defense_risk_exposure": 0.0,
},
)

assert weights == {"TQQQ": 0.35, "SOXL": 0.20, "BOXX": 0.45}
assert metadata["regime_state"] == "soft_defense"
assert metadata["ma20_slope_positive"]["QQQ"] is False


def test_us_equity_combo_leveraged_shadow_352045_config_matches_strategy_behavior() -> None:
config_path = (
Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -123,3 +166,34 @@ def test_us_equity_combo_leveraged_loads_package_runtime_config() -> None:
assert runtime_config["runtime_config_path"].startswith("package://us_equity_strategies/")
assert runtime_config["tqqq_weight"] == 0.35
assert runtime_config["hard_defense_risk_exposure"] == 0.0


def test_us_equity_combo_leveraged_loads_402040_shadow_config() -> None:
runtime_config = us_equity_combo_leveraged.load_runtime_parameters(
config_path=(
"package://us_equity_strategies/"
"configs/us_equity_combo_leveraged_shadow_402040.json"
),
logger=lambda _message: None,
)
risk_on_weights, _risk_on_metadata = us_equity_combo_leveraged.build_target_weights(
market_data={
"spy_above_ma200": True,
"qqq_above_ma200": True,
"soxx_above_ma200": True,
},
config=runtime_config,
)
hard_weights, hard_metadata = us_equity_combo_leveraged.build_target_weights(
market_data={
"spy_above_ma200": True,
"qqq_above_ma200": False,
"soxx_above_ma200": True,
},
config=runtime_config,
)

assert runtime_config["runtime_config_name"] == "us_equity_combo_leveraged_shadow_402040"
assert risk_on_weights == {"TQQQ": 0.4, "SOXL": 0.2, "BOXX": 0.4}
assert hard_weights == {"TQQQ": 0.0, "SOXL": 0.0, "BOXX": 1.0}
assert hard_metadata["regime_state"] == "hard_defense"
Loading