diff --git a/application/rebalance_service.py b/application/rebalance_service.py index a09e63d..83453a4 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -86,6 +86,48 @@ def _translate_snapshot_guard_decision(decision: object, *, translator) -> str: return value if translated == key else str(translated) +def _localize_timing_contract(contract: str, *, translator) -> str: + value = str(contract or "").strip() + if not value: + return "" + if value == "same_trading_day": + return "当日执行" if _translator_uses_zh(translator) else "same trading day" + if value == "next_trading_day": + return "次一交易日执行" if _translator_uses_zh(translator) else "next trading day" + match = re.fullmatch(r"next_(\d+)_trading_days", value) + if match: + count = int(match.group(1)) + if _translator_uses_zh(translator): + return f"{count}个交易日后执行" + return f"next {count} trading days" + return _localize_notification_text(value, translator=translator) + + +def _render_notification_context_text( + notification_context: Mapping[str, object] | None, + *, + translator, + fallback: str = "", +) -> str: + if not isinstance(notification_context, Mapping): + return fallback + key = str(notification_context.get("code") or "").strip() + if not key: + return fallback + params = dict(notification_context.get("params") or {}) + rendered = translator(key, **params) + return fallback if rendered == key else str(rendered) + + +def _translate_snapshot_guard_decision(decision: object, *, translator) -> str: + value = str(decision or "").strip() + if not value: + return "" + key = f"snapshot_guard_decision_{value}" + translated = translator(key) + return value if translated == key else str(translated) + + def _split_detail_segment(text: str) -> list[str]: value = str(text or "").strip() if not value: @@ -283,12 +325,13 @@ def _build_timing_audit_lines(signal_metadata, *, translator) -> list[str]: if not signal_date and not effective_date and not contract: return [] label = "⏱ 执行时点" if _translator_uses_zh(translator) else "⏱ Timing" + localized_contract = _localize_timing_contract(contract, translator=translator) if signal_date and effective_date: value = f"{signal_date} -> {effective_date}" else: - value = signal_date or effective_date or contract - if contract and contract not in value: - value = f"{value} ({contract})" if value else contract + value = signal_date or effective_date or localized_contract + if localized_contract and localized_contract not in value: + value = f"{value} ({localized_contract})" if value else localized_contract return [f"{label}: {value}"] diff --git a/notifications/renderers.py b/notifications/renderers.py index 7de5b59..e04a5ed 100644 --- a/notifications/renderers.py +++ b/notifications/renderers.py @@ -49,6 +49,23 @@ def _localize_notification_text(text: str, *, translator) -> str: ) +def _localize_timing_contract(contract: str, *, translator) -> str: + value = str(contract or "").strip() + if not value: + return "" + if value == "same_trading_day": + return "当日执行" if _translator_uses_zh(translator) else "same trading day" + if value == "next_trading_day": + return "次一交易日执行" if _translator_uses_zh(translator) else "next trading day" + match = re.fullmatch(r"next_(\d+)_trading_days", value) + if match: + count = int(match.group(1)) + if _translator_uses_zh(translator): + return f"{count}个交易日后执行" + return f"next {count} trading days" + return _localize_notification_text(value, translator=translator) + + def _split_detail_segment(text: str) -> list[str]: value = str(text or "").strip() if not value: @@ -246,12 +263,13 @@ def _build_timing_audit_lines(signal_metadata, *, translator) -> list[str]: if not signal_date and not effective_date and not contract: return [] label = "⏱ 执行时点" if _translator_uses_zh(translator) else "⏱ Timing" + localized_contract = _localize_timing_contract(contract, translator=translator) if signal_date and effective_date: value = f"{signal_date} -> {effective_date}" else: - value = signal_date or effective_date or contract - if contract and contract not in value: - value = f"{value} ({contract})" if value else contract + value = signal_date or effective_date or localized_contract + if localized_contract and localized_contract not in value: + value = f"{value} ({localized_contract})" if value else localized_contract return [f"{label}: {value}"] diff --git a/notifications/telegram.py b/notifications/telegram.py index 4565aa4..19f6aac 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -12,6 +12,8 @@ "strategy_label": "🧭 策略: {name}", "equity": "净值", "buying_power": "购买力", + "reserved_cash": "预留现金", + "investable_cash": "可投资现金", "empty_positions": " (空仓)", "empty_target_weights": " (无目标持仓)", "account_summary_title": "📊 账户摘要", @@ -100,6 +102,8 @@ "strategy_label": "🧭 Strategy: {name}", "equity": "Equity", "buying_power": "Buying Power", + "reserved_cash": "Reserved Cash", + "investable_cash": "Investable Cash", "empty_positions": " (No positions)", "empty_target_weights": " (No target positions)", "account_summary_title": "📊 Account Summary", diff --git a/requirements.txt b/requirements.txt index aaeebf2..1fe15d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@c24d4c52e84c8c696006532590b15e9be92c8d89 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@a56c2adcb04beaf5f3dffb3a7e564497f97d0df1 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@fcffc7e3901089ddc10e161d4e5703417c0a03d3 pandas numpy requests diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index f568e04..51922f3 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -240,7 +240,7 @@ def fake_execute_rebalance( assert "Total assets (strategy symbols + cash): $1,000.00" in observed["messages"][0] assert "💼 Strategy holdings" in observed["messages"][0] assert "📏 breadth=60.0%" in observed["messages"][0] - assert "⏱ Timing: 2026-04-01 -> 2026-04-02 (next_trading_day)" in observed["messages"][0] + assert "⏱ Timing: 2026-04-01 -> 2026-04-02 (next trading day)" in observed["messages"][0] assert "Target Weights" not in observed["messages"][0]