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
4 changes: 2 additions & 2 deletions docs/research/income_layer_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ SOXL core overlay review:

| Core version | CAGR | Max drawdown | Note |
| --- | ---: | ---: | --- |
| current manifest: `SOXX 10d vol >= 55%, SOXL -> SOXX` | `49.74%` | `-42.31%` | kept; best combined result after income layer |
| May 2026 manifest: `SOXX 10d vol >= 55%, SOXL -> SOXX` | `49.74%` | `-42.31%` | kept in this income-layer study; best combined result after income layer |
| `SOXX 10d vol >= 55%, SOXL -> BOXX` | `49.84%` | `-42.31%` | core-only CAGR is slightly higher, but combined income-layer result is worse |
| `SOXX 10d vol >= 50%, SOXL -> SOXX` | `48.48%` | `-42.31%` | more frequent de-levering lowers return |

Therefore the SOXL core `blend_gate_volatility_delever_*` defaults stay unchanged; only the income-layer defaults changed.
Therefore this income-layer study left the SOXL core `blend_gate_volatility_delever_*` defaults unchanged; a later June 2026 volatility-threshold recheck promoted a bounded dynamic threshold separately.

A lightweight 2026-06-04 refresh using Nasdaq real history and official yield proxies moved the SOXL income layer to the earlier, more SGOV-heavy `start=150000, max=95%, log_factor=0.50` version. In that sample it produced about `38.73%` CAGR and `-9.28%` max drawdown while still passing the SPY drawdown-window constraint.

Expand Down
4 changes: 2 additions & 2 deletions docs/research/income_layer_design.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ SOXL 核心 overlay 也做了窄候选复核:

| Core version | CAGR | Max drawdown | Note |
| --- | ---: | ---: | --- |
| current manifest:`SOXX 10d vol >= 55%, SOXL -> SOXX` | `49.74%` | `-42.31%` | 保留;与收入层组合后的 CAGR 最高 |
| 2026-05 manifest:`SOXX 10d vol >= 55%, SOXL -> SOXX` | `49.74%` | `-42.31%` | 本次收入层研究中保留;与收入层组合后的 CAGR 最高 |
| `SOXX 10d vol >= 55%, SOXL -> BOXX` | `49.84%` | `-42.31%` | 核心略高,但加入收入层后不如当前 manifest |
| `SOXX 10d vol >= 50%, SOXL -> SOXX` | `48.48%` | `-42.31%` | 更频繁降档,收益低于当前 manifest |

因此 SOXL 本次只调整收入层,不改核心 `blend_gate_volatility_delever_*` 默认值。
因此本次收入层研究没有调整 SOXL 核心 `blend_gate_volatility_delever_*` 默认值;后续 2026-06 波动率阈值复核已单独推广有边界的动态阈值

2026-06-04 使用 Nasdaq 真实历史和官方收益率代理做轻量复核后,SOXL 默认收入层进一步切到更早启动、更偏 SGOV 的 `start=150000, max=95%, log_factor=0.50` 版本;样本内 CAGR 约 `38.73%`、最大回撤约 `-9.28%`,仍通过 SPY 窗口回撤约束。

Expand Down
4 changes: 2 additions & 2 deletions docs/us_equity_strategy_status.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 美股策略状态与研究手册

_更新日期:2026-06-04_
_更新日期:2026-06-09_

这份文档只记录当前可配置的美股策略 profile、输入形态和研究状态,不记录任何账户或服务正在运行的 profile。部署单元当前跑什么属于私有运行信息,应留在云端配置或私有运行记录里。

