diff --git a/application/signal_snapshot.py b/application/signal_snapshot.py index 4e0463d..992e23f 100644 --- a/application/signal_snapshot.py +++ b/application/signal_snapshot.py @@ -24,6 +24,17 @@ "trend_rsi14_dynamic_threshold", "trend_rsi14_effective_threshold", "trend_bb_upper", + "blend_gate_volatility_delever_symbol", + "blend_gate_volatility_delever_window", + "blend_gate_volatility_delever_threshold_mode", + "blend_gate_volatility_delever_threshold", + "blend_gate_volatility_delever_dynamic_threshold", + "blend_gate_volatility_delever_dynamic_sample_count", + "blend_gate_volatility_delever_dynamic_lookback", + "blend_gate_volatility_delever_dynamic_percentile", + "blend_gate_volatility_delever_dynamic_min_periods", + "blend_gate_volatility_delever_dynamic_floor", + "blend_gate_volatility_delever_dynamic_cap", "blend_gate_volatility_delever_metric", "blend_gate_volatility_delever_triggered", ) diff --git a/notifications/telegram.py b/notifications/telegram.py index 308f575..e6e459a 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -210,6 +210,10 @@ def format_small_account_cash_substitution_notes( "blend_gate_reason_rsi_cap": "RSI 超阈值", "blend_gate_reason_bollinger_cap": "突破布林上轨", "blend_gate_reason_volatility_delever": "{symbol} {window} 日年化波动率 {volatility} 高于 {threshold},SOXL 转向 {redirect_symbol}", + "blend_gate_reason_volatility_delever_dynamic": "{symbol} {window} 日年化波动率 {volatility} 高于实际阈值 {threshold}({threshold_detail}),SOXL 转向 {redirect_symbol}", + "blend_gate_volatility_threshold_detail_dynamic": "动态 {percentile},{lookback}日窗口,范围 {floor}-{cap},样本 {sample_count}", + "blend_gate_volatility_threshold_detail_dynamic_fallback": "动态样本不足,回退固定 {fixed_threshold}(样本 {sample_count}/{min_periods},{percentile})", + "blend_gate_volatility_threshold_detail_fixed": "固定阈值 {threshold}", "small_account_warning_note": "小账户提示:净值 {portfolio_equity} 低于建议 {min_recommended_equity};{reason}", "small_account_warning_reason_integer_shares_min_position_value_may_prevent_backtest_replication": "整数股和最小仓位限制可能导致实盘无法完全复现回测", "strategy_name_tqqq_growth_income": "TQQQ 增长收益", @@ -325,6 +329,10 @@ def format_small_account_cash_substitution_notes( "blend_gate_reason_rsi_cap": "RSI over threshold", "blend_gate_reason_bollinger_cap": "price above upper band", "blend_gate_reason_volatility_delever": "{symbol} {window}d annualized volatility {volatility} is above {threshold}; redirect SOXL to {redirect_symbol}", + "blend_gate_reason_volatility_delever_dynamic": "{symbol} {window}d annualized volatility {volatility} is above effective threshold {threshold} ({threshold_detail}); redirect SOXL to {redirect_symbol}", + "blend_gate_volatility_threshold_detail_dynamic": "dynamic {percentile}, {lookback}d lookback, bounded {floor}-{cap}, samples {sample_count}", + "blend_gate_volatility_threshold_detail_dynamic_fallback": "dynamic warm-up, fallback fixed {fixed_threshold} (samples {sample_count}/{min_periods}, {percentile})", + "blend_gate_volatility_threshold_detail_fixed": "fixed threshold {threshold}", "small_account_warning_note": "small account warning: portfolio equity {portfolio_equity} is below recommended {min_recommended_equity}; {reason}", "small_account_warning_reason_integer_shares_min_position_value_may_prevent_backtest_replication": "integer-share minimum position sizing may prevent backtest replication", "strategy_name_tqqq_growth_income": "TQQQ Growth Income", diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 2165ae7..60845ae 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -138,6 +138,46 @@ def test_notification_i18n_keys_are_aligned(): assert set(I18N["zh"]) == set(I18N["en"]) assert build_translator("zh")("account_label", account="****1234") == "🆔 账户: ****1234" assert build_translator("en")("account_label", account="****1234") == "🆔 Account: ****1234" + zh = build_translator("zh") + assert ( + zh( + "blend_gate_reason_volatility_delever_dynamic", + symbol="SOXX", + window=10, + volatility="61.0%", + threshold="60.0%", + threshold_detail=zh( + "blend_gate_volatility_threshold_detail_dynamic", + percentile="p95", + lookback="252", + floor="50.0%", + cap="75.0%", + sample_count="252", + ), + redirect_symbol="SOXX", + ) + == "SOXX 10 日年化波动率 61.0% 高于实际阈值 60.0%(动态 p95,252日窗口,范围 50.0%-75.0%,样本 252),SOXL 转向 SOXX" + ) + en = build_translator("en") + assert ( + en( + "blend_gate_reason_volatility_delever_dynamic", + symbol="SOXX", + window=10, + volatility="61.0%", + threshold="60.0%", + threshold_detail=en( + "blend_gate_volatility_threshold_detail_dynamic", + percentile="p95", + lookback="252", + floor="50.0%", + cap="75.0%", + sample_count="252", + ), + redirect_symbol="SOXX", + ) + == "SOXX 10d annualized volatility 61.0% is above effective threshold 60.0% (dynamic p95, 252d lookback, bounded 50.0%-75.0%, samples 252); redirect SOXL to SOXX" + ) def test_run_strategy_cycle_builds_dry_run_order(monkeypatch): diff --git a/tests/test_signal_snapshot.py b/tests/test_signal_snapshot.py new file mode 100644 index 0000000..81568dd --- /dev/null +++ b/tests/test_signal_snapshot.py @@ -0,0 +1,23 @@ +from application.signal_snapshot import build_signal_snapshot + + +def test_includes_soxl_dynamic_volatility_fields(): + snapshot = build_signal_snapshot( + platform="firstrade", + strategy_profile="soxl_soxx_trend_income", + execution={ + "blend_gate_volatility_delever_threshold_mode": "rolling_percentile", + "blend_gate_volatility_delever_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_sample_count": 252, + "blend_gate_volatility_delever_dynamic_percentile": 0.95, + "blend_gate_volatility_delever_metric": 0.61, + "blend_gate_volatility_delever_triggered": True, + }, + ) + + indicators = snapshot["indicators"] + assert indicators["blend_gate_volatility_delever_threshold_mode"] == "rolling_percentile" + assert indicators["blend_gate_volatility_delever_dynamic_threshold"] == 0.60 + assert indicators["blend_gate_volatility_delever_dynamic_sample_count"] == 252 + assert indicators["blend_gate_volatility_delever_triggered"] is True