diff --git a/src/crypto_strategies/catalog.py b/src/crypto_strategies/catalog.py index 27e6fed..69a0f16 100644 --- a/src/crypto_strategies/catalog.py +++ b/src/crypto_strategies/catalog.py @@ -143,6 +143,7 @@ "btc_weight": 0.30, "trend_weight": 0.70, "dynamic_mode": True, + "dynamic_regime_off_cut": 0.50, "smart_multiplier_enabled": True, "cycle_indicator_enabled": True, "zscore_exit_enabled": True, diff --git a/src/crypto_strategies/entrypoints/__init__.py b/src/crypto_strategies/entrypoints/__init__.py index 4f12f36..c2c60c5 100644 --- a/src/crypto_strategies/entrypoints/__init__.py +++ b/src/crypto_strategies/entrypoints/__init__.py @@ -460,6 +460,7 @@ def evaluate_crypto_equity_combo(ctx: StrategyContext) -> StrategyDecision: btc_weight=float(config.get("btc_weight", 0.30)), trend_weight=float(config.get("trend_weight", 0.70)), dynamic_mode=bool(config.get("dynamic_mode", True)), + dynamic_regime_off_cut=float(config.get("dynamic_regime_off_cut", 0.50)), smart_multiplier_enabled=bool(config.get("smart_multiplier_enabled", True)), cycle_indicator_enabled=bool(config.get("cycle_indicator_enabled", True)), zscore_exit_enabled=bool(config.get("zscore_exit_enabled", True)), diff --git a/src/crypto_strategies/manifests/__init__.py b/src/crypto_strategies/manifests/__init__.py index e5543fc..307c49a 100644 --- a/src/crypto_strategies/manifests/__init__.py +++ b/src/crypto_strategies/manifests/__init__.py @@ -146,6 +146,7 @@ "btc_weight": 0.30, "trend_weight": 0.70, "dynamic_mode": True, + "dynamic_regime_off_cut": 0.50, "smart_multiplier_enabled": True, "cycle_indicator_enabled": True, "zscore_exit_enabled": True, diff --git a/src/crypto_strategies/strategies/crypto_equity_combo.py b/src/crypto_strategies/strategies/crypto_equity_combo.py index d113b46..3ac8b09 100644 --- a/src/crypto_strategies/strategies/crypto_equity_combo.py +++ b/src/crypto_strategies/strategies/crypto_equity_combo.py @@ -208,6 +208,7 @@ def build_target_weights( btc_weight: float = DEFAULT_BTC_WEIGHT, trend_weight: float = DEFAULT_TREND_WEIGHT, dynamic_mode: bool = True, + dynamic_regime_off_cut: float = DYNAMIC_REGIME_OFF_CUT, translator=None, **kwargs: Any, ) -> tuple[dict[str, float], dict[str, object]]: @@ -243,9 +244,11 @@ def build_target_weights( regime_off = not btc_snapshot.get("regime_on", True) if dynamic_mode and regime_off: - effective_btc = btc_weight + trend_weight * DYNAMIC_REGIME_OFF_CUT - effective_trend = trend_weight * (1.0 - DYNAMIC_REGIME_OFF_CUT) + regime_off_cut = _clamp_ratio(dynamic_regime_off_cut, default=DYNAMIC_REGIME_OFF_CUT) + effective_btc = btc_weight + trend_weight * regime_off_cut + effective_trend = trend_weight * (1.0 - regime_off_cut) else: + regime_off_cut = 0.0 effective_btc = btc_weight effective_trend = trend_weight @@ -292,6 +295,7 @@ def build_target_weights( "trend_weight": effective_trend, "base_btc_weight": btc_weight, "base_trend_weight": trend_weight, + "dynamic_regime_off_cut": regime_off_cut, }, "btc_leg": {"weights": btc_weights, **btc_leg_metadata}, "trend_leg": {"weights": trend_weights, **trend_metadata}, diff --git a/tests/test_crypto_equity_combo.py b/tests/test_crypto_equity_combo.py index 3cf6d48..708a0a2 100644 --- a/tests/test_crypto_equity_combo.py +++ b/tests/test_crypto_equity_combo.py @@ -125,6 +125,37 @@ def test_trend_leg_honors_rotation_refresh_lock(self) -> None: self.assertEqual(positive_weights, {"ETHUSDT"}) self.assertEqual(set(metadata["trend_leg"]["weights"]), {"ETHUSDT"}) + def test_dynamic_regime_off_cut_is_configurable(self) -> None: + """Regime-off cut should be configurable while keeping the 50% default.""" + _, default_metadata = build_target_weights( + prices={"BTCUSDT": 60000.0}, + indicators_map={}, + universe_snapshot=[], + benchmark_snapshot={"regime_on": False}, + portfolio={"total_equity": 100000.0, "buying_power": 1000.0}, + btc_weight=0.30, + trend_weight=0.70, + smart_multiplier_enabled=False, + ) + _, custom_metadata = build_target_weights( + prices={"BTCUSDT": 60000.0}, + indicators_map={}, + universe_snapshot=[], + benchmark_snapshot={"regime_on": False}, + portfolio={"total_equity": 100000.0, "buying_power": 1000.0}, + btc_weight=0.30, + trend_weight=0.70, + dynamic_regime_off_cut=0.30, + smart_multiplier_enabled=False, + ) + + self.assertAlmostEqual(default_metadata["combo"]["btc_weight"], 0.65) + self.assertAlmostEqual(default_metadata["combo"]["trend_weight"], 0.35) + self.assertAlmostEqual(default_metadata["combo"]["dynamic_regime_off_cut"], 0.50) + self.assertAlmostEqual(custom_metadata["combo"]["btc_weight"], 0.51) + self.assertAlmostEqual(custom_metadata["combo"]["trend_weight"], 0.49) + self.assertAlmostEqual(custom_metadata["combo"]["dynamic_regime_off_cut"], 0.30) + def test_compute_signals_returns_tuple(self) -> None: """compute_signals should return a 5-tuple with weights, signal_desc, cash_residual, status_desc, metadata.""" prices = {"BTCUSDT": 60000.0}