Expand All @@ -16,7 +16,7 @@ _更新日期:2026-06-04_
| --- | --- | --- | --- | --- |
| `global_etf_rotation` | 全球 ETF 防守轮动 | 直接运行输入 | 季度 Top2 ETF 轮动,默认启用 SMA250 置信度 + 相对波动门控;每日 canary 防守,弱市切 `BIL`。 | 默认保留;当前推荐档。 |
| `tqqq_growth_income` | TQQQ 增长收益 | 直接运行输入 | `QQQ` / `TQQQ` 双轮增长,默认 `45% / 45% / 8% BOXX / 2% cash`;`QQQM` 可作为低单价交易代理。 | 小账户最容易落地;不需要 snapshot artifact。 |
| `soxl_soxx_trend_income` | SOXL/SOXX 半导体趋势收益 | 直接运行输入 | 以 `SOXX` 140 日趋势闸门控制 `SOXL` / `SOXX` / `BOXX`;默认 `SOXX` 10 日实际波动率 `>=55%` 时将 `SOXL` 转向 `SOXX`并叠加收入层。 | 半导体高弹性直接输入策略;波动高于宽基。 |
| `soxl_soxx_trend_income` | SOXL/SOXX 半导体趋势收益 | 直接运行输入 | 以 `SOXX` 140 日趋势闸门控制 `SOXL` / `SOXX` / `BOXX`;默认用 `SOXX` 10 日年化实际波动率的 252 日滚动 95 分位阈值,边界 `50%`-`75%`,样本不足时回退固定 `55%`,触发后将 `SOXL` 转向 `SOXX`并叠加收入层。 | 半导体高弹性直接输入策略;波动高于宽基。 |
| `nasdaq_sp500_smart_dca` | 纳斯达克 / 标普智能定投 | 直接运行输入 | 只买不卖;用 `QQQ/SPY` 的 200 日均线距离、252 日回撤和 RSI 过热状态决定本期定投金额倍数,默认买入 `QQQM/SPLG`。 | 适合现金账户长期积累;建议月度窗口运行。 |
| `russell_1000_multi_factor_defensive` | Russell 1000 多因子防守 | feature snapshot | Russell 1000 price-only 多因子,SPY 趋势 + breadth 防守,默认 24 股。 | 可切换但更适合大账户;长周期代理研究仍需补归档。 |
| `mega_cap_leader_rotation_top50_balanced` | Top50 平衡龙头轮动 | feature snapshot | 固定 `50% Top2 cap50 + 50% Top4 cap25` 袖子混合,不默认趋势降仓。 | 当前保留的无杠杆龙头轮动路线;建议 paper 观察。 |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"

[project]
name = "us-equity-strategies"
version = "0.7.52"
version = "0.7.53"
description = "Shared US equity strategy catalog and implementations"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"pandas>=2.0",
"pytz>=2024.1",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@e0f760255232b62481444a8c1d6637546ba2c07e",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@023641c88506c732624a7329e48b51b9dbbe3c2a",
]

