Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions application/signal_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
8 changes: 8 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize nested threshold detail before formatting

When the strategy emits this new dynamic volatility reason in the same structured signal_display format used by the rest of the Telegram renderer, threshold_detail is another translation key such as blend_gate_volatility_threshold_detail_dynamic: .... _localize_structured_text only recursively localizes reason/reasons, so this template inserts that raw key and arguments into the alert instead of the intended human-readable dynamic/fixed threshold text; the new Telegram context is therefore not actually readable for dynamic volatility signals unless threshold_detail is localized before formatting.

Useful? React with 👍 / 👎.

"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 增长收益",
Expand Down Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions tests/test_signal_snapshot.py
Original file line number Diff line number Diff line change
@@ -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