From e6bf3feaedcfd3dc9506e887399fbb2ac03c83eb Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 7 May 2026 19:30:16 +0800 Subject: [PATCH] Use dynamic RSI threshold for SOXL overlay --- pyproject.toml | 4 +-- src/us_equity_strategies/catalog.py | 5 +++ .../entrypoints/__init__.py | 4 +++ .../manifests/__init__.py | 1 + .../strategies/soxl_soxx_trend_income.py | 17 +++++++-- tests/test_strategy_plans.py | 35 +++++++++++++++++++ 6 files changed, 62 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0188afd..7b45957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "us-equity-strategies" -version = "0.7.32" +version = "0.7.33" description = "Shared US equity strategy catalog and implementations" readme = "README.md" requires-python = ">=3.11" dependencies = [ "pandas>=2.0", "pytz>=2024.1", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@268171a74a1b522f03940af399f71c37e9a32c70", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3368b261b6ab2210ecbfa79d0f93c5d0cd85c37d", ] [tool.setuptools] diff --git a/src/us_equity_strategies/catalog.py b/src/us_equity_strategies/catalog.py index 69b9962..e8d719c 100644 --- a/src/us_equity_strategies/catalog.py +++ b/src/us_equity_strategies/catalog.py @@ -99,6 +99,11 @@ "blend_gate_mid_soxl_weight": 0.65, "blend_gate_active_soxx_weight": 0.20, "blend_gate_defensive_soxx_weight": 0.15, + "blend_gate_rsi_cap_enabled": True, + "blend_gate_rsi_threshold": 70.0, + "blend_gate_dynamic_rsi_threshold_enabled": True, + "blend_gate_bollinger_cap_enabled": True, + "blend_gate_overlay_stack_triggers": True, }, RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: { "benchmark_symbol": "SPY", diff --git a/src/us_equity_strategies/entrypoints/__init__.py b/src/us_equity_strategies/entrypoints/__init__.py index 167fa77..53eacfb 100644 --- a/src/us_equity_strategies/entrypoints/__init__.py +++ b/src/us_equity_strategies/entrypoints/__init__.py @@ -442,6 +442,8 @@ def evaluate_soxl_soxx_trend_income(ctx: StrategyContext) -> StrategyDecision: "trend_ma20": plan.get("trend_ma20"), "trend_ma20_slope": plan.get("trend_ma20_slope"), "trend_rsi14": plan.get("trend_rsi14"), + "trend_rsi14_dynamic_threshold": plan.get("trend_rsi14_dynamic_threshold"), + "trend_rsi14_effective_threshold": plan.get("trend_rsi14_effective_threshold"), "trend_bb_upper": plan.get("trend_bb_upper"), **account_size_diagnostics, "execution_annotations": { @@ -476,6 +478,8 @@ def evaluate_soxl_soxx_trend_income(ctx: StrategyContext) -> StrategyDecision: "trend_ma20": plan.get("trend_ma20"), "trend_ma20_slope": plan.get("trend_ma20_slope"), "trend_rsi14": plan.get("trend_rsi14"), + "trend_rsi14_dynamic_threshold": plan.get("trend_rsi14_dynamic_threshold"), + "trend_rsi14_effective_threshold": plan.get("trend_rsi14_effective_threshold"), "trend_bb_upper": plan.get("trend_bb_upper"), }, } diff --git a/src/us_equity_strategies/manifests/__init__.py b/src/us_equity_strategies/manifests/__init__.py index a89eb43..dea7204 100644 --- a/src/us_equity_strategies/manifests/__init__.py +++ b/src/us_equity_strategies/manifests/__init__.py @@ -124,6 +124,7 @@ def _manifest( "blend_gate_defensive_soxx_weight": 0.15, "blend_gate_rsi_cap_enabled": True, "blend_gate_rsi_threshold": 70.0, + "blend_gate_dynamic_rsi_threshold_enabled": True, "blend_gate_bollinger_cap_enabled": True, "blend_gate_overlay_stack_triggers": True, }, 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 9e23000..0fd1fce 100644 --- a/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py +++ b/src/us_equity_strategies/strategies/soxl_soxx_trend_income.py @@ -134,6 +134,7 @@ def build_rebalance_plan( blend_gate_defensive_soxx_weight=0.15, blend_gate_rsi_cap_enabled=False, blend_gate_rsi_threshold=70.0, + blend_gate_dynamic_rsi_threshold_enabled=False, blend_gate_bollinger_cap_enabled=False, blend_gate_overlay_stack_triggers=False, ): @@ -181,6 +182,9 @@ def build_rebalance_plan( trend_ma20 = _as_float_or_none(_indicator_value(indicators, trend_symbol, "ma20")) trend_ma20_slope = _as_float_or_none(_indicator_value(indicators, trend_symbol, "ma20_slope")) trend_rsi14 = _as_float_or_none(_indicator_value(indicators, trend_symbol, "rsi14")) + trend_rsi14_dynamic_threshold = _as_float_or_none( + _indicator_value(indicators, trend_symbol, "rsi14_dynamic_threshold") + ) trend_bb_mid = _as_float_or_none(_indicator_value(indicators, trend_symbol, "bb_mid")) trend_bb_upper = _as_float_or_none(_indicator_value(indicators, trend_symbol, "bb_upper")) trend_bb_lower = _as_float_or_none(_indicator_value(indicators, trend_symbol, "bb_lower")) @@ -197,11 +201,15 @@ def build_rebalance_plan( target_active_soxx_ratio = _as_clamped_ratio(blend_gate_active_soxx_weight, default=0.20) target_defensive_soxx_ratio = _as_clamped_ratio(blend_gate_defensive_soxx_weight, default=0.15) use_rsi_cap = _as_bool(blend_gate_rsi_cap_enabled, default=False) + use_dynamic_rsi_threshold = _as_bool(blend_gate_dynamic_rsi_threshold_enabled, default=False) use_bollinger_cap = _as_bool(blend_gate_bollinger_cap_enabled, default=False) stack_overlay_triggers = _as_bool(blend_gate_overlay_stack_triggers, default=False) rsi_threshold = _as_float_or_none(blend_gate_rsi_threshold) if rsi_threshold is None: rsi_threshold = 70.0 + effective_rsi_threshold = rsi_threshold + if use_dynamic_rsi_threshold and trend_rsi14_dynamic_threshold is not None: + effective_rsi_threshold = max(rsi_threshold, trend_rsi14_dynamic_threshold) blend_tier = "defensive" if trend_price > trend_entry_line: @@ -212,10 +220,10 @@ def build_rebalance_plan( overlay_trigger_reasons: list[str] = [] overlay_trigger_codes: list[str] = [] if base_blend_tier in {"full", "mid"}: - if use_rsi_cap and trend_rsi14 is not None and trend_rsi14 > rsi_threshold: + if use_rsi_cap and trend_rsi14 is not None and trend_rsi14 > effective_rsi_threshold: overlay_trigger_codes.append("blend_gate_reason_rsi_cap") overlay_trigger_reasons.append( - _translate_with_fallback(translator, "blend_gate_reason_rsi_cap", f"RSI>{rsi_threshold:.0f}") + _translate_with_fallback(translator, "blend_gate_reason_rsi_cap", f"RSI>{effective_rsi_threshold:.0f}") ) if use_bollinger_cap and trend_bb_upper is not None and trend_price > trend_bb_upper: overlay_trigger_codes.append("blend_gate_reason_bollinger_cap") @@ -331,6 +339,8 @@ def build_rebalance_plan( "ma20": trend_ma20, "ma20_slope": trend_ma20_slope, "rsi14": trend_rsi14, + "rsi14_dynamic_threshold": trend_rsi14_dynamic_threshold, + "rsi14_effective_threshold": effective_rsi_threshold, "bb_mid": trend_bb_mid, "bb_upper": trend_bb_upper, "bb_lower": trend_bb_lower, @@ -401,11 +411,14 @@ def build_rebalance_plan( "trend_ma20": trend_ma20, "trend_ma20_slope": trend_ma20_slope, "trend_rsi14": trend_rsi14, + "trend_rsi14_dynamic_threshold": trend_rsi14_dynamic_threshold, + "trend_rsi14_effective_threshold": effective_rsi_threshold, "trend_bb_mid": trend_bb_mid, "trend_bb_upper": trend_bb_upper, "trend_bb_lower": trend_bb_lower, "blend_gate_rsi_cap_enabled": use_rsi_cap, "blend_gate_rsi_threshold": rsi_threshold, + "blend_gate_dynamic_rsi_threshold_enabled": use_dynamic_rsi_threshold, "blend_gate_bollinger_cap_enabled": use_bollinger_cap, "blend_gate_overlay_stack_triggers": stack_overlay_triggers, } diff --git a/tests/test_strategy_plans.py b/tests/test_strategy_plans.py index 4cf4d31..6f77b5e 100644 --- a/tests/test_strategy_plans.py +++ b/tests/test_strategy_plans.py @@ -456,6 +456,41 @@ def test_soxl_soxx_trend_income_overlay_cap_can_downgrade_live_tier(self): "market_status_blend_gate_overlay_capped", ) + dynamic_hold_plan = build_soxl_soxx_plan( + { + "soxl": {"price": 50.0, "ma_trend": 45.0}, + "soxx": { + "price": 109.0, + "ma_trend": 100.0, + "rsi14": 75.0, + "rsi14_dynamic_threshold": 78.0, + "bb_upper": 120.0, + }, + }, + account_state, + **{**common_kwargs, "blend_gate_dynamic_rsi_threshold_enabled": True}, + ) + self.assertEqual(dynamic_hold_plan["blend_tier"], "full") + self.assertEqual(dynamic_hold_plan["overlay_trigger_count"], 0) + self.assertEqual(dynamic_hold_plan["trend_rsi14_effective_threshold"], 78.0) + + dynamic_mid_plan = build_soxl_soxx_plan( + { + "soxl": {"price": 50.0, "ma_trend": 45.0}, + "soxx": { + "price": 109.0, + "ma_trend": 100.0, + "rsi14": 79.0, + "rsi14_dynamic_threshold": 78.0, + "bb_upper": 120.0, + }, + }, + account_state, + **{**common_kwargs, "blend_gate_dynamic_rsi_threshold_enabled": True}, + ) + self.assertEqual(dynamic_mid_plan["blend_tier"], "mid") + self.assertEqual(dynamic_mid_plan["overlay_trigger_codes"], ("blend_gate_reason_rsi_cap",)) + defensive_plan = build_soxl_soxx_plan( { "soxl": {"price": 50.0, "ma_trend": 45.0},