[tool.setuptools]
Expand Down
6 changes: 6 additions & 0 deletions src/us_equity_strategies/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@
"blend_gate_volatility_delever_symbol": "SOXX",
"blend_gate_volatility_delever_window": 10,
"blend_gate_volatility_delever_threshold": 0.55,
"blend_gate_volatility_delever_threshold_mode": "rolling_percentile",
"blend_gate_volatility_delever_dynamic_lookback": 252,
"blend_gate_volatility_delever_dynamic_percentile": 0.95,
"blend_gate_volatility_delever_dynamic_min_periods": 126,
"blend_gate_volatility_delever_dynamic_floor": 0.50,
"blend_gate_volatility_delever_dynamic_cap": 0.75,
"blend_gate_volatility_delever_retention_ratio": 0.0,
"blend_gate_volatility_delever_redirect_symbol": "SOXX",
},
Expand Down
36 changes: 36 additions & 0 deletions src/us_equity_strategies/entrypoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,24 @@ def evaluate_soxl_soxx_trend_income(ctx: StrategyContext) -> StrategyDecision:
"blend_gate_volatility_delever_symbol": plan.get("blend_gate_volatility_delever_symbol"),
"blend_gate_volatility_delever_window": plan.get("blend_gate_volatility_delever_window"),
"blend_gate_volatility_delever_threshold": plan.get("blend_gate_volatility_delever_threshold"),
"blend_gate_volatility_delever_threshold_mode": plan.get("blend_gate_volatility_delever_threshold_mode"),
"blend_gate_volatility_delever_dynamic_threshold": plan.get(
"blend_gate_volatility_delever_dynamic_threshold"
),
"blend_gate_volatility_delever_dynamic_sample_count": plan.get(
"blend_gate_volatility_delever_dynamic_sample_count"
),
"blend_gate_volatility_delever_dynamic_lookback": plan.get(
"blend_gate_volatility_delever_dynamic_lookback"
),
"blend_gate_volatility_delever_dynamic_percentile": plan.get(
"blend_gate_volatility_delever_dynamic_percentile"
),
"blend_gate_volatility_delever_dynamic_min_periods": plan.get(
"blend_gate_volatility_delever_dynamic_min_periods"
),
"blend_gate_volatility_delever_dynamic_floor": plan.get("blend_gate_volatility_delever_dynamic_floor"),
"blend_gate_volatility_delever_dynamic_cap": plan.get("blend_gate_volatility_delever_dynamic_cap"),
"blend_gate_volatility_delever_metric": plan.get("blend_gate_volatility_delever_metric"),
"blend_gate_volatility_delever_triggered": plan.get("blend_gate_volatility_delever_triggered"),
"blend_gate_volatility_delever_retention_ratio": plan.get("blend_gate_volatility_delever_retention_ratio"),
Expand Down Expand Up @@ -716,6 +734,24 @@ def evaluate_soxl_soxx_trend_income(ctx: StrategyContext) -> StrategyDecision:
"blend_gate_volatility_delever_symbol": plan.get("blend_gate_volatility_delever_symbol"),
"blend_gate_volatility_delever_window": plan.get("blend_gate_volatility_delever_window"),
"blend_gate_volatility_delever_threshold": plan.get("blend_gate_volatility_delever_threshold"),
"blend_gate_volatility_delever_threshold_mode": plan.get("blend_gate_volatility_delever_threshold_mode"),
"blend_gate_volatility_delever_dynamic_threshold": plan.get(
"blend_gate_volatility_delever_dynamic_threshold"
),
"blend_gate_volatility_delever_dynamic_sample_count": plan.get(
"blend_gate_volatility_delever_dynamic_sample_count"
),
"blend_gate_volatility_delever_dynamic_lookback": plan.get(
"blend_gate_volatility_delever_dynamic_lookback"
),
"blend_gate_volatility_delever_dynamic_percentile": plan.get(
"blend_gate_volatility_delever_dynamic_percentile"
),
"blend_gate_volatility_delever_dynamic_min_periods": plan.get(
"blend_gate_volatility_delever_dynamic_min_periods"
),
"blend_gate_volatility_delever_dynamic_floor": plan.get("blend_gate_volatility_delever_dynamic_floor"),
"blend_gate_volatility_delever_dynamic_cap": plan.get("blend_gate_volatility_delever_dynamic_cap"),
"blend_gate_volatility_delever_metric": plan.get("blend_gate_volatility_delever_metric"),
"blend_gate_volatility_delever_triggered": plan.get("blend_gate_volatility_delever_triggered"),
"blend_gate_volatility_delever_retention_ratio": plan.get("blend_gate_volatility_delever_retention_ratio"),
Expand Down
6 changes: 6 additions & 0 deletions src/us_equity_strategies/manifests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ def _manifest(
"blend_gate_volatility_delever_symbol": "SOXX",
"blend_gate_volatility_delever_window": 10,
"blend_gate_volatility_delever_threshold": 0.55,
"blend_gate_volatility_delever_threshold_mode": "rolling_percentile",
"blend_gate_volatility_delever_dynamic_lookback": 252,
"blend_gate_volatility_delever_dynamic_percentile": 0.95,
"blend_gate_volatility_delever_dynamic_min_periods": 126,
"blend_gate_volatility_delever_dynamic_floor": 0.50,
"blend_gate_volatility_delever_dynamic_cap": 0.75,
"blend_gate_volatility_delever_retention_ratio": 0.0,
"blend_gate_volatility_delever_redirect_symbol": "SOXX",
},
Expand Down
109 changes: 106 additions & 3 deletions src/us_equity_strategies/strategies/soxl_soxx_trend_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

