diff --git a/README.md b/README.md index d1509bc..a3da8a8 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Research-only profiles may stay in code for reproducibility and future review, b | Profile | Name | Notes | | --- | --- | --- | -| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | research-only; kept out of current configurable live profiles. | +| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | archived research-only; no longer a catalog/entrypoint runtime profile. | ## How this connects to execution diff --git a/README.zh-CN.md b/README.zh-CN.md index afcd92c..3354c49 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -45,7 +45,7 @@ UsEquityStrategies 是 QuantStrategyLab 的美股策略包。为 QuantStrategyLa | Profile | 名称 | 说明 | | --- | --- | --- | -| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | research-only,不进入当前可配置 live profile。 | +| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | 已归档 research-only,不再是 catalog/entrypoint 可运行 profile。 | ## 如何接到执行平台 diff --git a/docs/research/income_layer_design.md b/docs/research/income_layer_design.md index 82a136e..b542156 100644 --- a/docs/research/income_layer_design.md +++ b/docs/research/income_layer_design.md @@ -9,7 +9,7 @@ - 阅读顺序:先确认边界、输入输出和权限要求,再执行文档里的命令、CI、dry-run、发布或切换步骤。 - 风险提示:涉及实盘、密钥、权限、Cloud Run、交易所或券商 API 的变更,必须先在测试环境或 dry-run 验证;不要只凭示例直接修改生产。 - 英文正文保留更完整的命令、字段名和配置键;如果摘要和正文不一致,以正文中的实际命令和配置为准。 -_Updated: 2026-05-26_ +_Updated: 2026-06-04_ ## Decision @@ -19,25 +19,44 @@ Current defaults: | Profile | Mode | Start | Activation band | Hard cap | Default income basket | | --- | --- | ---: | ---: | ---: | --- | -| `tqqq_growth_income` | `log_cap` | `250000` | `20%` | `50%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | -| `soxl_soxx_trend_income` | `log_cap` | `250000` | `20%` | `95%` | `SCHD 25% / DGRO 15% / SGOV 55% / SPYI 4% / QQQI 1%` | -| `global_etf_rotation` | `log_loss_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | -| `russell_1000_multi_factor_defensive` | `log_loss_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | -| `tech_communication_pullback_enhancement` | `log_loss_budget` | `250000` | `15%` | `30%` | `SCHD 40% / DGRO 25% / SGOV 20% / SPYI 10% / QQQI 5%` | -| `mega_cap_leader_rotation_top50_balanced` | `log_loss_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 20% / SPYI 5%` | +| `tqqq_growth_income` | `log_total_drawdown_budget` | `250000` | `20%` | `55%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | +| `soxl_soxx_trend_income` | `log_total_drawdown_budget` | `150000` | `20%` | `95%` | `SCHD 15% / DGRO 10% / SGOV 70% / SPYI 4% / QQQI 1%` | +| `global_etf_rotation` | `log_total_drawdown_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | +| `russell_1000_multi_factor_defensive` | `log_total_drawdown_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | +| `mega_cap_leader_rotation_top50_balanced` | `log_total_drawdown_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 25%` | + +`tech_communication_pullback_enhancement` is removed from runtime exposure. Its +strategy implementation and bundled config remain only as archived research, so +it has no current income-layer default. Activation and near-cap visualization: [`income_layer_activation_drawdown_2026-05-26.svg`](./income_layer_activation_drawdown_2026-05-26.svg). ## Design Rules -- Leveraged profiles use `log_cap`: the account-level goal is to keep combined drawdown near or inside SPY / QQQ while preserving compound growth. -- Non-leveraged profiles use `log_loss_budget`: the goal is to damp account volatility as account size grows, not to materially rewrite the core strategy. +- Defaults use `log_total_drawdown_budget`: first set a target account-level drawdown budget from account size, then reverse it into an income-layer ratio from core-strategy and income-basket stress drawdown assumptions. +- The income layer is enabled by default; set `income_layer_enabled = false` to disable it. +- Leveraged profiles start from about a `45%` small-account stress budget, then tighten by account doubling toward about `30%`, and continue toward about `25%` for larger accounts. +- Non-leveraged profiles use a softer account-level budget curve so the income layer does not rewrite the core strategy too early. - `income_layer_start_usd` is strategy-specific. Leveraged strategies start earlier; non-leveraged strategies generally start later or use a smaller cap. - `income_layer_activation_band_ratio` prevents threshold churn. The normal target ratio is multiplied from 0 to 1 between `start` and `start * (1 + band)`. - `income_layer_max_ratio` is a risk-budget parameter, not a pure return-maximization parameter. Raising the cap usually reduces drawdown and long-run CAGR. - Existing income holdings are locked with `max(current_income_layer_value, desired_income_layer_value)`, so the layer adds capital by default instead of force-selling income assets down. +## Account-Budget Parameter Design + +`base_drawdown_budget` equals each strategy's estimated core stress drawdown, so the target income-layer ratio starts continuously from 0 just above `start`. The budget then tightens smoothly by `drawdown_budget_decay_per_double * log2(nav / start)`, with `min_drawdown_budget` as the large-account floor. The income ratio is reversed from: + +`income_ratio = (core_stress_drawdown - account_budget) / (core_stress_drawdown - income_stress_drawdown)` + +| Profile | Role | Core stress drawdown | Income stress drawdown | Account budget curve | Income cap | Rationale | +| --- | --- | ---: | ---: | --- | ---: | --- | +| `tqqq_growth_income` | Broad-market leveraged growth | `45%` | `8%` | Starts at `45%`, tightens `5%` per double, floor `25%` | `55%` | Small accounts can accept near-core volatility; around `500k` the budget tightens near `40%`, around `2M` near `30%`, while preserving compounding. | +| `soxl_soxx_trend_income` | Semiconductor leveraged trend | `45%` | `6%` | Starts at `45%`, tightens `5%` per double, floor `25%` | `95%` | SOXL has sharper path risk and a more SGOV-heavy income basket, so the cap is high enough to satisfy large-account budgets. | +| `global_etf_rotation` | Defensive ETF rotation | `30%` | `8%` | Starts at `30%`, tightens `1.5%` per double, floor `26.7%` | `15%` | The core already has canary/BIL defense; the floor matches the drawdown achievable with the 15% income cap. | +| `russell_1000_multi_factor_defensive` | Defensive multi-factor stocks | `30%` | `8%` | Starts at `30%`, tightens `1.5%` per double, floor `25.6%` | `20%` | Single-stock equity risk is higher than Global ETF, so the cap and achievable floor are slightly more defensive. | +| `mega_cap_leader_rotation_top50_balanced` | Concentrated leader rotation | `35%` | `8%` | Starts at `35%`, tightens `2%` per double, floor `28.25%` | `25%` | Top2/Top4 concentration needs a Tech-like budget curve, but the cap is lower to avoid muting strong trend capture too much. | + ## Leveraged Profile Review Research output: @@ -51,7 +70,7 @@ Selection rules: - TQQQ additionally uses a roughly `15%` max drawdown constraint at `1000000 USD`, matching the "at most 150k loss on a 1M account" budget. - Among candidates that pass, rank by CAGR; if return is close, prefer the simpler path closest to the current default core. -Final selected defaults: +Defaults selected on 2026-05-26 are archived below. The 2026-06-04 defaults now use account-level `log_total_drawdown_budget`; the current-default table above is authoritative: | Strategy | Version | CAGR | Max drawdown | SPY windows | QQQ windows | Avg income ratio | End income ratio | Decision | | --- | --- | ---: | ---: | --- | --- | ---: | ---: | --- | @@ -71,6 +90,8 @@ SOXL core overlay review: Therefore the SOXL core `blend_gate_volatility_delever_*` defaults stay unchanged; only the income-layer defaults changed. +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. + ## Core Default Review Follow-up research on 2026-05-26/27 retested the leveraged cores after the @@ -98,4 +119,4 @@ this review. - Small-account mode does not require combined drawdown to beat the broad market. The account is still mainly in growth-compounding mode until it crosses the threshold. - At `1000000 USD`, the income layer must pull TQQQ and SOXL combined drawdown inside the SPY / QQQ reference windows. - TQQQ moved from `150000` to `250000` start to reduce early income drag while keeping about `-14.87%` drawdown in the `1000000 USD` calibration. -- SOXL moved from `150000 / 90% / current_soxl` to `250000 / 95% / balanced_income`; in the `1000000 USD` calibration, CAGR improved from about `32.16%` to `36.14%`, while max drawdown widened from about `-7.70%` to `-9.04%` and stayed well inside the loss budget. +- SOXL first moved from `150000 / 90% / current_soxl` to `250000 / 95% / balanced_income`; the 2026-06-04 refresh then moved it to `150000 / 95% / SGOV-heavy`, using the higher SGOV weight to offset the earlier activation's return drag. diff --git a/docs/research/income_layer_design.zh-CN.md b/docs/research/income_layer_design.zh-CN.md index 0bc54a5..1e13250 100644 --- a/docs/research/income_layer_design.zh-CN.md +++ b/docs/research/income_layer_design.zh-CN.md @@ -10,7 +10,7 @@ - For live trading, secrets, Cloud Run, exchange, or broker API changes, validate in test or dry-run mode first and do not change production only from examples. - If this summary differs from the detailed Chinese body, follow the concrete commands, configuration keys, and constraints in the body. -_更新日期:2026-05-26_ +_更新日期:2026-06-04_ ## 结论 @@ -20,25 +20,42 @@ _更新日期:2026-05-26_ | Profile | 模式 | 起点 | 平滑带 | 硬上限 | 默认收入篮子 | | --- | --- | ---: | ---: | ---: | --- | -| `tqqq_growth_income` | `log_cap` | `250000` | `20%` | `50%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | -| `soxl_soxx_trend_income` | `log_cap` | `250000` | `20%` | `95%` | `SCHD 25% / DGRO 15% / SGOV 55% / SPYI 4% / QQQI 1%` | -| `global_etf_rotation` | `log_loss_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | -| `russell_1000_multi_factor_defensive` | `log_loss_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | -| `tech_communication_pullback_enhancement` | `log_loss_budget` | `250000` | `15%` | `30%` | `SCHD 40% / DGRO 25% / SGOV 20% / SPYI 10% / QQQI 5%` | -| `mega_cap_leader_rotation_top50_balanced` | `log_loss_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 20% / SPYI 5%` | +| `tqqq_growth_income` | `log_total_drawdown_budget` | `250000` | `20%` | `55%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | +| `soxl_soxx_trend_income` | `log_total_drawdown_budget` | `150000` | `20%` | `95%` | `SCHD 15% / DGRO 10% / SGOV 70% / SPYI 4% / QQQI 1%` | +| `global_etf_rotation` | `log_total_drawdown_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | +| `russell_1000_multi_factor_defensive` | `log_total_drawdown_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | +| `mega_cap_leader_rotation_top50_balanced` | `log_total_drawdown_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 25%` | + +`tech_communication_pullback_enhancement` 已从可运行暴露中移除;策略实现和 bundled config 仅作为离线研究归档保留,因此不再有当前收入层默认参数。 启动门槛、平滑带和接近上限位置的图表见 [`income_layer_activation_drawdown_2026-05-26.svg`](./income_layer_activation_drawdown_2026-05-26.svg)。 ## 设计规则 -- 杠杆策略使用 `log_cap`:目标是让组合层回撤接近或不超过 SPY / QQQ 对照,同时尽量保留复利。 -- 非杠杆策略使用 `log_loss_budget`:目标是账户规模变大后的波动钝化,而不是显著改变策略本身。 +- 默认模式统一使用 `log_total_drawdown_budget`:先按账户规模给出目标总回撤预算,再用核心策略压力回撤和收入篮子压力回撤反推出收入层比例。 +- 收入层默认启用;需要关闭时设置 `income_layer_enabled = false`。 +- 杠杆策略的小资金压力预算约为 `45%`,随后随账户翻倍逐步收紧到约 `30%`,更大账户继续向 `25%` 附近收敛。 +- 非杠杆策略使用更温和的账户级预算曲线,避免收入层过早改写核心策略。 - `income_layer_start_usd` 必须按策略单独配置。杠杆策略更早启动,非杠杆策略更晚启动。 - `income_layer_activation_band_ratio` 用来解决门槛附近来回切换的问题。目标收入层比例在 `start` 到 `start * (1 + band)` 之间从 0 平滑放大到正常值。 - `income_layer_max_ratio` 是组合层风险预算,不是收益最大化参数。上限提高通常降低回撤,但也会降低长期 CAGR。 - 现有收入层采用 `max(current_income_layer_value, desired_income_layer_value)` 锁定已有收入资产,默认只增配,不主动减配。 +## 账户级预算参数设计 + +`base_drawdown_budget` 默认等于该策略的核心压力回撤估计,因此刚跨过 `start` 时收入层目标比例仍从 0 连续起步;随后按 `drawdown_budget_decay_per_double * log2(nav / start)` 平滑收紧,并由 `min_drawdown_budget` 设置大账户下限。收入层比例由公式反推: + +`income_ratio = (core_stress_drawdown - account_budget) / (core_stress_drawdown - income_stress_drawdown)` + +| Profile | 设计角色 | 核心压力回撤 | 收入篮子压力回撤 | 账户预算曲线 | 收入层上限 | 设计理由 | +| --- | --- | ---: | ---: | --- | ---: | --- | +| `tqqq_growth_income` | 宽基杠杆增长 | `45%` | `8%` | `45%` 起,每翻倍降 `5%`,最低 `25%` | `55%` | 小资金允许接近核心波动;约 `500k` 附近预算收紧到 `40%`,约 `2M` 附近到 `30%`,但保留复利弹性。 | +| `soxl_soxx_trend_income` | 半导体杠杆趋势 | `45%` | `6%` | `45%` 起,每翻倍降 `5%`,最低 `25%` | `95%` | SOXL 路径更尖锐,收入篮子更偏 SGOV,因此允许更高收入层上限来处理大账户压力预算。 | +| `global_etf_rotation` | 防守型 ETF 轮动 | `30%` | `8%` | `30%` 起,每翻倍降 `1.5%`,最低 `26.7%` | `15%` | 核心本身已有 canary 和 BIL 防守,收入层只做大账户钝化;最低预算贴合 15% 收入层上限的可实现回撤。 | +| `russell_1000_multi_factor_defensive` | 防守型多因子股票 | `30%` | `8%` | `30%` 起,每翻倍降 `1.5%`,最低 `25.6%` | `20%` | 个股组合比 Global ETF 更分散但仍有权益风险,上限略高,最低预算贴合 20% 收入层上限。 | +| `mega_cap_leader_rotation_top50_balanced` | 高集中龙头轮动 | `35%` | `8%` | `35%` 起,每翻倍降 `2%`,最低 `28.25%` | `25%` | Top2/Top4 集中度高,预算曲线同科技增强,但上限略低,避免过度拖累强趋势。 | + ## 杠杆策略实盘候选复核 研究输出: @@ -52,7 +69,7 @@ _更新日期:2026-05-26_ - TQQQ 额外使用 `1000000 USD` 最大回撤不超过约 `15%` 的约束,匹配“100 万最多亏 15 万”的账户约束。 - 在通过约束的候选里按 CAGR 排序,若收益接近则优先保留更简单、更贴近当前生产路径的核心策略。 -最终固定: +2026-05-26 当时固定的候选如下;2026-06-04 已切换到账户级 `log_total_drawdown_budget` 默认,当前默认以本文开头表格为准: | Strategy | Version | CAGR | Max drawdown | SPY windows | QQQ windows | Avg income ratio | End income ratio | Decision | | --- | --- | ---: | ---: | --- | --- | ---: | ---: | --- | @@ -72,6 +89,8 @@ SOXL 核心 overlay 也做了窄候选复核: 因此 SOXL 本次只调整收入层,不改核心 `blend_gate_volatility_delever_*` 默认值。 +2026-06-04 使用 Nasdaq 真实历史和官方收益率代理做轻量复核后,SOXL 默认收入层进一步切到更早启动、更偏 SGOV 的 `start=150000, max=95%, log_factor=0.50` 版本;样本内 CAGR 约 `38.73%`、最大回撤约 `-9.28%`,仍通过 SPY 窗口回撤约束。 + ## 核心默认参数复核 2026-05-26/27 在收入层默认值选定后,又对杠杆核心做了一轮小范围复核。复核刻意保持窄候选: @@ -112,7 +131,7 @@ SOXL 核心 overlay 也做了窄候选复核: - 小账户阶段不强行要求组合回撤不超过大盘。小账户的目标仍是增长层复利,收入层只在权益跨过门槛后逐步介入。 - 资金达到 `1000000 USD` 后,收入层配置必须把 TQQQ 和 SOXL 的组合层回撤压到 SPY / QQQ 窗口对照以内。 - TQQQ 从 `150000` 启动上移到 `250000`,能让小账户阶段少受收入层拖累;在 `1000000 USD` 校准下仍保持约 `-14.87%` 最大回撤。 -- SOXL 从 `150000 / 90% / current_soxl` 调整为 `250000 / 95% / balanced_income`,在 `1000000 USD` 校准下 CAGR 从约 `32.16%` 提升到 `36.14%`,最大回撤从约 `-7.70%` 扩到 `-9.04%`,仍明显低于 15% 亏损预算并通过 SPY / QQQ 回撤窗口。 +- SOXL 先从 `150000 / 90% / current_soxl` 调整为 `250000 / 95% / balanced_income`;2026-06-04 复核后改为 `150000 / 95% / SGOV-heavy`,用更高 SGOV 占比抵消更早启动带来的收益拖累。 ## 预设方向 diff --git a/docs/us_equity_contract_gap_matrix.md b/docs/us_equity_contract_gap_matrix.md index aea8f47..a68ca2f 100644 --- a/docs/us_equity_contract_gap_matrix.md +++ b/docs/us_equity_contract_gap_matrix.md @@ -8,7 +8,7 @@ - 阅读顺序:先确认边界、输入输出和权限要求,再执行文档里的命令、CI、dry-run、发布或切换步骤。 - 风险提示:涉及实盘、密钥、权限、Cloud Run、交易所或券商 API 的变更,必须先在测试环境或 dry-run 验证;不要只凭示例直接修改生产。 - 英文正文保留更完整的命令、字段名和配置键;如果摘要和正文不一致,以正文中的实际命令和配置为准。 -_Updated: 2026-05-26_ +_Updated: 2026-06-04_ This document tracks the current shared US equity strategy contract across `UsEquityStrategies`, `QuantPlatformKit`, and the platform runtimes. @@ -25,7 +25,6 @@ The current runtime-enabled US equity profiles are: - `tqqq_growth_income` - `soxl_soxx_trend_income` - `russell_1000_multi_factor_defensive` -- `tech_communication_pullback_enhancement` - `mega_cap_leader_rotation_top50_balanced` - `nasdaq_sp500_smart_dca` @@ -56,10 +55,14 @@ comparison with runtime-enabled peers: - `mega_cap_leader_rotation_dynamic_top20` - `mega_cap_leader_rotation_aggressive` - `dynamic_mega_leveraged_pullback` +- `tech_communication_pullback_enhancement` The first two were superseded by `mega_cap_leader_rotation_top50_balanced`. The 2x dynamic mega-cap/MAGS route added more product and input complexity without a better promoted profile result. +`tech_communication_pullback_enhancement` stayed as an archived research +implementation/config for reproducibility, but it is no longer exposed through +catalog, manifest, entrypoint, runtime adapter, or platform rollout surfaces. Historical output files can still be inspected in research directories, but these names are no longer valid `STRATEGY_PROFILE` values and should not appear @@ -83,7 +86,6 @@ New US equity profiles should use only these canonical `required_inputs`: | `tqqq_growth_income` | `value` | `benchmark_history`, `portfolio_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Direct QQQ/TQQQ growth-income profile with explicit portfolio input. | | `soxl_soxx_trend_income` | `value` | `derived_indicators`, `portfolio_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Semiconductor trend profile using canonical derived indicators. | | `russell_1000_multi_factor_defensive` | `weight` | `feature_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Artifact-backed Russell 1000 defensive selection. | -| `tech_communication_pullback_enhancement` | `weight` | `feature_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Artifact-backed tech/communication pullback selection with bundled config support. | | `mega_cap_leader_rotation_top50_balanced` | `weight` | `feature_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Retained Top50 balanced leader-rotation path. | | `nasdaq_sp500_smart_dca` | `value` | `market_history`, `portfolio_snapshot` | `ibkr`, `schwab`, `longbridge`, `firstrade`, `paper_signal` | runtime-enabled | Buy-only Nasdaq/S&P 500 smart DCA using market-history indicators and cash availability. | diff --git a/docs/us_equity_notification_i18n_contract.md b/docs/us_equity_notification_i18n_contract.md index 5982148..4e25c42 100644 --- a/docs/us_equity_notification_i18n_contract.md +++ b/docs/us_equity_notification_i18n_contract.md @@ -105,10 +105,12 @@ Platform audit logs should keep a single structured event per strategy evaluatio "execution_annotations": {}, "income_layer": { "applied": true, - "ratio": 0.361, - "mode": "log_cap", - "start_usd": 250000.0, - "max_ratio": 0.95 + "ratio": 0.270, + "mode": "log_total_drawdown_budget", + "start_usd": 150000.0, + "max_ratio": 0.95, + "account_drawdown_budget_ratio": 0.35, + "account_stress_drawdown_ratio": 0.35 } } ``` diff --git a/docs/us_equity_notification_i18n_contract.zh-CN.md b/docs/us_equity_notification_i18n_contract.zh-CN.md index debd686..566e3d0 100644 --- a/docs/us_equity_notification_i18n_contract.zh-CN.md +++ b/docs/us_equity_notification_i18n_contract.zh-CN.md @@ -102,10 +102,12 @@ notification_context = { "execution_annotations": {}, "income_layer": { "applied": true, - "ratio": 0.361, - "mode": "log_cap", - "start_usd": 250000.0, - "max_ratio": 0.95 + "ratio": 0.270, + "mode": "log_total_drawdown_budget", + "start_usd": 150000.0, + "max_ratio": 0.95, + "account_drawdown_budget_ratio": 0.35, + "account_stress_drawdown_ratio": 0.35 } } ``` diff --git a/docs/us_equity_strategy_status.zh-CN.md b/docs/us_equity_strategy_status.zh-CN.md index 2778af2..7ca65d7 100644 --- a/docs/us_equity_strategy_status.zh-CN.md +++ b/docs/us_equity_strategy_status.zh-CN.md @@ -9,7 +9,7 @@ - For live trading, secrets, Cloud Run, exchange, or broker API changes, validate in test or dry-run mode first and do not change production only from examples. - If this summary differs from the detailed Chinese body, follow the concrete commands, configuration keys, and constraints in the body. -_更新日期:2026-05-26_ +_更新日期:2026-06-04_ 这份文档只记录当前可配置的美股策略 profile、输入形态和研究状态,不记录任何账户或服务正在运行的 profile。部署单元当前跑什么属于私有运行信息,应留在云端配置或私有运行记录里。 @@ -19,7 +19,7 @@ _更新日期:2026-05-26_ ## 当前可配置 profiles -这 7 条 profile 是当前 `runtime_enabled` `us_equity` 集合。它们按共享文档规范设计为通用策略,平台侧通过同一份 catalog、manifest、entrypoint 和 runtime adapter 契约接入;是否部署启用仍由各部署配置和风控决定。`global_etf_confidence_vol_gate` 现在只是 `global_etf_rotation` 的 legacy alias,不再是独立 runtime profile。 +这 6 条 profile 是当前 `runtime_enabled` `us_equity` 集合。它们按共享文档规范设计为通用策略,平台侧通过同一份 catalog、manifest、entrypoint 和 runtime adapter 契约接入;是否部署启用仍由各部署配置和风控决定。`global_etf_confidence_vol_gate` 现在只是 `global_etf_rotation` 的 legacy alias,不再是独立 runtime profile。 | Profile | 中文定位 | 输入类型 | 特点 | 当前建议 | | --- | --- | --- | --- | --- | @@ -27,34 +27,33 @@ _更新日期:2026-05-26_ | `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`,并叠加收入层。 | 半导体高弹性直接输入策略;波动高于宽基。 | | `nasdaq_sp500_smart_dca` | 纳斯达克 / 标普智能定投 | 直接运行输入 | 只买不卖;用 `QQQ/SPY` 的 200 日均线距离、252 日回撤和 RSI 过热状态决定本期定投金额倍数,默认买入 `QQQM/SPLG`。 | 适合现金账户长期积累;建议月度窗口运行。 | -| `tech_communication_pullback_enhancement` | 科技通信回调增强 | feature snapshot | 科技/通信个股月频选择,受控回调入场,保留 BOXX 缓冲。 | 需要月度 snapshot;适合先小比例或观察运行。 | | `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 观察。 | ## 已移除的重复/较弱研究 profile 暴露 -按“如果比同类 runtime-enabled 策略表现差,就不要继续保留可运行入口”的口径,下面 3 条已经从 catalog、manifest、entrypoint、runtime adapter、snapshot publish 和平台 rollout 暴露中移除: +按“如果比同类 runtime-enabled 策略表现差,就不要继续保留可运行入口”的口径,下面 4 条已经从 catalog、manifest、entrypoint、runtime adapter、snapshot publish 和平台 rollout 暴露中移除: | 已移除 profile | 移除原因 | | --- | --- | | `mega_cap_leader_rotation_dynamic_top20` | 同期 CAGR 21.51%、最大回撤 -23.14%;收益明显弱于 `mega_cap_leader_rotation_top50_balanced` 的 36.41%。 | | `mega_cap_leader_rotation_aggressive` | Top50 top3/cap35 CAGR 32.42%、最大回撤 -28.64%;仍弱于 Top50 balanced,且更集中。 | | `dynamic_mega_leveraged_pullback` | CAGR 30.96%、最大回撤 -34.80%;2x 产品和事件反弹路线更复杂,未优于当前保留路线。 | +| `tech_communication_pullback_enhancement` | 行业限制在科技/通信,收益明显低于 `mega_cap_leader_rotation_top50_balanced`,最大回撤也没有改善;策略实现和 bundled config 仅作为离线研究归档保留。 | 历史研究输出可以继续作为离线证据查看,但这些名字不再是有效 `STRATEGY_PROFILE`,也不再保留平台 replay adapter。 ## 收入层默认启用口径 -除 `nasdaq_sp500_smart_dca` 这类只买不卖的现金定投 profile 外,保留的组合型 runtime profile 默认都启用收入层,且下游策略配置可以覆盖任意 `income_layer_*` 参数;需要关闭时设置 `income_layer_enabled = false`。杠杆策略使用 `log_cap`,目标是让组合层最大回撤不超过 SPY 的同时尽量保留复利;非杠杆策略使用更轻的 `log_loss_budget`,作为账户规模变大后的波动钝化器。`income_layer_activation_band_ratio` 会在 `start` 到 `start * (1 + band)` 之间把正常目标比例从 0 平滑放大到 1,避免门槛附近来回卡住。 +除 `nasdaq_sp500_smart_dca` 这类只买不卖的现金定投 profile 外,保留的组合型 runtime profile 默认都启用收入层,且下游策略配置可以覆盖任意 `income_layer_*` 参数;需要关闭时设置 `income_layer_enabled = false`。当前默认统一使用 `log_total_drawdown_budget`,先按账户规模给出目标总回撤预算,再用核心策略压力回撤和收入篮子压力回撤反推出收入层比例。`income_layer_activation_band_ratio` 会在 `start` 到 `start * (1 + band)` 之间把正常目标比例从 0 平滑放大到 1,避免门槛附近来回卡住。 | Profile | 模式 | 起点 | 平滑带 | 硬上限 | 默认收入篮子 | | --- | --- | ---: | ---: | ---: | --- | -| `tqqq_growth_income` | `log_cap` | `250000` | `20%` | `50%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | -| `soxl_soxx_trend_income` | `log_cap` | `250000` | `20%` | `95%` | `SCHD 25% / DGRO 15% / SGOV 55% / SPYI 4% / QQQI 1%` | -| `global_etf_rotation` | `log_loss_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | -| `russell_1000_multi_factor_defensive` | `log_loss_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | -| `tech_communication_pullback_enhancement` | `log_loss_budget` | `250000` | `15%` | `30%` | `SCHD 40% / DGRO 25% / SGOV 20% / SPYI 10% / QQQI 5%` | -| `mega_cap_leader_rotation_top50_balanced` | `log_loss_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 20% / SPYI 5%` | +| `tqqq_growth_income` | `log_total_drawdown_budget` | `250000` | `20%` | `55%` | `SCHD 30% / DGRO 20% / SGOV 40% / SPYI 8% / QQQI 2%` | +| `soxl_soxx_trend_income` | `log_total_drawdown_budget` | `150000` | `20%` | `95%` | `SCHD 15% / DGRO 10% / SGOV 70% / SPYI 4% / QQQI 1%` | +| `global_etf_rotation` | `log_total_drawdown_budget` | `500000` | `10%` | `15%` | `SCHD 40% / DGRO 25% / SGOV 30% / SPYI 5%` | +| `russell_1000_multi_factor_defensive` | `log_total_drawdown_budget` | `400000` | `10%` | `20%` | `SCHD 45% / DGRO 30% / SGOV 25%` | +| `mega_cap_leader_rotation_top50_balanced` | `log_total_drawdown_budget` | `300000` | `15%` | `25%` | `SCHD 45% / DGRO 30% / SGOV 25%` | ## 已归档回测摘要 diff --git a/src/us_equity_strategies/account_sizing.py b/src/us_equity_strategies/account_sizing.py index 198390c..0de3ca3 100644 --- a/src/us_equity_strategies/account_sizing.py +++ b/src/us_equity_strategies/account_sizing.py @@ -9,8 +9,6 @@ "tqqq_growth_income": 500.0, "soxl_soxx_trend_income": 1_000.0, "russell_1000_multi_factor_defensive": 30_000.0, - "tech_communication_pullback_enhancement": 10_000.0, - "qqq_tech_enhancement": 10_000.0, "mega_cap_leader_rotation_top50_balanced": 10_000.0, "nasdaq_sp500_smart_dca": 1_000.0, } diff --git a/src/us_equity_strategies/catalog.py b/src/us_equity_strategies/catalog.py index d37111b..86fcbc8 100644 --- a/src/us_equity_strategies/catalog.py +++ b/src/us_equity_strategies/catalog.py @@ -17,6 +17,7 @@ ) from .ai_extensions import build_default_ai_extension_config +from .income_layer_defaults import income_layer_default_config GLOBAL_ETF_ROTATION_PROFILE = "global_etf_rotation" # Legacy alias retained for lookups and docs; runtime registry is canonical rotation. @@ -24,11 +25,8 @@ TQQQ_GROWTH_INCOME_PROFILE = "tqqq_growth_income" SOXL_SOXX_TREND_INCOME_PROFILE = "soxl_soxx_trend_income" RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive" -TECH_COMMUNICATION_PULLBACK_ENHANCEMENT_PROFILE = "tech_communication_pullback_enhancement" MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE = "mega_cap_leader_rotation_top50_balanced" NASDAQ_SP500_SMART_DCA_PROFILE = "nasdaq_sp500_smart_dca" -QQQ_TECH_ENHANCEMENT_LEGACY_PROFILE = "qqq_tech_enhancement" -QQQ_TECH_ENHANCEMENT_PROFILE = TECH_COMMUNICATION_PULLBACK_ENHANCEMENT_PROFILE FULL_SHARED_PLATFORM_MATRIX = frozenset( {"ibkr", "schwab", "longbridge", "firstrade", "paper_signal"} ) @@ -39,7 +37,6 @@ TQQQ_GROWTH_INCOME_PROFILE: FULL_SHARED_PLATFORM_MATRIX, SOXL_SOXX_TREND_INCOME_PROFILE: FULL_SHARED_PLATFORM_MATRIX, RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: FULL_SHARED_PLATFORM_MATRIX, - QQQ_TECH_ENHANCEMENT_PROFILE: FULL_SHARED_PLATFORM_MATRIX, MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: FULL_SHARED_PLATFORM_MATRIX, NASDAQ_SP500_SMART_DCA_PROFILE: FULL_SHARED_PLATFORM_MATRIX, } @@ -49,7 +46,6 @@ TQQQ_GROWTH_INCOME_PROFILE: frozenset({"benchmark_history", "portfolio_snapshot"}), SOXL_SOXX_TREND_INCOME_PROFILE: frozenset({"derived_indicators", "portfolio_snapshot"}), RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: frozenset({"feature_snapshot"}), - QQQ_TECH_ENHANCEMENT_PROFILE: frozenset({"feature_snapshot"}), MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: frozenset({"feature_snapshot"}), NASDAQ_SP500_SMART_DCA_PROFILE: frozenset({"market_history", "portfolio_snapshot"}), } @@ -74,22 +70,7 @@ "confidence_volatility_gate_enabled": True, "confidence_volatility_window": 126, "confidence_volatility_max_ratio": 1.3, - "income_layer_enabled": True, - "income_layer_start_usd": 500000.0, - "income_layer_max_ratio": 0.15, - "income_layer_activation_band_ratio": 0.10, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.18, - "income_layer_base_loss_budget_ratio": 0.025, - "income_layer_min_loss_budget_ratio": 0.020, - "income_layer_loss_budget_decay_per_double": 0.0025, - "income_layer_allocations": { - "SCHD": 0.40, - "DGRO": 0.25, - "SGOV": 0.30, - "SPYI": 0.05, - }, + **income_layer_default_config(GLOBAL_ETF_ROTATION_PROFILE), "market_regime_control_enabled": True, "market_regime_control_apply_risk_reduced": True, "market_regime_control_apply_risk_off": True, @@ -104,25 +85,7 @@ "cash_reserve_ratio": 0.02, "rebalance_threshold_ratio": 0.01, "execution_cash_reserve_ratio": 0.0, - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.50, - "income_layer_activation_band_ratio": 0.20, - "income_layer_ratio_mode": "log_cap", - "income_layer_log_growth_factor": 0.70, - "income_layer_stress_drawdown_ratio": 0.30, - "income_layer_base_loss_budget_ratio": 0.08, - "income_layer_min_loss_budget_ratio": 0.06, - "income_layer_loss_budget_decay_per_double": 0.01, - "income_layer_qqqi_weight": 0.02, - "income_layer_spyi_weight": 0.08, - "income_layer_allocations": { - "SCHD": 0.30, - "DGRO": 0.20, - "SGOV": 0.40, - "SPYI": 0.08, - "QQQI": 0.02, - }, + **income_layer_default_config(TQQQ_GROWTH_INCOME_PROFILE), "option_growth_overlay_enabled": True, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": 250000.0, @@ -154,25 +117,7 @@ "min_trade_ratio": 0.01, "min_trade_floor": 100.0, "rebalance_threshold_ratio": 0.01, - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.95, - "income_layer_activation_band_ratio": 0.20, - "income_layer_ratio_mode": "log_cap", - "income_layer_log_growth_factor": 0.70, - "income_layer_stress_drawdown_ratio": 0.30, - "income_layer_base_loss_budget_ratio": 0.08, - "income_layer_min_loss_budget_ratio": 0.06, - "income_layer_loss_budget_decay_per_double": 0.01, - "income_layer_qqqi_weight": 0.01, - "income_layer_spyi_weight": 0.04, - "income_layer_allocations": { - "SCHD": 0.25, - "DGRO": 0.15, - "SGOV": 0.55, - "SPYI": 0.04, - "QQQI": 0.01, - }, + **income_layer_default_config(SOXL_SOXX_TREND_INCOME_PROFILE), "option_income_overlay_enabled": True, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": 1000000.0, @@ -209,70 +154,7 @@ "hard_defense_exposure": 0.10, "soft_breadth_threshold": 0.55, "hard_breadth_threshold": 0.35, - "income_layer_enabled": True, - "income_layer_start_usd": 400000.0, - "income_layer_max_ratio": 0.20, - "income_layer_activation_band_ratio": 0.10, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.18, - "income_layer_base_loss_budget_ratio": 0.030, - "income_layer_min_loss_budget_ratio": 0.025, - "income_layer_loss_budget_decay_per_double": 0.0025, - "income_layer_allocations": { - "SCHD": 0.45, - "DGRO": 0.30, - "SGOV": 0.25, - }, - "market_regime_control_enabled": True, - "market_regime_control_apply_risk_reduced": True, - "market_regime_control_apply_risk_off": True, - "market_regime_control_risk_reduced_scalar": 0.50, - "market_regime_control_risk_off_scalar": 0.0, - }, - QQQ_TECH_ENHANCEMENT_PROFILE: { - "benchmark_symbol": "QQQ", - "safe_haven": "BOXX", - "holdings_count": 8, - "single_name_cap": 0.10, - "sector_cap": 0.40, - "min_position_value_usd": 3000.0, - "max_dynamic_single_name_cap": 0.40, - "max_dynamic_sector_cap": 0.60, - "hold_bonus": 0.10, - "risk_on_exposure": 0.80, - "soft_defense_exposure": 0.60, - "hard_defense_exposure": 0.00, - "soft_breadth_threshold": 0.55, - "hard_breadth_threshold": 0.35, - "min_adv20_usd": 50000000.0, - "sector_whitelist": ("Information Technology", "Communication"), - "normalization": "universe_cross_sectional", - "score_template": "balanced_pullback", - "runtime_execution_window_trading_days": 3, - "execution_cash_reserve_ratio": 0.0, - "residual_proxy": "simple_excess_return_vs_QQQ", - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.30, - "income_layer_activation_band_ratio": 0.15, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.60, - "income_layer_stress_drawdown_ratio": 0.22, - "income_layer_base_loss_budget_ratio": 0.050, - "income_layer_min_loss_budget_ratio": 0.040, - "income_layer_loss_budget_decay_per_double": 0.0050, - "income_layer_allocations": { - "SCHD": 0.40, - "DGRO": 0.25, - "SGOV": 0.20, - "SPYI": 0.10, - "QQQI": 0.05, - }, - "option_growth_overlay_enabled": True, - "option_growth_overlay_recipe": "qqq_leaps_growth_v1", - "option_growth_overlay_start_usd": 1000000.0, - "option_growth_overlay_nav_budget_ratio": 0.03, + **income_layer_default_config(RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE), "market_regime_control_enabled": True, "market_regime_control_apply_risk_reduced": True, "market_regime_control_apply_risk_off": True, @@ -301,22 +183,7 @@ "min_adv20_usd": 20000000.0, "runtime_execution_window_trading_days": 3, "execution_cash_reserve_ratio": 0.0, - "income_layer_enabled": True, - "income_layer_start_usd": 300000.0, - "income_layer_max_ratio": 0.25, - "income_layer_activation_band_ratio": 0.15, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.20, - "income_layer_base_loss_budget_ratio": 0.040, - "income_layer_min_loss_budget_ratio": 0.030, - "income_layer_loss_budget_decay_per_double": 0.0050, - "income_layer_allocations": { - "SCHD": 0.45, - "DGRO": 0.30, - "SGOV": 0.20, - "SPYI": 0.05, - }, + **income_layer_default_config(MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE), "option_growth_overlay_enabled": True, "option_growth_overlay_recipe": "qqq_leaps_growth_v1", "option_growth_overlay_start_usd": 1000000.0, @@ -367,7 +234,6 @@ TQQQ_GROWTH_INCOME_PROFILE: "tqqq_growth_income_entrypoint", SOXL_SOXX_TREND_INCOME_PROFILE: "soxl_soxx_trend_income_entrypoint", RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: "russell_1000_multi_factor_defensive_entrypoint", - QQQ_TECH_ENHANCEMENT_PROFILE: "qqq_tech_enhancement_entrypoint", MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: "mega_cap_leader_rotation_top50_balanced_entrypoint", NASDAQ_SP500_SMART_DCA_PROFILE: "nasdaq_sp500_smart_dca_entrypoint", } @@ -377,16 +243,11 @@ TQQQ_GROWTH_INCOME_PROFILE: "value", SOXL_SOXX_TREND_INCOME_PROFILE: "value", RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: "weight", - QQQ_TECH_ENHANCEMENT_PROFILE: "weight", MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: "weight", NASDAQ_SP500_SMART_DCA_PROFILE: "value", } -STRATEGY_BUNDLED_CONFIG_RELPATHS: dict[str, str] = { - QQQ_TECH_ENHANCEMENT_PROFILE: ( - "package://us_equity_strategies/configs/tech_communication_pullback_enhancement.json" - ), -} +STRATEGY_BUNDLED_CONFIG_RELPATHS: dict[str, str] = {} # `supported_platforms` 仍保留为兼容镜像,避免一次性改动所有平台 runtime。 @@ -439,11 +300,6 @@ def _build_strategy_definition( component_name="signal_logic", module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive", ), - QQQ_TECH_ENHANCEMENT_PROFILE: _build_strategy_definition( - QQQ_TECH_ENHANCEMENT_PROFILE, - component_name="signal_logic", - module_path="us_equity_strategies.strategies.qqq_tech_enhancement", - ), MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: _build_strategy_definition( MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE, component_name="signal_logic", @@ -502,17 +358,6 @@ def _build_strategy_definition( role="defensive_stock_baseline", status="runtime_enabled", ), - QQQ_TECH_ENHANCEMENT_PROFILE: StrategyMetadata( - canonical_profile=QQQ_TECH_ENHANCEMENT_PROFILE, - display_name="Tech/Communication Pullback Enhancement", - description="Tech-heavy monthly stock selection with controlled pullback entry and explicit BOXX cash buffer.", - aliases=(QQQ_TECH_ENHANCEMENT_LEGACY_PROFILE,), - cadence="monthly", - asset_scope="us_tech_communication_stocks", - benchmark="QQQ", - role="parallel_cash_buffer_branch", - status="research_only", - ), MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: StrategyMetadata( canonical_profile=MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE, display_name="Mega Cap Leader Rotation Top50 Balanced", diff --git a/src/us_equity_strategies/entrypoints/__init__.py b/src/us_equity_strategies/entrypoints/__init__.py index aca333b..e32c5b6 100644 --- a/src/us_equity_strategies/entrypoints/__init__.py +++ b/src/us_equity_strategies/entrypoints/__init__.py @@ -21,7 +21,6 @@ global_etf_rotation_manifest, mega_cap_leader_rotation_top50_balanced_manifest, nasdaq_sp500_smart_dca_manifest, - qqq_tech_enhancement_manifest, russell_1000_multi_factor_defensive_manifest, soxl_soxx_trend_income_manifest, tqqq_growth_income_manifest, @@ -33,7 +32,6 @@ tqqq_growth_income as tqqq_growth_income_strategy, russell_1000_multi_factor_defensive as legacy_russell, soxl_soxx_trend_income as soxl_soxx_trend_income_strategy, - qqq_tech_enhancement as qqq_tech_enhancement_strategy, ) from ._common import ( @@ -764,101 +762,6 @@ def evaluate_russell_1000_multi_factor_defensive(ctx: StrategyContext) -> Strate ) -def evaluate_qqq_tech_enhancement(ctx: StrategyContext) -> StrategyDecision: - config = merge_runtime_config(qqq_tech_enhancement_manifest.default_config, ctx) - income_layer_config = pop_income_layer_config(config) - option_overlay_config = pop_option_overlay_config(config) - market_regime_control_config = pop_market_regime_control_config(config) - translator = config.get("translator", default_translator) - config.pop("signal_effective_after_trading_days", None) - pop_execution_only_config(config) - if ctx.portfolio is not None and "portfolio_total_equity" not in config: - total_equity = getattr(ctx.portfolio, "total_equity", None) - if total_equity is not None: - config["portfolio_total_equity"] = float(total_equity) - weights, signal_desc, is_emergency, status_desc, metadata = qqq_tech_enhancement_strategy.compute_signals( - require_market_data(ctx, "feature_snapshot"), - get_current_holdings(ctx), - **config, - ) - weights, income_layer_diagnostics = apply_income_layer_to_weights( - weights, - income_layer_config=income_layer_config, - ctx=ctx, - excluded_symbols=(config.get("safe_haven"), config.get("benchmark_symbol")), - ) - weights, market_regime_control_diagnostics = apply_market_regime_control_to_weights( - weights, - market_regime_control_config=market_regime_control_config, - ctx=ctx, - safe_haven=str(config.get("safe_haven", "BOXX")), - excluded_symbols=(config.get("safe_haven"), config.get("benchmark_symbol")), - ) - rendered_signal_desc, rendered_status_desc, notification_context = _render_notification_displays( - signal_desc, - status_desc, - metadata, - translator=translator, - ) - notification_context = _merge_notification_contexts( - notification_context, - market_regime_control_diagnostics.get("market_regime_control_notification_context"), - ) - option_overlay_diagnostics = build_option_overlay_diagnostics( - option_overlay_config, - ctx, - base_diagnostics=metadata, - ) - diagnostics = { - **metadata, - **income_layer_diagnostics, - **market_regime_control_diagnostics, - **option_overlay_diagnostics, - "signal_description": rendered_signal_desc, - "status_description": rendered_status_desc, - "signal_source": qqq_tech_enhancement_strategy.SIGNAL_SOURCE, - "actionable": weights is not None, - } - diagnostics.update(_account_size_diagnostics(qqq_tech_enhancement_manifest.profile, ctx)) - diagnostics["signal_description"] = append_account_size_warning( - str(diagnostics["signal_description"]), - diagnostics, - translator=translator, - ) - _attach_dashboard_text( - diagnostics, - _build_dashboard_text( - ctx, - strategy_symbols=_symbols_from_sources( - metadata.get("managed_symbols"), - weights or {}, - get_current_holdings(ctx), - config.get("safe_haven"), - ), - translator=translator, - signal_text=diagnostics["signal_description"], - ), - ) - _attach_notification_context(diagnostics, notification_context) - risk_flags: tuple[str, ...] = () - if is_emergency: - risk_flags += ("hard_defense",) - if weights is None: - risk_flags += ("no_execute",) - return StrategyDecision( - positions=weights_to_positions(weights, safe_haven=str(config.get("safe_haven", "BOXX"))), - risk_flags=risk_flags, - diagnostics=diagnostics, - ) - - -qqq_tech_enhancement_strategy.compute_signals.__doc__ = ( - ((qqq_tech_enhancement_strategy.compute_signals.__doc__ or "").strip() + - "\n\nLegacy adapter: prefer us_equity_strategies entrypoints for new integrations.") - .strip() -) - - def _evaluate_mega_cap_leader_rotation_snapshot_profile( ctx: StrategyContext, *, @@ -1080,10 +983,6 @@ def evaluate_nasdaq_sp500_smart_dca(ctx: StrategyContext) -> StrategyDecision: manifest=russell_1000_multi_factor_defensive_manifest, _evaluate=evaluate_russell_1000_multi_factor_defensive, ) -qqq_tech_enhancement_entrypoint = CallableStrategyEntrypoint( - manifest=qqq_tech_enhancement_manifest, - _evaluate=evaluate_qqq_tech_enhancement, -) mega_cap_leader_rotation_top50_balanced_entrypoint = CallableStrategyEntrypoint( manifest=mega_cap_leader_rotation_top50_balanced_manifest, _evaluate=evaluate_mega_cap_leader_rotation_top50_balanced, @@ -1098,7 +997,6 @@ def evaluate_nasdaq_sp500_smart_dca(ctx: StrategyContext) -> StrategyDecision: "global_etf_rotation_entrypoint", "tqqq_growth_income_entrypoint", "soxl_soxx_trend_income_entrypoint", - "qqq_tech_enhancement_entrypoint", "russell_1000_multi_factor_defensive_entrypoint", "mega_cap_leader_rotation_top50_balanced_entrypoint", "nasdaq_sp500_smart_dca_entrypoint", @@ -1106,7 +1004,6 @@ def evaluate_nasdaq_sp500_smart_dca(ctx: StrategyContext) -> StrategyDecision: "evaluate_tqqq_growth_income", "evaluate_soxl_soxx_trend_income", "evaluate_russell_1000_multi_factor_defensive", - "evaluate_qqq_tech_enhancement", "evaluate_mega_cap_leader_rotation_top50_balanced", "evaluate_nasdaq_sp500_smart_dca", ] diff --git a/src/us_equity_strategies/entrypoints/_common.py b/src/us_equity_strategies/entrypoints/_common.py index 8fc07c3..61352ff 100644 --- a/src/us_equity_strategies/entrypoints/_common.py +++ b/src/us_equity_strategies/entrypoints/_common.py @@ -18,11 +18,11 @@ "income_layer_max_ratio", "income_layer_activation_band_ratio", "income_layer_ratio_mode", - "income_layer_log_growth_factor", - "income_layer_stress_drawdown_ratio", - "income_layer_base_loss_budget_ratio", - "income_layer_min_loss_budget_ratio", - "income_layer_loss_budget_decay_per_double", + "income_layer_core_stress_drawdown_ratio", + "income_layer_income_stress_drawdown_ratio", + "income_layer_base_drawdown_budget_ratio", + "income_layer_min_drawdown_budget_ratio", + "income_layer_drawdown_budget_decay_per_double", "income_layer_qqqi_weight", "income_layer_spyi_weight", "income_layer_allocations", @@ -857,14 +857,26 @@ def apply_income_layer_to_weights( "income_layer_activation_band_ratio", 0.0, ), - income_layer_ratio_mode=income_layer_config.get("income_layer_ratio_mode", "linear_cap"), - income_layer_log_growth_factor=income_layer_config.get("income_layer_log_growth_factor", 0.70), - income_layer_stress_drawdown_ratio=income_layer_config.get("income_layer_stress_drawdown_ratio", 0.30), - income_layer_base_loss_budget_ratio=income_layer_config.get("income_layer_base_loss_budget_ratio", 0.08), - income_layer_min_loss_budget_ratio=income_layer_config.get("income_layer_min_loss_budget_ratio", 0.06), - income_layer_loss_budget_decay_per_double=income_layer_config.get( - "income_layer_loss_budget_decay_per_double", - 0.01, + income_layer_ratio_mode=income_layer_config.get("income_layer_ratio_mode", "log_total_drawdown_budget"), + income_layer_core_stress_drawdown_ratio=income_layer_config.get( + "income_layer_core_stress_drawdown_ratio", + 0.40, + ), + income_layer_income_stress_drawdown_ratio=income_layer_config.get( + "income_layer_income_stress_drawdown_ratio", + 0.08, + ), + income_layer_base_drawdown_budget_ratio=income_layer_config.get( + "income_layer_base_drawdown_budget_ratio", + 0.30, + ), + income_layer_min_drawdown_budget_ratio=income_layer_config.get( + "income_layer_min_drawdown_budget_ratio", + 0.15, + ), + income_layer_drawdown_budget_decay_per_double=income_layer_config.get( + "income_layer_drawdown_budget_decay_per_double", + 0.05, ), ) locked_ratio = min(1.0, plan.locked_value / total_equity) diff --git a/src/us_equity_strategies/income_layer.py b/src/us_equity_strategies/income_layer.py index d591f54..0eaae2b 100644 --- a/src/us_equity_strategies/income_layer.py +++ b/src/us_equity_strategies/income_layer.py @@ -5,13 +5,9 @@ import numpy as np -INCOME_LAYER_RATIO_MODE_LINEAR_CAP = "linear_cap" -INCOME_LAYER_RATIO_MODE_LOG_CAP = "log_cap" -INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET = "log_loss_budget" +INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET = "log_total_drawdown_budget" INCOME_LAYER_RATIO_MODES = { - INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - INCOME_LAYER_RATIO_MODE_LOG_CAP, - INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET, + INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, } @@ -118,12 +114,12 @@ def get_income_layer_ratio( income_layer_max_ratio, income_layer_enabled=True, income_layer_activation_band_ratio=0.0, - income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, ): ratio, _diagnostics = resolve_income_layer_ratio( total_equity_usd, @@ -132,11 +128,11 @@ def get_income_layer_ratio( income_layer_enabled=income_layer_enabled, income_layer_activation_band_ratio=income_layer_activation_band_ratio, income_layer_ratio_mode=income_layer_ratio_mode, - income_layer_log_growth_factor=income_layer_log_growth_factor, - income_layer_stress_drawdown_ratio=income_layer_stress_drawdown_ratio, - income_layer_base_loss_budget_ratio=income_layer_base_loss_budget_ratio, - income_layer_min_loss_budget_ratio=income_layer_min_loss_budget_ratio, - income_layer_loss_budget_decay_per_double=income_layer_loss_budget_decay_per_double, + income_layer_core_stress_drawdown_ratio=income_layer_core_stress_drawdown_ratio, + income_layer_income_stress_drawdown_ratio=income_layer_income_stress_drawdown_ratio, + income_layer_base_drawdown_budget_ratio=income_layer_base_drawdown_budget_ratio, + income_layer_min_drawdown_budget_ratio=income_layer_min_drawdown_budget_ratio, + income_layer_drawdown_budget_decay_per_double=income_layer_drawdown_budget_decay_per_double, ) return ratio @@ -148,15 +144,17 @@ def resolve_income_layer_ratio( income_layer_max_ratio, income_layer_enabled=True, income_layer_activation_band_ratio=0.0, - income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, ): enabled = as_bool(income_layer_enabled, default=True) - mode = str(income_layer_ratio_mode or INCOME_LAYER_RATIO_MODE_LINEAR_CAP).strip().lower() + mode = str( + income_layer_ratio_mode or INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET + ).strip().lower() if mode not in INCOME_LAYER_RATIO_MODES: modes = ", ".join(sorted(INCOME_LAYER_RATIO_MODES)) raise ValueError(f"Unsupported income layer ratio mode: {mode!r}; expected one of {modes}") @@ -178,95 +176,81 @@ def resolve_income_layer_ratio( "income_layer_activation_multiplier": 0.0, "income_layer_activation_end_usd": activation_end_usd, "income_layer_ratio_mode": mode, - "income_layer_log_ratio": 0.0, - "income_layer_loss_budget_ratio": 0.0, - "income_layer_loss_budget_cap_ratio": 0.0, - "income_layer_stress_drawdown_ratio": 0.0, + "income_layer_required_ratio": 0.0, + "income_layer_ratio_before_activation": 0.0, + "income_layer_account_drawdown_budget_ratio": 0.0, + "income_layer_account_stress_drawdown_ratio": 0.0, + "income_layer_drawdown_budget_gap_ratio": 0.0, + "income_layer_drawdown_budget_met": True, + "income_layer_core_stress_drawdown_ratio": 0.0, + "income_layer_income_stress_drawdown_ratio": 0.0, } if start <= 0.0: - linear_ratio = max_ratio doubles_since_start = 0.0 - elif total <= (start * 2): - linear_ratio = float( - np.interp( - total, - [start, start * 2], - [0.0, max_ratio], - ) - ) - doubles_since_start = max(0.0, float(np.log2(total / start))) else: - linear_ratio = max_ratio doubles_since_start = max(0.0, float(np.log2(total / start))) activation_multiplier = 1.0 if activation_band_usd > 0.0: activation_multiplier = max(0.0, min(1.0, (total - start) / activation_band_usd)) - if mode == INCOME_LAYER_RATIO_MODE_LINEAR_CAP: - return linear_ratio * activation_multiplier, { - "income_layer_enabled": enabled, - "income_layer_activation_band_ratio": activation_band_ratio, - "income_layer_activation_multiplier": activation_multiplier, - "income_layer_activation_end_usd": activation_end_usd, - "income_layer_ratio_mode": mode, - "income_layer_log_ratio": linear_ratio, - "income_layer_loss_budget_ratio": np.nan, - "income_layer_loss_budget_cap_ratio": max_ratio, - "income_layer_stress_drawdown_ratio": np.nan, - } - - growth_factor = max(0.0, float(income_layer_log_growth_factor or 0.0)) - log_ratio = max_ratio * (1.0 - float(np.exp(-growth_factor * doubles_since_start))) - if mode == INCOME_LAYER_RATIO_MODE_LOG_CAP: - return min(max_ratio, log_ratio) * activation_multiplier, { - "income_layer_enabled": enabled, - "income_layer_activation_band_ratio": activation_band_ratio, - "income_layer_activation_multiplier": activation_multiplier, - "income_layer_activation_end_usd": activation_end_usd, - "income_layer_ratio_mode": mode, - "income_layer_log_ratio": log_ratio, - "income_layer_loss_budget_ratio": np.nan, - "income_layer_loss_budget_cap_ratio": max_ratio, - "income_layer_stress_drawdown_ratio": np.nan, - } - - stress_drawdown_ratio = as_clamped_ratio( - income_layer_stress_drawdown_ratio, - default=0.30, + core_stress_drawdown_ratio = as_clamped_ratio( + income_layer_core_stress_drawdown_ratio, + default=0.45, upper=1.0, ) - base_loss_budget_ratio = as_clamped_ratio( - income_layer_base_loss_budget_ratio, + income_stress_drawdown_ratio = as_clamped_ratio( + income_layer_income_stress_drawdown_ratio, default=0.08, upper=1.0, ) - min_loss_budget_ratio = as_clamped_ratio( - income_layer_min_loss_budget_ratio, - default=min(base_loss_budget_ratio, 0.06), + base_drawdown_budget_ratio = as_clamped_ratio( + income_layer_base_drawdown_budget_ratio, + default=0.45, upper=1.0, ) - min_loss_budget_ratio = min(min_loss_budget_ratio, base_loss_budget_ratio) - loss_budget_decay = max(0.0, float(income_layer_loss_budget_decay_per_double or 0.0)) - loss_budget_ratio = max( - min_loss_budget_ratio, - base_loss_budget_ratio - loss_budget_decay * doubles_since_start, + min_drawdown_budget_ratio = as_clamped_ratio( + income_layer_min_drawdown_budget_ratio, + default=min(base_drawdown_budget_ratio, 0.25), + upper=1.0, + ) + min_drawdown_budget_ratio = min(min_drawdown_budget_ratio, base_drawdown_budget_ratio) + budget_decay = max(0.0, float(income_layer_drawdown_budget_decay_per_double or 0.0)) + account_drawdown_budget_ratio = max( + min_drawdown_budget_ratio, + base_drawdown_budget_ratio - budget_decay * doubles_since_start, + ) + if core_stress_drawdown_ratio <= income_stress_drawdown_ratio: + required_ratio = 0.0 if account_drawdown_budget_ratio >= core_stress_drawdown_ratio else max_ratio + else: + required_ratio = ( + (core_stress_drawdown_ratio - account_drawdown_budget_ratio) + / (core_stress_drawdown_ratio - income_stress_drawdown_ratio) + ) + required_ratio = max(0.0, required_ratio) + ratio_before_activation = min(max_ratio, required_ratio) + ratio = ratio_before_activation * activation_multiplier + account_stress_drawdown_ratio = ( + ratio * income_stress_drawdown_ratio + + (1.0 - ratio) * core_stress_drawdown_ratio ) - loss_budget_cap_ratio = max_ratio - if stress_drawdown_ratio > 0.0: - loss_budget_cap_ratio = min(max_ratio, loss_budget_ratio / stress_drawdown_ratio) + drawdown_budget_gap_ratio = max(0.0, account_stress_drawdown_ratio - account_drawdown_budget_ratio) - return min(max_ratio, log_ratio, loss_budget_cap_ratio) * activation_multiplier, { + return ratio, { "income_layer_enabled": enabled, "income_layer_activation_band_ratio": activation_band_ratio, "income_layer_activation_multiplier": activation_multiplier, "income_layer_activation_end_usd": activation_end_usd, "income_layer_ratio_mode": mode, - "income_layer_log_ratio": log_ratio, - "income_layer_loss_budget_ratio": loss_budget_ratio, - "income_layer_loss_budget_cap_ratio": loss_budget_cap_ratio, - "income_layer_stress_drawdown_ratio": stress_drawdown_ratio, + "income_layer_required_ratio": required_ratio, + "income_layer_ratio_before_activation": ratio_before_activation, + "income_layer_account_drawdown_budget_ratio": account_drawdown_budget_ratio, + "income_layer_account_stress_drawdown_ratio": account_stress_drawdown_ratio, + "income_layer_drawdown_budget_gap_ratio": drawdown_budget_gap_ratio, + "income_layer_drawdown_budget_met": drawdown_budget_gap_ratio <= 1e-12, + "income_layer_core_stress_drawdown_ratio": core_stress_drawdown_ratio, + "income_layer_income_stress_drawdown_ratio": income_stress_drawdown_ratio, } @@ -279,12 +263,12 @@ def build_income_layer_plan( income_layer_max_ratio, income_layer_enabled=True, income_layer_activation_band_ratio=0.0, - income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, ) -> IncomeLayerPlan: income_symbols = tuple(allocations) ratio, diagnostics = resolve_income_layer_ratio( @@ -294,11 +278,11 @@ def build_income_layer_plan( income_layer_enabled=income_layer_enabled, income_layer_activation_band_ratio=income_layer_activation_band_ratio, income_layer_ratio_mode=income_layer_ratio_mode, - income_layer_log_growth_factor=income_layer_log_growth_factor, - income_layer_stress_drawdown_ratio=income_layer_stress_drawdown_ratio, - income_layer_base_loss_budget_ratio=income_layer_base_loss_budget_ratio, - income_layer_min_loss_budget_ratio=income_layer_min_loss_budget_ratio, - income_layer_loss_budget_decay_per_double=income_layer_loss_budget_decay_per_double, + income_layer_core_stress_drawdown_ratio=income_layer_core_stress_drawdown_ratio, + income_layer_income_stress_drawdown_ratio=income_layer_income_stress_drawdown_ratio, + income_layer_base_drawdown_budget_ratio=income_layer_base_drawdown_budget_ratio, + income_layer_min_drawdown_budget_ratio=income_layer_min_drawdown_budget_ratio, + income_layer_drawdown_budget_decay_per_double=income_layer_drawdown_budget_decay_per_double, ) current_value = sum(float(market_values.get(symbol, 0.0)) for symbol in income_symbols) desired_value = float(total_equity_usd or 0.0) * ratio @@ -322,9 +306,7 @@ def build_income_layer_plan( __all__ = [ - "INCOME_LAYER_RATIO_MODE_LINEAR_CAP", - "INCOME_LAYER_RATIO_MODE_LOG_CAP", - "INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET", + "INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET", "INCOME_LAYER_RATIO_MODES", "IncomeLayerPlan", "as_bool", diff --git a/src/us_equity_strategies/income_layer_defaults.py b/src/us_equity_strategies/income_layer_defaults.py new file mode 100644 index 0000000..427fb3d --- /dev/null +++ b/src/us_equity_strategies/income_layer_defaults.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from copy import deepcopy + +INCOME_LAYER_RATIO_MODE = "log_total_drawdown_budget" + +GLOBAL_ETF_ROTATION_PROFILE = "global_etf_rotation" +TQQQ_GROWTH_INCOME_PROFILE = "tqqq_growth_income" +SOXL_SOXX_TREND_INCOME_PROFILE = "soxl_soxx_trend_income" +RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive" +MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE = "mega_cap_leader_rotation_top50_balanced" + +INCOME_LAYER_DEFAULT_CONFIGS: dict[str, dict[str, object]] = { + GLOBAL_ETF_ROTATION_PROFILE: { + "income_layer_enabled": True, + "income_layer_start_usd": 500000.0, + "income_layer_max_ratio": 0.15, + "income_layer_activation_band_ratio": 0.10, + "income_layer_ratio_mode": INCOME_LAYER_RATIO_MODE, + "income_layer_core_stress_drawdown_ratio": 0.30, + "income_layer_income_stress_drawdown_ratio": 0.08, + "income_layer_base_drawdown_budget_ratio": 0.30, + "income_layer_min_drawdown_budget_ratio": 0.267, + "income_layer_drawdown_budget_decay_per_double": 0.015, + "income_layer_allocations": { + "SCHD": 0.40, + "DGRO": 0.25, + "SGOV": 0.30, + "SPYI": 0.05, + }, + }, + TQQQ_GROWTH_INCOME_PROFILE: { + "income_layer_enabled": True, + "income_layer_start_usd": 250000.0, + "income_layer_max_ratio": 0.55, + "income_layer_activation_band_ratio": 0.20, + "income_layer_ratio_mode": INCOME_LAYER_RATIO_MODE, + "income_layer_core_stress_drawdown_ratio": 0.45, + "income_layer_income_stress_drawdown_ratio": 0.08, + "income_layer_base_drawdown_budget_ratio": 0.45, + "income_layer_min_drawdown_budget_ratio": 0.25, + "income_layer_drawdown_budget_decay_per_double": 0.05, + "income_layer_qqqi_weight": 0.02, + "income_layer_spyi_weight": 0.08, + "income_layer_allocations": { + "SCHD": 0.30, + "DGRO": 0.20, + "SGOV": 0.40, + "SPYI": 0.08, + "QQQI": 0.02, + }, + }, + SOXL_SOXX_TREND_INCOME_PROFILE: { + "income_layer_enabled": True, + "income_layer_start_usd": 150000.0, + "income_layer_max_ratio": 0.95, + "income_layer_activation_band_ratio": 0.20, + "income_layer_ratio_mode": INCOME_LAYER_RATIO_MODE, + "income_layer_core_stress_drawdown_ratio": 0.45, + "income_layer_income_stress_drawdown_ratio": 0.06, + "income_layer_base_drawdown_budget_ratio": 0.45, + "income_layer_min_drawdown_budget_ratio": 0.25, + "income_layer_drawdown_budget_decay_per_double": 0.05, + "income_layer_qqqi_weight": 0.01, + "income_layer_spyi_weight": 0.04, + "income_layer_allocations": { + "SCHD": 0.15, + "DGRO": 0.10, + "SGOV": 0.70, + "SPYI": 0.04, + "QQQI": 0.01, + }, + }, + RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: { + "income_layer_enabled": True, + "income_layer_start_usd": 400000.0, + "income_layer_max_ratio": 0.20, + "income_layer_activation_band_ratio": 0.10, + "income_layer_ratio_mode": INCOME_LAYER_RATIO_MODE, + "income_layer_core_stress_drawdown_ratio": 0.30, + "income_layer_income_stress_drawdown_ratio": 0.08, + "income_layer_base_drawdown_budget_ratio": 0.30, + "income_layer_min_drawdown_budget_ratio": 0.256, + "income_layer_drawdown_budget_decay_per_double": 0.015, + "income_layer_allocations": { + "SCHD": 0.45, + "DGRO": 0.30, + "SGOV": 0.25, + }, + }, + MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: { + "income_layer_enabled": True, + "income_layer_start_usd": 300000.0, + "income_layer_max_ratio": 0.25, + "income_layer_activation_band_ratio": 0.15, + "income_layer_ratio_mode": INCOME_LAYER_RATIO_MODE, + "income_layer_core_stress_drawdown_ratio": 0.35, + "income_layer_income_stress_drawdown_ratio": 0.08, + "income_layer_base_drawdown_budget_ratio": 0.35, + "income_layer_min_drawdown_budget_ratio": 0.2825, + "income_layer_drawdown_budget_decay_per_double": 0.020, + "income_layer_allocations": { + "SCHD": 0.45, + "DGRO": 0.30, + "SGOV": 0.25, + }, + }, +} + + +def income_layer_default_config(profile: str) -> dict[str, object]: + return deepcopy(INCOME_LAYER_DEFAULT_CONFIGS[profile]) + + +__all__ = [ + "INCOME_LAYER_DEFAULT_CONFIGS", + "INCOME_LAYER_RATIO_MODE", + "income_layer_default_config", +] diff --git a/src/us_equity_strategies/manifests/__init__.py b/src/us_equity_strategies/manifests/__init__.py index 872ecf0..83fe57a 100644 --- a/src/us_equity_strategies/manifests/__init__.py +++ b/src/us_equity_strategies/manifests/__init__.py @@ -3,12 +3,11 @@ from quant_platform_kit.strategy_contracts import StrategyManifest from us_equity_strategies.ai_extensions import build_default_ai_extension_config +from us_equity_strategies.income_layer_defaults import income_layer_default_config -TECH_COMMUNICATION_PULLBACK_ENHANCEMENT_PROFILE = "tech_communication_pullback_enhancement" GLOBAL_ETF_CONFIDENCE_VOL_GATE_PROFILE = "global_etf_confidence_vol_gate" MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE = "mega_cap_leader_rotation_top50_balanced" NASDAQ_SP500_SMART_DCA_PROFILE = "nasdaq_sp500_smart_dca" -QQQ_TECH_ENHANCEMENT_LEGACY_PROFILE = "qqq_tech_enhancement" def _manifest( @@ -78,22 +77,7 @@ def _manifest( "confidence_volatility_gate_enabled": True, "confidence_volatility_window": 126, "confidence_volatility_max_ratio": 1.3, - "income_layer_enabled": True, - "income_layer_start_usd": 500000.0, - "income_layer_max_ratio": 0.15, - "income_layer_activation_band_ratio": 0.10, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.18, - "income_layer_base_loss_budget_ratio": 0.025, - "income_layer_min_loss_budget_ratio": 0.020, - "income_layer_loss_budget_decay_per_double": 0.0025, - "income_layer_allocations": { - "SCHD": 0.40, - "DGRO": 0.25, - "SGOV": 0.30, - "SPYI": 0.05, - }, + **income_layer_default_config("global_etf_rotation"), "market_regime_control_enabled": True, "market_regime_control_apply_risk_reduced": True, "market_regime_control_apply_risk_off": True, @@ -116,25 +100,7 @@ def _manifest( "cash_reserve_ratio": 0.02, "rebalance_threshold_ratio": 0.01, "execution_cash_reserve_ratio": 0.0, - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.50, - "income_layer_activation_band_ratio": 0.20, - "income_layer_ratio_mode": "log_cap", - "income_layer_log_growth_factor": 0.70, - "income_layer_stress_drawdown_ratio": 0.30, - "income_layer_base_loss_budget_ratio": 0.08, - "income_layer_min_loss_budget_ratio": 0.06, - "income_layer_loss_budget_decay_per_double": 0.01, - "income_layer_qqqi_weight": 0.02, - "income_layer_spyi_weight": 0.08, - "income_layer_allocations": { - "SCHD": 0.30, - "DGRO": 0.20, - "SGOV": 0.40, - "SPYI": 0.08, - "QQQI": 0.02, - }, + **income_layer_default_config("tqqq_growth_income"), "option_growth_overlay_enabled": True, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": 250000.0, @@ -173,25 +139,7 @@ def _manifest( "min_trade_ratio": 0.01, "min_trade_floor": 100.0, "rebalance_threshold_ratio": 0.01, - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.95, - "income_layer_activation_band_ratio": 0.20, - "income_layer_ratio_mode": "log_cap", - "income_layer_log_growth_factor": 0.70, - "income_layer_stress_drawdown_ratio": 0.30, - "income_layer_base_loss_budget_ratio": 0.08, - "income_layer_min_loss_budget_ratio": 0.06, - "income_layer_loss_budget_decay_per_double": 0.01, - "income_layer_qqqi_weight": 0.01, - "income_layer_spyi_weight": 0.04, - "income_layer_allocations": { - "SCHD": 0.25, - "DGRO": 0.15, - "SGOV": 0.55, - "SPYI": 0.04, - "QQQI": 0.01, - }, + **income_layer_default_config("soxl_soxx_trend_income"), "option_income_overlay_enabled": True, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": 1000000.0, @@ -236,78 +184,7 @@ def _manifest( "hard_defense_exposure": 0.10, "soft_breadth_threshold": 0.55, "hard_breadth_threshold": 0.35, - "income_layer_enabled": True, - "income_layer_start_usd": 400000.0, - "income_layer_max_ratio": 0.20, - "income_layer_activation_band_ratio": 0.10, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.18, - "income_layer_base_loss_budget_ratio": 0.030, - "income_layer_min_loss_budget_ratio": 0.025, - "income_layer_loss_budget_decay_per_double": 0.0025, - "income_layer_allocations": { - "SCHD": 0.45, - "DGRO": 0.30, - "SGOV": 0.25, - }, - "market_regime_control_enabled": True, - "market_regime_control_apply_risk_reduced": True, - "market_regime_control_apply_risk_off": True, - "market_regime_control_risk_reduced_scalar": 0.50, - "market_regime_control_risk_off_scalar": 0.0, - }, -) - -qqq_tech_enhancement_manifest = _manifest( - profile=TECH_COMMUNICATION_PULLBACK_ENHANCEMENT_PROFILE, - display_name="Tech/Communication Pullback Enhancement", - description="Tech-heavy monthly stock selection with controlled pullback entry and explicit BOXX cash buffer.", - aliases=(QQQ_TECH_ENHANCEMENT_LEGACY_PROFILE,), - required_inputs=frozenset({"feature_snapshot"}), - default_config={ - "benchmark_symbol": "QQQ", - "safe_haven": "BOXX", - "holdings_count": 8, - "single_name_cap": 0.10, - "sector_cap": 0.40, - "min_position_value_usd": 3000.0, - "max_dynamic_single_name_cap": 0.40, - "max_dynamic_sector_cap": 0.60, - "hold_bonus": 0.10, - "risk_on_exposure": 0.80, - "soft_defense_exposure": 0.60, - "hard_defense_exposure": 0.00, - "soft_breadth_threshold": 0.55, - "hard_breadth_threshold": 0.35, - "min_adv20_usd": 50000000.0, - "sector_whitelist": ("Information Technology", "Communication"), - "normalization": "universe_cross_sectional", - "score_template": "balanced_pullback", - "runtime_execution_window_trading_days": 3, - "execution_cash_reserve_ratio": 0.0, - "residual_proxy": "simple_excess_return_vs_QQQ", - "income_layer_enabled": True, - "income_layer_start_usd": 250000.0, - "income_layer_max_ratio": 0.30, - "income_layer_activation_band_ratio": 0.15, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.60, - "income_layer_stress_drawdown_ratio": 0.22, - "income_layer_base_loss_budget_ratio": 0.050, - "income_layer_min_loss_budget_ratio": 0.040, - "income_layer_loss_budget_decay_per_double": 0.0050, - "income_layer_allocations": { - "SCHD": 0.40, - "DGRO": 0.25, - "SGOV": 0.20, - "SPYI": 0.10, - "QQQI": 0.05, - }, - "option_growth_overlay_enabled": True, - "option_growth_overlay_recipe": "qqq_leaps_growth_v1", - "option_growth_overlay_start_usd": 1000000.0, - "option_growth_overlay_nav_budget_ratio": 0.03, + **income_layer_default_config("russell_1000_multi_factor_defensive"), "market_regime_control_enabled": True, "market_regime_control_apply_risk_reduced": True, "market_regime_control_apply_risk_off": True, @@ -344,22 +221,7 @@ def _manifest( "min_adv20_usd": 20000000.0, "runtime_execution_window_trading_days": 3, "execution_cash_reserve_ratio": 0.0, - "income_layer_enabled": True, - "income_layer_start_usd": 300000.0, - "income_layer_max_ratio": 0.25, - "income_layer_activation_band_ratio": 0.15, - "income_layer_ratio_mode": "log_loss_budget", - "income_layer_log_growth_factor": 0.55, - "income_layer_stress_drawdown_ratio": 0.20, - "income_layer_base_loss_budget_ratio": 0.040, - "income_layer_min_loss_budget_ratio": 0.030, - "income_layer_loss_budget_decay_per_double": 0.0050, - "income_layer_allocations": { - "SCHD": 0.45, - "DGRO": 0.30, - "SGOV": 0.20, - "SPYI": 0.05, - }, + **income_layer_default_config(MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE), "option_growth_overlay_enabled": True, "option_growth_overlay_recipe": "qqq_leaps_growth_v1", "option_growth_overlay_start_usd": 1000000.0, @@ -418,7 +280,6 @@ def _manifest( tqqq_growth_income_manifest.profile: tqqq_growth_income_manifest, soxl_soxx_trend_income_manifest.profile: soxl_soxx_trend_income_manifest, russell_1000_multi_factor_defensive_manifest.profile: russell_1000_multi_factor_defensive_manifest, - qqq_tech_enhancement_manifest.profile: qqq_tech_enhancement_manifest, mega_cap_leader_rotation_top50_balanced_manifest.profile: mega_cap_leader_rotation_top50_balanced_manifest, nasdaq_sp500_smart_dca_manifest.profile: nasdaq_sp500_smart_dca_manifest, } @@ -441,7 +302,6 @@ def get_strategy_manifest(profile: str) -> StrategyManifest: "global_etf_rotation_manifest", "tqqq_growth_income_manifest", "soxl_soxx_trend_income_manifest", - "qqq_tech_enhancement_manifest", "russell_1000_multi_factor_defensive_manifest", "mega_cap_leader_rotation_top50_balanced_manifest", "nasdaq_sp500_smart_dca_manifest", diff --git a/src/us_equity_strategies/runtime_adapters.py b/src/us_equity_strategies/runtime_adapters.py index 574b1e6..dd4b25d 100644 --- a/src/us_equity_strategies/runtime_adapters.py +++ b/src/us_equity_strategies/runtime_adapters.py @@ -13,7 +13,6 @@ from us_equity_strategies.catalog import ( MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE, NASDAQ_SP500_SMART_DCA_PROFILE, - QQQ_TECH_ENHANCEMENT_PROFILE, get_strategy_definition, get_strategy_definitions, resolve_canonical_profile, @@ -21,7 +20,6 @@ from us_equity_strategies.strategies import ( mega_cap_leader_rotation as mega_cap_leader_rotation_strategy, nasdaq_sp500_smart_dca as nasdaq_sp500_smart_dca_strategy, - qqq_tech_enhancement as qqq_tech_enhancement_strategy, russell_1000_multi_factor_defensive as legacy_russell, ) @@ -69,24 +67,6 @@ managed_symbols_extractor=legacy_russell.extract_managed_symbols, artifact_contract=StrategyArtifactContract(requires_snapshot_artifacts=True), ), - QQQ_TECH_ENHANCEMENT_PROFILE: StrategyRuntimeAdapter( - status_icon=qqq_tech_enhancement_strategy.STATUS_ICON, - required_feature_columns=qqq_tech_enhancement_strategy.REQUIRED_FEATURE_COLUMNS, - snapshot_date_columns=qqq_tech_enhancement_strategy.SNAPSHOT_DATE_COLUMNS, - max_snapshot_month_lag=qqq_tech_enhancement_strategy.MAX_SNAPSHOT_MONTH_LAG, - require_snapshot_manifest=qqq_tech_enhancement_strategy.REQUIRE_SNAPSHOT_MANIFEST, - snapshot_contract_version=qqq_tech_enhancement_strategy.SNAPSHOT_CONTRACT_VERSION, - runtime_parameter_loader=qqq_tech_enhancement_strategy.load_runtime_parameters, - managed_symbols_extractor=qqq_tech_enhancement_strategy.extract_managed_symbols, - artifact_contract=StrategyArtifactContract( - requires_snapshot_artifacts=True, - requires_snapshot_manifest_path=qqq_tech_enhancement_strategy.REQUIRE_SNAPSHOT_MANIFEST, - requires_strategy_config_path=True, - snapshot_contract_version=qqq_tech_enhancement_strategy.SNAPSHOT_CONTRACT_VERSION, - config_source_policy="bundled_or_env", - ), - runtime_policy=StrategyRuntimePolicy(reconciliation_output_policy="optional"), - ), MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE: StrategyRuntimeAdapter( status_icon=mega_cap_leader_rotation_strategy.STATUS_ICON, required_feature_columns=mega_cap_leader_rotation_strategy.REQUIRED_FEATURE_COLUMNS, @@ -145,20 +125,12 @@ def _build_runtime_adapter_for_platform( if normalized_platform == IBKR_PLATFORM: available_capabilities.add("broker_client") - runtime_policy = base_adapter.runtime_policy - if ( - canonical_profile == QQQ_TECH_ENHANCEMENT_PROFILE - and normalized_platform == LONGBRIDGE_PLATFORM - ): - runtime_policy = replace(runtime_policy, runtime_execution_window_trading_days=1) - return validate_strategy_runtime_adapter( replace( base_adapter, available_inputs=frozenset(available_inputs), available_capabilities=frozenset(available_capabilities), portfolio_input_name=portfolio_input_name, - runtime_policy=runtime_policy, ) ) diff --git a/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py b/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py index e97af48..50501bd 100644 --- a/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py +++ b/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py @@ -5,8 +5,7 @@ import numpy as np from us_equity_strategies.income_layer import ( - INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET, + INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, INCOME_LAYER_RATIO_MODES, build_income_layer_plan, get_income_layer_ratio, @@ -21,8 +20,7 @@ LEGACY_CRISIS_RESPONSE_PROFILE = "crisis_response_shadow" LEGACY_TRUE_CRISIS_ROUTE = "true_crisis" __all__ = [ - "INCOME_LAYER_RATIO_MODE_LINEAR_CAP", - "INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET", + "INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET", "INCOME_LAYER_RATIO_MODES", "SOXX_GATE_TIERED_BLEND_MODE", "build_rebalance_plan", @@ -276,12 +274,12 @@ def build_rebalance_plan( income_layer_allocations=None, income_layer_enabled=True, income_layer_activation_band_ratio=0.0, - income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.06, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, trend_entry_buffer=0.03, trend_mid_buffer=0.06, trend_exit_buffer=0.03, @@ -354,11 +352,11 @@ def build_rebalance_plan( income_layer_max_ratio=income_layer_max_ratio, income_layer_activation_band_ratio=income_layer_activation_band_ratio, income_layer_ratio_mode=income_layer_ratio_mode, - income_layer_log_growth_factor=income_layer_log_growth_factor, - income_layer_stress_drawdown_ratio=income_layer_stress_drawdown_ratio, - income_layer_base_loss_budget_ratio=income_layer_base_loss_budget_ratio, - income_layer_min_loss_budget_ratio=income_layer_min_loss_budget_ratio, - income_layer_loss_budget_decay_per_double=income_layer_loss_budget_decay_per_double, + income_layer_core_stress_drawdown_ratio=income_layer_core_stress_drawdown_ratio, + income_layer_income_stress_drawdown_ratio=income_layer_income_stress_drawdown_ratio, + income_layer_base_drawdown_budget_ratio=income_layer_base_drawdown_budget_ratio, + income_layer_min_drawdown_budget_ratio=income_layer_min_drawdown_budget_ratio, + income_layer_drawdown_budget_decay_per_double=income_layer_drawdown_budget_decay_per_double, ) core_equity = max(0.0, total_strategy_equity - income_layer_plan.locked_value) deploy_ratio_text = "0.0%" diff --git a/src/us_equity_strategies/strategies/tqqq_growth_income.py b/src/us_equity_strategies/strategies/tqqq_growth_income.py index 1c09fdb..1c2ac76 100644 --- a/src/us_equity_strategies/strategies/tqqq_growth_income.py +++ b/src/us_equity_strategies/strategies/tqqq_growth_income.py @@ -9,8 +9,7 @@ from quant_platform_kit.common.history import normalize_history_frame from us_equity_strategies.income_layer import ( - INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET, + INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, INCOME_LAYER_RATIO_MODES, as_clamped_ratio, build_income_layer_plan, @@ -32,8 +31,7 @@ MACRO_RISK_GOVERNOR_PROFILE = "macro_risk_governor" MACRO_RISK_GOVERNOR_ROUTES = frozenset({"delever", "crisis"}) __all__ = [ - "INCOME_LAYER_RATIO_MODE_LINEAR_CAP", - "INCOME_LAYER_RATIO_MODE_LOG_LOSS_BUDGET", + "INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET", "INCOME_LAYER_RATIO_MODES", "build_rebalance_plan", "get_income_layer_ratio", @@ -352,12 +350,12 @@ def build_rebalance_plan( income_layer_allocations=None, income_layer_enabled=True, income_layer_activation_band_ratio=0.0, - income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LINEAR_CAP, - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_ratio_mode=INCOME_LAYER_RATIO_MODE_LOG_TOTAL_DRAWDOWN_BUDGET, + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, attack_allocation_mode="fixed_qqq_tqqq_pullback", dual_drive_qqq_weight=0.45, dual_drive_tqqq_weight=0.45, @@ -431,11 +429,11 @@ def build_rebalance_plan( income_layer_max_ratio=layer_max_ratio, income_layer_activation_band_ratio=income_layer_activation_band_ratio, income_layer_ratio_mode=income_layer_ratio_mode, - income_layer_log_growth_factor=income_layer_log_growth_factor, - income_layer_stress_drawdown_ratio=income_layer_stress_drawdown_ratio, - income_layer_base_loss_budget_ratio=income_layer_base_loss_budget_ratio, - income_layer_min_loss_budget_ratio=income_layer_min_loss_budget_ratio, - income_layer_loss_budget_decay_per_double=income_layer_loss_budget_decay_per_double, + income_layer_core_stress_drawdown_ratio=income_layer_core_stress_drawdown_ratio, + income_layer_income_stress_drawdown_ratio=income_layer_income_stress_drawdown_ratio, + income_layer_base_drawdown_budget_ratio=income_layer_base_drawdown_budget_ratio, + income_layer_min_drawdown_budget_ratio=income_layer_min_drawdown_budget_ratio, + income_layer_drawdown_budget_decay_per_double=income_layer_drawdown_budget_decay_per_double, ) target_income_values = income_layer_plan.target_values diff --git a/tests/test_account_sizing.py b/tests/test_account_sizing.py index 3d31066..bc27571 100644 --- a/tests/test_account_sizing.py +++ b/tests/test_account_sizing.py @@ -11,13 +11,14 @@ get_min_recommended_equity_usd, ) -from tests.test_qqq_tech_enhancement import _feature_snapshot +from tests.test_mega_cap_leader_rotation import _mega_snapshot def test_min_recommended_equity_is_profile_specific() -> None: assert get_min_recommended_equity_usd("tqqq_growth_income") == 500.0 assert get_min_recommended_equity_usd("soxl_soxx_trend_income") == 1_000.0 - assert get_min_recommended_equity_usd("qqq_tech_enhancement") == 10_000.0 + assert get_min_recommended_equity_usd("qqq_tech_enhancement") is None + assert get_min_recommended_equity_usd("tech_communication_pullback_enhancement") is None assert get_min_recommended_equity_usd("mega_cap_leader_rotation_top50_balanced") == 10_000.0 assert get_min_recommended_equity_usd("nasdaq_sp500_smart_dca") == 1_000.0 assert get_min_recommended_equity_usd("russell_1000_multi_factor_defensive") == 30_000.0 @@ -26,7 +27,7 @@ def test_min_recommended_equity_is_profile_specific() -> None: def test_account_size_diagnostics_warn_below_recommended_equity() -> None: diagnostics = build_account_size_diagnostics( - "tech_communication_pullback_enhancement", + "mega_cap_leader_rotation_top50_balanced", 1_000.0, ) @@ -71,7 +72,7 @@ def test_account_size_diagnostics_do_not_warn_at_recommended_equity() -> None: def test_entrypoint_appends_small_account_warning_to_signal_description() -> None: - entrypoint = get_strategy_entrypoint("tech_communication_pullback_enhancement") + entrypoint = get_strategy_entrypoint("mega_cap_leader_rotation_top50_balanced") portfolio = PortfolioSnapshot( as_of=pd.Timestamp("2026-04-01").to_pydatetime(), total_equity=1_000.0, @@ -84,7 +85,7 @@ def test_entrypoint_appends_small_account_warning_to_signal_description() -> Non decision = entrypoint.evaluate( StrategyContext( as_of="2026-04-01", - market_data={"feature_snapshot": _feature_snapshot()}, + market_data={"feature_snapshot": _mega_snapshot()}, portfolio=portfolio, state={"current_holdings": set()}, ) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index f960548..3f63e5d 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,6 +1,4 @@ -import json import unittest -from importlib import resources from quant_platform_kit.common.strategies import get_strategy_component_map from us_equity_strategies import get_strategy_definitions @@ -9,7 +7,6 @@ GLOBAL_ETF_ROTATION_PROFILE, MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE, NASDAQ_SP500_SMART_DCA_PROFILE, - QQQ_TECH_ENHANCEMENT_PROFILE, TQQQ_GROWTH_INCOME_PROFILE, RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE, SOXL_SOXX_TREND_INCOME_PROFILE, @@ -18,7 +15,6 @@ get_runtime_enabled_profiles, get_strategy_index_rows, get_strategy_definition, - get_strategy_metadata, get_strategy_metadata_map, get_strategy_platform_compatibility_map, resolve_canonical_profile, @@ -70,13 +66,6 @@ def test_catalog_contains_supported_profiles(self): FULL_SHARED_PLATFORM_MATRIX, ) - self.assertIn(QQQ_TECH_ENHANCEMENT_PROFILE, catalog) - self.assertEqual(catalog[QQQ_TECH_ENHANCEMENT_PROFILE].domain, "us_equity") - self.assertEqual( - get_compatible_platforms(QQQ_TECH_ENHANCEMENT_PROFILE), - FULL_SHARED_PLATFORM_MATRIX, - ) - self.assertIn(NASDAQ_SP500_SMART_DCA_PROFILE, catalog) self.assertEqual(catalog[NASDAQ_SP500_SMART_DCA_PROFILE].domain, "us_equity") self.assertEqual( @@ -134,14 +123,6 @@ def test_known_profile_resolves(self): "us_equity_strategies.strategies.russell_1000_multi_factor_defensive", ) - cash_buffer_definition = get_strategy_definition("qqq_tech_enhancement") - self.assertEqual(cash_buffer_definition.profile, QQQ_TECH_ENHANCEMENT_PROFILE) - cash_buffer_module = get_strategy_component_map(cash_buffer_definition)["signal_logic"] - self.assertEqual( - cash_buffer_module.module_path, - "us_equity_strategies.strategies.qqq_tech_enhancement", - ) - balanced_definition = get_strategy_definition("mega_cap_leader_rotation_top50_balanced") self.assertEqual(balanced_definition.profile, MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE) balanced_module = get_strategy_component_map(balanced_definition)["signal_logic"] @@ -162,13 +143,13 @@ def test_known_profile_resolves(self): def test_aliases_resolve_to_canonical_profiles(self): self.assertEqual(resolve_canonical_profile("global_macro_etf_rotation"), GLOBAL_ETF_ROTATION_PROFILE) self.assertEqual(resolve_canonical_profile("r1000_multifactor_defensive"), RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE) - self.assertEqual(get_strategy_definition("qqq_tech_enhancement").profile, QQQ_TECH_ENHANCEMENT_PROFILE) for legacy_profile in ( "hybrid_growth_income", "qqq_tqqq_growth_income", "semiconductor_rotation_income", "semiconductor_trend_income", "tech_pullback_cash_buffer", + "qqq_tech_enhancement", ): with self.subTest(profile=legacy_profile): with self.assertRaises(ValueError): @@ -176,39 +157,23 @@ def test_aliases_resolve_to_canonical_profiles(self): def test_metadata_map_exposes_display_names_and_roles(self): metadata_map = get_strategy_metadata_map() - self.assertEqual(metadata_map[QQQ_TECH_ENHANCEMENT_PROFILE].display_name, "Tech/Communication Pullback Enhancement") - self.assertEqual(metadata_map[QQQ_TECH_ENHANCEMENT_PROFILE].role, "parallel_cash_buffer_branch") self.assertEqual(metadata_map[GLOBAL_ETF_ROTATION_PROFILE].benchmark, "VOO") - self.assertEqual(get_strategy_metadata("qqq_tech_enhancement").canonical_profile, QQQ_TECH_ENHANCEMENT_PROFILE) aliases = get_profile_aliases() - self.assertEqual(aliases["qqq_tech_enhancement"], QQQ_TECH_ENHANCEMENT_PROFILE) self.assertEqual( aliases["global_etf_confidence_vol_gate"], GLOBAL_ETF_ROTATION_PROFILE, ) self.assertNotIn("tech_pullback_cash_buffer", aliases) + self.assertNotIn("qqq_tech_enhancement", aliases) compatibility = get_strategy_platform_compatibility_map() - self.assertEqual( - compatibility[QQQ_TECH_ENHANCEMENT_PROFILE], - FULL_SHARED_PLATFORM_MATRIX, - ) self.assertEqual( compatibility[TQQQ_GROWTH_INCOME_PROFILE], FULL_SHARED_PLATFORM_MATRIX, ) - self.assertEqual(metadata_map[QQQ_TECH_ENHANCEMENT_PROFILE].status, "research_only") - with ( - resources.files("us_equity_strategies") - .joinpath("configs", "tech_communication_pullback_enhancement.json") - .open(encoding="utf-8") - ) as handle: - tech_config = json.load(handle) - self.assertEqual(tech_config["status"], "research_only") self.assertEqual( metadata_map[GLOBAL_ETF_ROTATION_PROFILE].status, "runtime_enabled", ) - self.assertEqual(get_strategy_definition("qqq_tech_enhancement").target_mode, "weight") self.assertEqual( metadata_map[MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE].role, "balanced_leader_rotation", @@ -236,13 +201,11 @@ def test_option_overlay_defaults_are_scoped_to_supported_profiles(self): self.assertEqual(soxl["option_income_overlay_start_usd"], 1000000.0) self.assertEqual(soxl["option_income_overlay_nav_risk_ratio"], 0.01) - tech = get_strategy_definition(QQQ_TECH_ENHANCEMENT_PROFILE).default_config mega = get_strategy_definition(MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE).default_config - for config in (tech, mega): - self.assertIs(config["option_growth_overlay_enabled"], True) - self.assertEqual(config["option_growth_overlay_recipe"], "qqq_leaps_growth_v1") - self.assertEqual(config["option_growth_overlay_start_usd"], 1000000.0) - self.assertEqual(config["option_growth_overlay_nav_budget_ratio"], 0.03) + self.assertIs(mega["option_growth_overlay_enabled"], True) + self.assertEqual(mega["option_growth_overlay_recipe"], "qqq_leaps_growth_v1") + self.assertEqual(mega["option_growth_overlay_start_usd"], 1000000.0) + self.assertEqual(mega["option_growth_overlay_nav_budget_ratio"], 0.03) self.assertNotIn( "option_growth_overlay_enabled", @@ -266,7 +229,6 @@ def test_market_regime_control_position_defaults_match_strategy_consumption_poli weight_scaled_profiles = ( GLOBAL_ETF_ROTATION_PROFILE, RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE, - QQQ_TECH_ENHANCEMENT_PROFILE, MEGA_CAP_LEADER_ROTATION_TOP50_BALANCED_PROFILE, ) for profile in weight_scaled_profiles: @@ -285,14 +247,10 @@ def test_market_regime_control_position_defaults_match_strategy_consumption_poli def test_strategy_index_rows_are_human_readable(self): rows = get_strategy_index_rows() by_profile = {row["canonical_profile"]: row for row in rows} - self.assertEqual(by_profile[QQQ_TECH_ENHANCEMENT_PROFILE]["display_name"], "Tech/Communication Pullback Enhancement") self.assertEqual(by_profile[TQQQ_GROWTH_INCOME_PROFILE]["aliases"], ()) self.assertIn("signal_logic", by_profile[GLOBAL_ETF_ROTATION_PROFILE]["component_names"]) self.assertNotIn("global_etf_confidence_vol_gate", by_profile) - self.assertEqual( - by_profile[QQQ_TECH_ENHANCEMENT_PROFILE]["compatible_platforms"], - FULL_SHARED_PLATFORM_MATRIX, - ) + self.assertNotIn("tech_communication_pullback_enhancement", by_profile) def test_removed_research_profiles_are_not_cataloged(self): catalog = get_strategy_definitions() @@ -300,6 +258,8 @@ def test_removed_research_profiles_are_not_cataloged(self): "mega_cap_leader_rotation_dynamic_top20", "mega_cap_leader_rotation_aggressive", "dynamic_mega_leveraged_pullback", + "tech_communication_pullback_enhancement", + "qqq_tech_enhancement", ): with self.subTest(profile=profile): self.assertNotIn(profile, catalog) diff --git a/tests/test_contract_governance.py b/tests/test_contract_governance.py index 1acb9cb..d2de959 100644 --- a/tests/test_contract_governance.py +++ b/tests/test_contract_governance.py @@ -46,9 +46,6 @@ "tqqq_growth_income", "soxl_soxx_trend_income", } -LIVE_PROFILE_TRANSITION_ALIASES = { - "tech_communication_pullback_enhancement": ("qqq_tech_enhancement",), -} LIVE_US_EQUITY_FULL_MATRIX_PROFILES = get_runtime_enabled_profiles() ALLOWED_TARGET_MODES = frozenset({"weight", "value"}) PLATFORM_NATIVE_TARGET_MODES = { @@ -201,9 +198,6 @@ def test_live_profiles_do_not_keep_unplanned_legacy_aliases(self) -> None: for profile in LIVE_PROFILE_LEGACY_ALIASES: with self.subTest(profile=profile): self.assertEqual(STRATEGY_CATALOG.metadata[profile].aliases, ()) - for profile, aliases in LIVE_PROFILE_TRANSITION_ALIASES.items(): - with self.subTest(profile=profile): - self.assertEqual(STRATEGY_CATALOG.metadata[profile].aliases, aliases) def test_live_us_equity_profiles_now_cover_the_full_four_platform_matrix(self) -> None: for profile in LIVE_US_EQUITY_FULL_MATRIX_PROFILES: diff --git a/tests/test_entrypoints.py b/tests/test_entrypoints.py index 489d796..8d63813 100644 --- a/tests/test_entrypoints.py +++ b/tests/test_entrypoints.py @@ -14,11 +14,9 @@ from us_equity_strategies.strategies.tqqq_growth_income import build_rebalance_plan as tqqq_growth_build_rebalance_plan from us_equity_strategies.strategies.soxl_soxx_trend_income import build_rebalance_plan as soxl_soxx_trend_build_rebalance_plan from us_equity_strategies.strategies.russell_1000_multi_factor_defensive import extract_managed_symbols as legacy_russell_managed_symbols -from us_equity_strategies.strategies.qqq_tech_enhancement import extract_managed_symbols as qqq_tech_managed_symbols from us_equity_strategies.strategies.mega_cap_leader_rotation import extract_managed_symbols as mega_cap_managed_symbols from tests.test_russell_1000_multi_factor_defensive import _normal_snapshot -from tests.test_qqq_tech_enhancement import _feature_snapshot from tests.test_mega_cap_leader_rotation import _mega_snapshot @@ -348,14 +346,14 @@ def test_tqqq_growth_income_defaults_to_fixed_dual_drive_live_profile(self) -> N self.assertEqual(config["income_threshold_usd"], 250000.0) self.assertIs(config["income_layer_enabled"], True) self.assertEqual(config["income_layer_start_usd"], 250000.0) - self.assertEqual(config["income_layer_max_ratio"], 0.50) + self.assertEqual(config["income_layer_max_ratio"], 0.55) self.assertEqual(config["income_layer_activation_band_ratio"], 0.20) - self.assertEqual(config["income_layer_ratio_mode"], "log_cap") - self.assertEqual(config["income_layer_log_growth_factor"], 0.70) - self.assertEqual(config["income_layer_stress_drawdown_ratio"], 0.30) - self.assertEqual(config["income_layer_base_loss_budget_ratio"], 0.08) - self.assertEqual(config["income_layer_min_loss_budget_ratio"], 0.06) - self.assertEqual(config["income_layer_loss_budget_decay_per_double"], 0.01) + self.assertEqual(config["income_layer_ratio_mode"], "log_total_drawdown_budget") + self.assertEqual(config["income_layer_core_stress_drawdown_ratio"], 0.45) + self.assertEqual(config["income_layer_income_stress_drawdown_ratio"], 0.08) + self.assertEqual(config["income_layer_base_drawdown_budget_ratio"], 0.45) + self.assertEqual(config["income_layer_min_drawdown_budget_ratio"], 0.25) + self.assertEqual(config["income_layer_drawdown_budget_decay_per_double"], 0.05) self.assertEqual( config["income_layer_allocations"], {"SCHD": 0.30, "DGRO": 0.20, "SGOV": 0.40, "SPYI": 0.08, "QQQI": 0.02}, @@ -372,19 +370,33 @@ def test_tqqq_growth_income_defaults_to_fixed_dual_drive_live_profile(self) -> N def test_weight_mode_profiles_default_to_income_layer_config(self) -> None: expected = { - "global_etf_rotation": (500000.0, 0.15, 0.10), - "russell_1000_multi_factor_defensive": (400000.0, 0.20, 0.10), - "qqq_tech_enhancement": (250000.0, 0.30, 0.15), - "mega_cap_leader_rotation_top50_balanced": (300000.0, 0.25, 0.15), + "global_etf_rotation": ( + 500000.0, + 0.15, + 0.10, + {"SCHD": 0.40, "DGRO": 0.25, "SGOV": 0.30, "SPYI": 0.05}, + ), + "russell_1000_multi_factor_defensive": ( + 400000.0, + 0.20, + 0.10, + {"SCHD": 0.45, "DGRO": 0.30, "SGOV": 0.25}, + ), + "mega_cap_leader_rotation_top50_balanced": ( + 300000.0, + 0.25, + 0.15, + {"SCHD": 0.45, "DGRO": 0.30, "SGOV": 0.25}, + ), } - for profile, (start_usd, max_ratio, activation_band_ratio) in expected.items(): + for profile, (start_usd, max_ratio, activation_band_ratio, allocations) in expected.items(): config = get_strategy_entrypoint(profile).manifest.default_config self.assertIs(config["income_layer_enabled"], True) self.assertEqual(config["income_layer_start_usd"], start_usd) self.assertEqual(config["income_layer_max_ratio"], max_ratio) self.assertEqual(config["income_layer_activation_band_ratio"], activation_band_ratio) - self.assertEqual(config["income_layer_ratio_mode"], "log_loss_budget") - self.assertTrue(config["income_layer_allocations"]) + self.assertEqual(config["income_layer_ratio_mode"], "log_total_drawdown_budget") + self.assertEqual(config["income_layer_allocations"], allocations) def test_value_mode_hybrid_runtime_adapters_use_canonical_inputs(self) -> None: for platform_id in ("ibkr", "schwab", "longbridge", "firstrade", "paper_signal"): @@ -505,22 +517,6 @@ def test_tqqq_growth_income_entrypoint_accepts_qqqm_unlevered_sleeve(self) -> No self.assertIn("QQQ: ", decision.diagnostics["dashboard"]) def test_runtime_requirements_classify_snapshot_and_non_snapshot_profiles(self) -> None: - tech = describe_platform_runtime_requirements("qqq_tech_enhancement", platform_id="schwab") - self.assertEqual(tech["profile_group"], "snapshot_backed") - self.assertEqual(tech["input_mode"], "feature_snapshot") - self.assertTrue(tech["requires_snapshot_artifacts"]) - self.assertTrue(tech["requires_snapshot_manifest_path"]) - self.assertTrue(tech["requires_strategy_config_path"]) - self.assertEqual(tech["config_source_policy"], "bundled_or_env") - self.assertEqual(tech["reconciliation_output_policy"], "optional") - self.assertIsNone(tech["runtime_execution_window_trading_days"]) - - longbridge_tech = describe_platform_runtime_requirements( - "qqq_tech_enhancement", - platform_id="longbridge", - ) - self.assertEqual(longbridge_tech["runtime_execution_window_trading_days"], 1) - mega = describe_platform_runtime_requirements("mega_cap_leader_rotation_top50_balanced", platform_id="ibkr") self.assertEqual(mega["profile_group"], "snapshot_backed") self.assertEqual(mega["input_mode"], "feature_snapshot") @@ -540,6 +536,14 @@ def test_runtime_requirements_classify_snapshot_and_non_snapshot_profiles(self) self.assertFalse(tqqq["requires_strategy_config_path"]) self.assertEqual(tqqq["signal_effective_after_trading_days"], 1) + for removed_profile in ( + "tech_communication_pullback_enhancement", + "qqq_tech_enhancement", + ): + with self.subTest(profile=removed_profile): + with self.assertRaises(ValueError): + describe_platform_runtime_requirements(removed_profile, platform_id="schwab") + def test_soxl_soxx_trend_income_entrypoint_maps_target_values_without_execution_fields(self) -> None: entrypoint = get_strategy_entrypoint("soxl_soxx_trend_income") indicators = { @@ -660,15 +664,21 @@ def test_soxl_soxx_trend_income_entrypoint_maps_target_values_without_execution_ ("SOXL", "SOXX", "BOXX", "SCHD", "DGRO", "SGOV", "SPYI", "QQQI"), ) self.assertIs(entrypoint.manifest.default_config["income_layer_enabled"], True) - self.assertEqual(entrypoint.manifest.default_config["income_layer_ratio_mode"], "log_cap") + self.assertEqual(entrypoint.manifest.default_config["income_layer_ratio_mode"], "log_total_drawdown_budget") + self.assertEqual(entrypoint.manifest.default_config["income_layer_start_usd"], 150000.0) self.assertEqual(entrypoint.manifest.default_config["income_layer_max_ratio"], 0.95) self.assertEqual(entrypoint.manifest.default_config["income_layer_activation_band_ratio"], 0.20) + self.assertEqual(entrypoint.manifest.default_config["income_layer_core_stress_drawdown_ratio"], 0.45) + self.assertEqual(entrypoint.manifest.default_config["income_layer_income_stress_drawdown_ratio"], 0.06) + self.assertEqual(entrypoint.manifest.default_config["income_layer_base_drawdown_budget_ratio"], 0.45) + self.assertEqual(entrypoint.manifest.default_config["income_layer_min_drawdown_budget_ratio"], 0.25) + self.assertEqual(entrypoint.manifest.default_config["income_layer_drawdown_budget_decay_per_double"], 0.05) self.assertNotIn("market_regime_control_enabled", entrypoint.manifest.default_config) self.assertNotIn("market_regime_control_apply_risk_reduced", entrypoint.manifest.default_config) self.assertNotIn("market_regime_control_apply_risk_off", entrypoint.manifest.default_config) self.assertEqual( entrypoint.manifest.default_config["income_layer_allocations"], - {"SCHD": 0.25, "DGRO": 0.15, "SGOV": 0.55, "SPYI": 0.04, "QQQI": 0.01}, + {"SCHD": 0.15, "DGRO": 0.10, "SGOV": 0.70, "SPYI": 0.04, "QQQI": 0.01}, ) def test_soxl_soxx_trend_income_entrypoint_rejects_retired_fixed_dual_drive_runtime_config(self) -> None: @@ -719,7 +729,7 @@ def test_value_mode_semiconductor_runtime_adapters_use_canonical_inputs(self) -> ) self.assertEqual(adapter.portfolio_input_name, "portfolio_snapshot") - def test_russell_and_tech_entrypoints_match_legacy_weight_outputs(self) -> None: + def test_snapshot_entrypoints_match_legacy_weight_outputs(self) -> None: russell = get_strategy_entrypoint("russell_1000_multi_factor_defensive") russell_decision = russell.evaluate( StrategyContext( @@ -737,26 +747,6 @@ def test_russell_and_tech_entrypoints_match_legacy_weight_outputs(self) -> None: self.assertIn("BBB", {position.symbol for position in russell_decision.positions}) self.assertEqual(russell_decision.diagnostics["signal_source"], "feature_snapshot") - tech = get_strategy_entrypoint("qqq_tech_enhancement") - tech_decision = tech.evaluate( - StrategyContext( - as_of="2026-04-01", - market_data={"feature_snapshot": _feature_snapshot()}, - portfolio=PortfolioSnapshot( - as_of="2026-04-01", - total_equity=10_000.0, - buying_power=10_000.0, - cash_balance=10_000.0, - positions=(), - ), - state={"current_holdings": {"AAPL"}}, - ) - ) - self.assertIn("BOXX", {position.symbol for position in tech_decision.positions}) - self.assertNotIn("portfolio_rows", tech_decision.diagnostics) - self.assertEqual(tech_decision.diagnostics["signal_source"], "feature_snapshot") - self.assertEqual(tech_decision.diagnostics["effective_holdings_count"], 2) - mega = get_strategy_entrypoint("mega_cap_leader_rotation_top50_balanced") mega_decision = mega.evaluate( StrategyContext( @@ -777,11 +767,11 @@ def test_russell_and_tech_entrypoints_match_legacy_weight_outputs(self) -> None: self.assertNotIn("SPY", {position.symbol for position in mega_decision.positions}) def test_weight_mode_income_layer_scales_core_weights_when_portfolio_is_large(self) -> None: - tech = get_strategy_entrypoint("qqq_tech_enhancement") - decision = tech.evaluate( + mega = get_strategy_entrypoint("mega_cap_leader_rotation_top50_balanced") + decision = mega.evaluate( StrategyContext( as_of="2026-04-01", - market_data={"feature_snapshot": _feature_snapshot()}, + market_data={"feature_snapshot": _mega_snapshot()}, portfolio=PortfolioSnapshot( as_of="2026-04-01", total_equity=1_000_000.0, @@ -789,7 +779,6 @@ def test_weight_mode_income_layer_scales_core_weights_when_portfolio_is_large(se cash_balance=1_000_000.0, positions=(), ), - state={"current_holdings": {"AAPL"}}, ) ) @@ -805,6 +794,17 @@ def test_weight_mode_income_layer_scales_core_weights_when_portfolio_is_large(se 1.0, ) + def test_removed_tech_profile_has_no_runtime_entrypoint(self) -> None: + for removed_profile in ( + "tech_communication_pullback_enhancement", + "qqq_tech_enhancement", + ): + with self.subTest(profile=removed_profile): + with self.assertRaises(ValueError): + get_strategy_entrypoint(removed_profile) + with self.assertRaises(ValueError): + get_platform_runtime_adapter(removed_profile, platform_id="ibkr") + def test_ibkr_runtime_adapters_expose_unified_snapshot_runtime_metadata(self) -> None: global_adapter = get_platform_runtime_adapter("global_macro_etf_rotation", platform_id="ibkr") self.assertEqual(global_adapter.status_icon, "🐤") @@ -852,62 +852,6 @@ def test_ibkr_runtime_adapters_expose_unified_snapshot_runtime_metadata(self) -> self.assertIsNone(paper_russell_adapter.portfolio_input_name) self.assertEqual(paper_russell_adapter.status_icon, "📏") - tech_adapter = get_platform_runtime_adapter("qqq_tech_enhancement", platform_id="ibkr") - self.assertEqual(tech_adapter.status_icon, "🧲") - self.assertEqual(tech_adapter.snapshot_date_columns, ("as_of", "snapshot_date")) - self.assertTrue(tech_adapter.require_snapshot_manifest) - self.assertIsNotNone(tech_adapter.artifact_contract) - self.assertTrue(tech_adapter.artifact_contract.requires_strategy_config_path) - self.assertEqual(tech_adapter.artifact_contract.config_source_policy, "bundled_or_env") - self.assertEqual(tech_adapter.runtime_policy.reconciliation_output_policy, "optional") - self.assertEqual( - tech_adapter.managed_symbols_extractor( - _feature_snapshot(), - benchmark_symbol="QQQ", - safe_haven="BOXX", - ), - qqq_tech_managed_symbols( - _feature_snapshot(), - benchmark_symbol="QQQ", - safe_haven="BOXX", - ), - ) - self.assertEqual( - tech_adapter.runtime_parameter_loader( - config_path=None, - logger=lambda _message: None, - )["runtime_config_name"], - "tech_communication_pullback_enhancement", - ) - longbridge_tech_adapter = get_platform_runtime_adapter("qqq_tech_enhancement", platform_id="longbridge") - self.assertEqual( - longbridge_tech_adapter.available_inputs, - frozenset({"feature_snapshot", "portfolio_snapshot"}), - ) - self.assertEqual(longbridge_tech_adapter.portfolio_input_name, "portfolio_snapshot") - self.assertEqual(longbridge_tech_adapter.runtime_policy.runtime_execution_window_trading_days, 1) - schwab_tech_adapter = get_platform_runtime_adapter("qqq_tech_enhancement", platform_id="schwab") - self.assertEqual( - schwab_tech_adapter.available_inputs, - frozenset({"feature_snapshot", "portfolio_snapshot"}), - ) - self.assertEqual(schwab_tech_adapter.portfolio_input_name, "portfolio_snapshot") - firstrade_tech_adapter = get_platform_runtime_adapter( - "qqq_tech_enhancement", - platform_id="firstrade", - ) - self.assertEqual( - firstrade_tech_adapter.available_inputs, - frozenset({"feature_snapshot", "portfolio_snapshot"}), - ) - self.assertEqual(firstrade_tech_adapter.portfolio_input_name, "portfolio_snapshot") - paper_tech_adapter = get_platform_runtime_adapter("qqq_tech_enhancement", platform_id="paper_signal") - self.assertEqual( - paper_tech_adapter.available_inputs, - frozenset({"feature_snapshot"}), - ) - self.assertIsNone(paper_tech_adapter.portfolio_input_name) - mega_adapter = get_platform_runtime_adapter("mega_cap_leader_rotation_top50_balanced", platform_id="ibkr") self.assertEqual(mega_adapter.status_icon, "👑") self.assertEqual(mega_adapter.snapshot_date_columns, ("as_of", "snapshot_date")) diff --git a/tests/test_income_layer.py b/tests/test_income_layer.py index 9d4c665..9ac9e95 100644 --- a/tests/test_income_layer.py +++ b/tests/test_income_layer.py @@ -2,6 +2,7 @@ import unittest +from us_equity_strategies.income_layer_defaults import INCOME_LAYER_DEFAULT_CONFIGS from us_equity_strategies.income_layer import ( build_income_layer_plan, get_income_layer_ratio, @@ -45,54 +46,74 @@ def test_disabled_layer_locks_existing_income_value_without_new_target(self) -> self.assertEqual(plan.target_values, {"SCHD": 12000.0, "DGRO": 8000.0}) self.assertIs(plan.diagnostics["income_layer_enabled"], False) - def test_log_loss_budget_caps_income_layer_ratio(self) -> None: + def test_total_drawdown_budget_reverses_into_income_layer_ratio(self) -> None: ratio = get_income_layer_ratio( 600000.0, income_layer_start_usd=150000.0, - income_layer_max_ratio=0.50, - income_layer_ratio_mode="log_loss_budget", - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_max_ratio=0.65, + income_layer_ratio_mode="log_total_drawdown_budget", + income_layer_core_stress_drawdown_ratio=0.40, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.30, + income_layer_min_drawdown_budget_ratio=0.15, + income_layer_drawdown_budget_decay_per_double=0.05, ) - self.assertAlmostEqual(ratio, 0.20) + self.assertAlmostEqual(ratio, 0.625) - def test_log_cap_grows_without_loss_budget_cap(self) -> None: + def test_total_drawdown_budget_reports_stress_diagnostics(self) -> None: ratio, diagnostics = resolve_income_layer_ratio( 600000.0, income_layer_start_usd=150000.0, - income_layer_max_ratio=0.50, - income_layer_ratio_mode="log_cap", - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_max_ratio=0.65, + income_layer_ratio_mode="log_total_drawdown_budget", + income_layer_core_stress_drawdown_ratio=0.40, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.30, + income_layer_min_drawdown_budget_ratio=0.15, + income_layer_drawdown_budget_decay_per_double=0.05, ) - self.assertGreater(ratio, 0.35) - self.assertLess(ratio, 0.50) - self.assertEqual(diagnostics["income_layer_ratio_mode"], "log_cap") - self.assertEqual(diagnostics["income_layer_loss_budget_cap_ratio"], 0.50) + self.assertAlmostEqual(ratio, 0.625) + self.assertEqual(diagnostics["income_layer_ratio_mode"], "log_total_drawdown_budget") + self.assertAlmostEqual(diagnostics["income_layer_account_drawdown_budget_ratio"], 0.20) + self.assertAlmostEqual(diagnostics["income_layer_account_stress_drawdown_ratio"], 0.20) + self.assertTrue(diagnostics["income_layer_drawdown_budget_met"]) + + def test_retired_income_layer_modes_are_rejected(self) -> None: + for mode in ("linear_cap", "log_cap", "log_loss_budget"): + with self.subTest(mode=mode): + with self.assertRaisesRegex(ValueError, "Unsupported income layer ratio mode"): + get_income_layer_ratio( + 600000.0, + income_layer_start_usd=150000.0, + income_layer_max_ratio=0.65, + income_layer_ratio_mode=mode, + ) def test_activation_band_smooths_ratio_after_start_threshold(self) -> None: normal_ratio = get_income_layer_ratio( 165000.0, income_layer_start_usd=150000.0, - income_layer_max_ratio=0.50, - income_layer_ratio_mode="log_cap", - income_layer_log_growth_factor=0.70, + income_layer_max_ratio=0.65, + income_layer_ratio_mode="log_total_drawdown_budget", + income_layer_core_stress_drawdown_ratio=0.40, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.30, + income_layer_min_drawdown_budget_ratio=0.15, + income_layer_drawdown_budget_decay_per_double=0.05, ) softened_ratio, diagnostics = resolve_income_layer_ratio( 165000.0, income_layer_start_usd=150000.0, - income_layer_max_ratio=0.50, + income_layer_max_ratio=0.65, income_layer_activation_band_ratio=0.20, - income_layer_ratio_mode="log_cap", - income_layer_log_growth_factor=0.70, + income_layer_ratio_mode="log_total_drawdown_budget", + income_layer_core_stress_drawdown_ratio=0.40, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.30, + income_layer_min_drawdown_budget_ratio=0.15, + income_layer_drawdown_budget_decay_per_double=0.05, ) self.assertAlmostEqual(softened_ratio, normal_ratio * 0.5) @@ -100,6 +121,43 @@ def test_activation_band_smooths_ratio_after_start_threshold(self) -> None: self.assertAlmostEqual(diagnostics["income_layer_activation_multiplier"], 0.5) self.assertEqual(diagnostics["income_layer_activation_end_usd"], 180000.0) + def test_default_budget_curves_are_monotonic_and_start_smoothly(self) -> None: + param_keys = ( + "income_layer_start_usd", + "income_layer_max_ratio", + "income_layer_activation_band_ratio", + "income_layer_ratio_mode", + "income_layer_core_stress_drawdown_ratio", + "income_layer_income_stress_drawdown_ratio", + "income_layer_base_drawdown_budget_ratio", + "income_layer_min_drawdown_budget_ratio", + "income_layer_drawdown_budget_decay_per_double", + ) + for profile, config in INCOME_LAYER_DEFAULT_CONFIGS.items(): + with self.subTest(profile=profile): + params = {key: config[key] for key in param_keys} + start = float(params["income_layer_start_usd"]) + band = float(params["income_layer_activation_band_ratio"]) + navs = ( + start, + start * 1.001, + start * (1.0 + band / 2.0), + start * (1.0 + band), + start * 2.0, + start * 4.0, + start * 8.0, + start * 16.0, + ) + ratios = [ + resolve_income_layer_ratio(nav, income_layer_enabled=True, **params)[0] + for nav in navs + ] + + self.assertEqual(ratios[0], 0.0) + self.assertLess(ratios[1], 0.0001) + self.assertLessEqual(max(ratios), float(params["income_layer_max_ratio"])) + self.assertEqual(ratios, sorted(ratios)) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_platform_registry_support.py b/tests/test_platform_registry_support.py index 3014d67..0d3b372 100644 --- a/tests/test_platform_registry_support.py +++ b/tests/test_platform_registry_support.py @@ -9,7 +9,7 @@ class PlatformRegistrySupportTest(unittest.TestCase): def test_get_enabled_profiles_for_platform_filters_by_platform(self): - enabled = frozenset({"tech_communication_pullback_enhancement"}) + enabled = frozenset({"mega_cap_leader_rotation_top50_balanced"}) self.assertEqual( get_enabled_profiles_for_platform( "ibkr", @@ -30,40 +30,40 @@ def test_get_enabled_profiles_for_platform_filters_by_platform(self): def test_build_platform_profile_matrix_uses_metadata(self): rows = build_platform_profile_matrix( platform_id="ibkr", - enabled_profiles=frozenset({"tech_communication_pullback_enhancement"}), + enabled_profiles=frozenset({"mega_cap_leader_rotation_top50_balanced"}), default_profile="global_etf_rotation", rollback_profile="global_etf_rotation", ) self.assertEqual(len(rows), 1) - self.assertEqual(rows[0]["canonical_profile"], "tech_communication_pullback_enhancement") - self.assertEqual(rows[0]["display_name"], "Tech/Communication Pullback Enhancement") - self.assertEqual(rows[0]["aliases"], ("qqq_tech_enhancement",)) + self.assertEqual(rows[0]["canonical_profile"], "mega_cap_leader_rotation_top50_balanced") + self.assertEqual(rows[0]["display_name"], "Mega Cap Leader Rotation Top50 Balanced") + self.assertEqual(rows[0]["aliases"], ()) self.assertFalse(rows[0]["is_default"]) self.assertFalse(rows[0]["is_rollback"]) def test_resolve_platform_strategy_definition_supports_canonical_profile(self): definition = resolve_platform_strategy_definition( - "tech_communication_pullback_enhancement", + "mega_cap_leader_rotation_top50_balanced", platform_id="ibkr", expected_platform_id="ibkr", - enabled_profiles=frozenset({"tech_communication_pullback_enhancement"}), + enabled_profiles=frozenset({"mega_cap_leader_rotation_top50_balanced"}), platform_supported_domains={"ibkr": frozenset({"us_equity"})}, require_explicit=True, ) - self.assertEqual(definition.profile, "tech_communication_pullback_enhancement") + self.assertEqual(definition.profile, "mega_cap_leader_rotation_top50_balanced") class PlatformRegistryAliasSupportTest(unittest.TestCase): def test_resolve_platform_strategy_definition_supports_legacy_alias(self): definition = resolve_platform_strategy_definition( - "qqq_tech_enhancement", + "r1000_multifactor_defensive", platform_id="ibkr", expected_platform_id="ibkr", - enabled_profiles=frozenset({"tech_communication_pullback_enhancement"}), + enabled_profiles=frozenset({"russell_1000_multi_factor_defensive"}), platform_supported_domains={"ibkr": frozenset({"us_equity"})}, require_explicit=True, ) - self.assertEqual(definition.profile, "tech_communication_pullback_enhancement") + self.assertEqual(definition.profile, "russell_1000_multi_factor_defensive") if __name__ == "__main__": diff --git a/tests/test_portfolio_dashboard.py b/tests/test_portfolio_dashboard.py index f20a28c..7104bb9 100644 --- a/tests/test_portfolio_dashboard.py +++ b/tests/test_portfolio_dashboard.py @@ -9,7 +9,8 @@ from us_equity_strategies import get_strategy_entrypoint from us_equity_strategies.entrypoints._portfolio_dashboard import build_portfolio_dashboard -from tests.test_qqq_tech_enhancement import _feature_snapshot +from tests.test_mega_cap_leader_rotation import _mega_snapshot +from tests.test_russell_1000_multi_factor_defensive import _normal_snapshot def _zh_translator(key: str, **_kwargs) -> str: @@ -64,7 +65,7 @@ def test_semiconductor_entrypoint_attaches_strategy_cash_and_holdings_dashboard( self.assertNotIn("跟踪股票池", dashboard) def test_snapshot_entrypoint_attaches_strategy_portfolio_dashboard(self) -> None: - entrypoint = get_strategy_entrypoint("qqq_tech_enhancement") + entrypoint = get_strategy_entrypoint("russell_1000_multi_factor_defensive") snapshot = PortfolioSnapshot( as_of=pd.Timestamp("2026-04-21").to_pydatetime(), total_equity=12500.0, @@ -83,7 +84,7 @@ def test_snapshot_entrypoint_attaches_strategy_portfolio_dashboard(self) -> None decision = entrypoint.evaluate( StrategyContext( as_of="2026-04-21", - market_data={"feature_snapshot": _feature_snapshot()}, + market_data={"feature_snapshot": _normal_snapshot()}, portfolio=snapshot, state={"current_holdings": {"AAPL"}}, runtime_config={"translator": _zh_translator}, @@ -140,7 +141,7 @@ def test_russell_entrypoint_accepts_runtime_helpers_and_attaches_dashboard(self) self.assertIn("BOXX: $2,000.00 / 20股", dashboard) def test_snapshot_entrypoint_renders_structured_monthly_waiting_text_in_zh(self) -> None: - entrypoint = get_strategy_entrypoint("qqq_tech_enhancement") + entrypoint = get_strategy_entrypoint("mega_cap_leader_rotation_top50_balanced") snapshot = PortfolioSnapshot( as_of=pd.Timestamp("2026-04-21").to_pydatetime(), total_equity=12500.0, @@ -156,7 +157,7 @@ def test_snapshot_entrypoint_renders_structured_monthly_waiting_text_in_zh(self) decision = entrypoint.evaluate( StrategyContext( as_of="2026-04-10", - market_data={"feature_snapshot": _feature_snapshot()}, + market_data={"feature_snapshot": _mega_snapshot()}, portfolio=snapshot, state={"current_holdings": set()}, runtime_config={"translator": _zh_translator, "run_as_of": "2026-04-10"}, diff --git a/tests/test_strategy_plans.py b/tests/test_strategy_plans.py index e389fd9..a10d049 100644 --- a/tests/test_strategy_plans.py +++ b/tests/test_strategy_plans.py @@ -243,18 +243,18 @@ def test_tqqq_growth_income_supports_diversified_income_layer(self): self.assertEqual(plan["strategy_symbols"], ["TQQQ", "QQQ", "BOXX", *income_symbols]) self.assertEqual(plan["buy_order_symbols"], (*income_symbols, "TQQQ", "QQQ")) self.assertEqual(plan["portfolio_rows"], (("TQQQ", "QQQ", "BOXX"), income_symbols)) - self.assertAlmostEqual(plan["income_layer_ratio"], 0.10) - self.assertAlmostEqual(plan["income_layer_value"], 22500.0) - self.assertAlmostEqual(plan["target_values"]["TQQQ"], 91125.0) - self.assertAlmostEqual(plan["target_values"]["QQQ"], 91125.0) - self.assertAlmostEqual(plan["target_values"]["BOXX"], 16200.0) - self.assertAlmostEqual(plan["target_values"]["SCHD"], 9000.0) - self.assertAlmostEqual(plan["target_values"]["DGRO"], 4500.0) - self.assertAlmostEqual(plan["target_values"]["SGOV"], 2250.0) - self.assertAlmostEqual(plan["target_values"]["SPYI"], 4500.0) - self.assertAlmostEqual(plan["target_values"]["QQQI"], 2250.0) - - def test_tqqq_growth_income_log_loss_budget_caps_income_layer(self): + self.assertAlmostEqual(plan["income_layer_ratio"], 0.0790489865839401) + self.assertAlmostEqual(plan["income_layer_value"], 17786.021981386522) + self.assertAlmostEqual(plan["target_values"]["TQQQ"], 93246.29010837656) + self.assertAlmostEqual(plan["target_values"]["QQQ"], 93246.29010837656) + self.assertAlmostEqual(plan["target_values"]["BOXX"], 16577.118241489167) + self.assertAlmostEqual(plan["target_values"]["SCHD"], 7114.408792554609) + self.assertAlmostEqual(plan["target_values"]["DGRO"], 3557.2043962773044) + self.assertAlmostEqual(plan["target_values"]["SGOV"], 1778.6021981386523) + self.assertAlmostEqual(plan["target_values"]["SPYI"], 3557.2043962773044) + self.assertAlmostEqual(plan["target_values"]["QQQI"], 1778.6021981386523) + + def test_tqqq_growth_income_total_drawdown_budget_sets_income_layer(self): _skip_if_missing_numeric_stack() from us_equity_strategies.strategies.tqqq_growth_income import ( get_income_layer_ratio, @@ -263,16 +263,16 @@ def test_tqqq_growth_income_log_loss_budget_caps_income_layer(self): ratio = get_income_layer_ratio( 600000.0, income_layer_start_usd=150000.0, - income_layer_max_ratio=0.50, - income_layer_ratio_mode="log_loss_budget", - income_layer_log_growth_factor=0.70, - income_layer_stress_drawdown_ratio=0.30, - income_layer_base_loss_budget_ratio=0.08, - income_layer_min_loss_budget_ratio=0.06, - income_layer_loss_budget_decay_per_double=0.01, + income_layer_max_ratio=0.55, + income_layer_ratio_mode="log_total_drawdown_budget", + income_layer_core_stress_drawdown_ratio=0.45, + income_layer_income_stress_drawdown_ratio=0.08, + income_layer_base_drawdown_budget_ratio=0.45, + income_layer_min_drawdown_budget_ratio=0.25, + income_layer_drawdown_budget_decay_per_double=0.05, ) - self.assertAlmostEqual(ratio, 0.20) + self.assertAlmostEqual(ratio, 0.27027027027027023) def test_tqqq_growth_income_live_dual_drive_uses_stateful_ma200_exit(self): _skip_if_missing_numeric_stack() @@ -1051,14 +1051,14 @@ def test_soxl_soxx_trend_income_supports_diversified_income_basket(self): self.assertEqual(plan["income_layer_symbols"], income_symbols) self.assertEqual(plan["limit_order_symbols"], ("SOXL", "SOXX", *income_symbols)) - self.assertAlmostEqual(plan["targets"]["SOXL"], 157500.0) - self.assertAlmostEqual(plan["targets"]["SOXX"], 45000.0) - self.assertAlmostEqual(plan["targets"]["BOXX"], 22500.0) - self.assertAlmostEqual(plan["targets"]["SCHD"], 30000.0) - self.assertAlmostEqual(plan["targets"]["DGRO"], 15000.0) - self.assertAlmostEqual(plan["targets"]["SGOV"], 15000.0) - self.assertAlmostEqual(plan["targets"]["SPYI"], 11250.0) - self.assertAlmostEqual(plan["targets"]["QQQI"], 3750.0) + self.assertAlmostEqual(plan["targets"]["SOXL"], 183076.9230769231) + self.assertAlmostEqual(plan["targets"]["SOXX"], 52307.69230769232) + self.assertAlmostEqual(plan["targets"]["BOXX"], 26153.84615384616) + self.assertAlmostEqual(plan["targets"]["SCHD"], 15384.61538461538) + self.assertAlmostEqual(plan["targets"]["DGRO"], 7692.30769230769) + self.assertAlmostEqual(plan["targets"]["SGOV"], 7692.30769230769) + self.assertAlmostEqual(plan["targets"]["SPYI"], 5769.230769230767) + self.assertAlmostEqual(plan["targets"]["QQQI"], 1923.0769230769226) def test_soxl_soxx_trend_income_overlay_cap_can_downgrade_live_tier(self): _skip_if_missing_numeric_stack()