SOXX_GATE_TIERED_BLEND_MODE = "soxx_gate_tiered_blend"
CORE_ASSETS = ("SOXL", "SOXX", "BOXX")
VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED = "fixed"
VOLATILITY_DELEVER_THRESHOLD_MODE_ROLLING_PERCENTILE = "rolling_percentile"
VOLATILITY_DELEVER_THRESHOLD_MODES = frozenset(
{VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED, VOLATILITY_DELEVER_THRESHOLD_MODE_ROLLING_PERCENTILE}
)
MARKET_REGIME_CONTROL_PROFILE = "market_regime_control"
MARKET_REGIME_POSITION_ROUTES = frozenset({"risk_reduced", "risk_off"})
LEGACY_CRISIS_RESPONSE_PROFILE = "crisis_response_shadow"
Expand All @@ -23,6 +28,8 @@
"INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET",
"INCOME_LAYER_RATIO_MODES",
"SOXX_GATE_TIERED_BLEND_MODE",
"VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED",
"VOLATILITY_DELEVER_THRESHOLD_MODE_ROLLING_PERCENTILE",
"build_rebalance_plan",
"get_income_layer_ratio",
]
Expand Down Expand Up @@ -80,6 +87,21 @@ def _as_positive_int(value, *, default: int) -> int:
return max(1, result)


def _as_unit_interval(value, *, default: float) -> float:
result = _as_float_or_none(value)
if result is None or result <= 0.0 or result >= 1.0:
return float(default)
return float(result)


def _indicator_first_float(indicators, symbol: str, keys: Sequence[str]) -> float | None:
for key in keys:
value = _as_float_or_none(_indicator_value(indicators, symbol, key))
if value is not None:
return value
return None


def _iter_mapping_payloads(value, *, _depth: int = 0):
if _depth > 4:
return
Expand Down Expand Up @@ -298,6 +320,12 @@ def build_rebalance_plan(
blend_gate_volatility_delever_symbol="SOXX",
blend_gate_volatility_delever_window=10,
blend_gate_volatility_delever_threshold=0.55,
blend_gate_volatility_delever_threshold_mode=VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED,
blend_gate_volatility_delever_dynamic_lookback=252,
blend_gate_volatility_delever_dynamic_percentile=0.95,
blend_gate_volatility_delever_dynamic_min_periods=126,
blend_gate_volatility_delever_dynamic_floor=0.50,
blend_gate_volatility_delever_dynamic_cap=0.75,
blend_gate_volatility_delever_retention_ratio=0.0,
blend_gate_volatility_delever_redirect_symbol="SOXX",
market_regime_control_enabled=False,
Expand Down Expand Up @@ -412,9 +440,36 @@ def build_rebalance_plan(
if not volatility_delever_symbol:
volatility_delever_symbol = trend_symbol
volatility_delever_window = _as_positive_int(blend_gate_volatility_delever_window, default=10)
volatility_delever_threshold = _as_float_or_none(blend_gate_volatility_delever_threshold)
if volatility_delever_threshold is None:
volatility_delever_threshold = 0.55
volatility_delever_fixed_threshold = _as_float_or_none(blend_gate_volatility_delever_threshold)
if volatility_delever_fixed_threshold is None:
volatility_delever_fixed_threshold = 0.55
volatility_delever_threshold_mode = str(
blend_gate_volatility_delever_threshold_mode or VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED
).strip().lower()
if volatility_delever_threshold_mode not in VOLATILITY_DELEVER_THRESHOLD_MODES:
volatility_delever_threshold_mode = VOLATILITY_DELEVER_THRESHOLD_MODE_FIXED
volatility_delever_dynamic_lookback = _as_positive_int(
blend_gate_volatility_delever_dynamic_lookback,
default=252,
)
volatility_delever_dynamic_percentile = _as_unit_interval(
blend_gate_volatility_delever_dynamic_percentile,
default=0.95,
)
volatility_delever_dynamic_min_periods = min(
volatility_delever_dynamic_lookback,
_as_positive_int(blend_gate_volatility_delever_dynamic_min_periods, default=126),
)
volatility_delever_dynamic_floor = _as_clamped_ratio(
blend_gate_volatility_delever_dynamic_floor,
default=0.50,
)
volatility_delever_dynamic_cap = _as_clamped_ratio(
blend_gate_volatility_delever_dynamic_cap,
default=0.75,
)
if volatility_delever_dynamic_cap < volatility_delever_dynamic_floor:
volatility_delever_dynamic_cap = volatility_delever_dynamic_floor
volatility_delever_retention_ratio = _as_clamped_ratio(
blend_gate_volatility_delever_retention_ratio,
default=0.0,
Expand All @@ -435,6 +490,38 @@ def build_rebalance_plan(
volatility_delever_metric = _as_float_or_none(
_indicator_value(indicators, volatility_delever_symbol, "realized_volatility")
)
volatility_delever_dynamic_threshold = _indicator_first_float(
indicators,
volatility_delever_symbol,
(
f"realized_volatility_{volatility_delever_window}_dynamic_threshold",
"realized_volatility_dynamic_threshold",
),
)
volatility_delever_dynamic_sample_count = _indicator_first_float(
indicators,
volatility_delever_symbol,
(
f"realized_volatility_{volatility_delever_window}_dynamic_sample_count",
"realized_volatility_dynamic_sample_count",
),
)
if volatility_delever_dynamic_threshold is not None:
volatility_delever_dynamic_threshold = max(
volatility_delever_dynamic_floor,
min(volatility_delever_dynamic_cap, volatility_delever_dynamic_threshold),
)
if (
volatility_delever_dynamic_sample_count is not None
and volatility_delever_dynamic_sample_count < volatility_delever_dynamic_min_periods
):
volatility_delever_dynamic_threshold = None
volatility_delever_threshold = volatility_delever_fixed_threshold
if (
volatility_delever_threshold_mode == VOLATILITY_DELEVER_THRESHOLD_MODE_ROLLING_PERCENTILE
and volatility_delever_dynamic_threshold is not None
):
volatility_delever_threshold = volatility_delever_dynamic_threshold
rsi_threshold = _as_float_or_none(blend_gate_rsi_threshold)
if rsi_threshold is None:
rsi_threshold = 70.0
Expand Down Expand Up @@ -668,6 +755,14 @@ def build_rebalance_plan(
"volatility_delever_symbol": volatility_delever_symbol,
"volatility_delever_window": volatility_delever_window,
"volatility_delever_threshold": volatility_delever_threshold,
"volatility_delever_threshold_mode": volatility_delever_threshold_mode,
"volatility_delever_dynamic_threshold": volatility_delever_dynamic_threshold,
"volatility_delever_dynamic_sample_count": volatility_delever_dynamic_sample_count,
"volatility_delever_dynamic_lookback": volatility_delever_dynamic_lookback,
"volatility_delever_dynamic_percentile": volatility_delever_dynamic_percentile,
"volatility_delever_dynamic_min_periods": volatility_delever_dynamic_min_periods,
"volatility_delever_dynamic_floor": volatility_delever_dynamic_floor,
"volatility_delever_dynamic_cap": volatility_delever_dynamic_cap,
"volatility_delever_metric": volatility_delever_metric,
"volatility_delever_triggered": volatility_delever_triggered,
"volatility_delever_retention_ratio": volatility_delever_retention_ratio,
Expand Down Expand Up @@ -783,6 +878,14 @@ def build_rebalance_plan(
"blend_gate_volatility_delever_symbol": volatility_delever_symbol,
"blend_gate_volatility_delever_window": volatility_delever_window,
"blend_gate_volatility_delever_threshold": volatility_delever_threshold,
"blend_gate_volatility_delever_threshold_mode": volatility_delever_threshold_mode,
"blend_gate_volatility_delever_dynamic_threshold": volatility_delever_dynamic_threshold,
"blend_gate_volatility_delever_dynamic_sample_count": volatility_delever_dynamic_sample_count,
"blend_gate_volatility_delever_dynamic_lookback": volatility_delever_dynamic_lookback,
"blend_gate_volatility_delever_dynamic_percentile": volatility_delever_dynamic_percentile,
"blend_gate_volatility_delever_dynamic_min_periods": volatility_delever_dynamic_min_periods,
"blend_gate_volatility_delever_dynamic_floor": volatility_delever_dynamic_floor,
"blend_gate_volatility_delever_dynamic_cap": volatility_delever_dynamic_cap,
"blend_gate_volatility_delever_metric": volatility_delever_metric,
"blend_gate_volatility_delever_triggered": volatility_delever_triggered,
"blend_gate_volatility_delever_retention_ratio": volatility_delever_retention_ratio,
Expand Down
Loading