diff --git a/application/rebalance_service.py b/application/rebalance_service.py index ec382bb..6276d19 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import re import traceback from datetime import datetime @@ -79,6 +80,7 @@ ("fail_closed", "关闭执行"), ("reason=", "原因="), ) +_DETAIL_FIELD_SPLIT_RE = re.compile(r"\s+(?=[^\s=::]+[=::])") def _plan_portfolio(plan): @@ -116,6 +118,33 @@ def _localize_notification_text(text, *, translator): return localized +def _split_detail_segment(text): + value = str(text or "").strip() + if not value: + return [] + if "=" not in value and ":" not in value and ":" not in value: + return [value] + return [part.strip() for part in _DETAIL_FIELD_SPLIT_RE.split(value) if part.strip()] + + +def _split_labeled_text(text): + segments = [segment.strip() for segment in str(text or "").split(" | ") if segment.strip()] + if not segments: + return [] + lines = [segments[0]] + for segment in segments[1:]: + lines.extend(_split_detail_segment(segment)) + return lines + + +def _append_labeled_text(lines, template_key, value, *, translator, value_key): + parts = _split_labeled_text(value) + if not parts: + return + lines.append(translator(template_key, **{value_key: parts[0]})) + lines.extend(f" - {part}" for part in parts[1:]) + + def _has_benchmark_context(execution): return any( float(execution.get(key) or 0.0) > 0.0 @@ -123,17 +152,19 @@ def _has_benchmark_context(execution): ) -def _build_benchmark_line(execution): +def _build_benchmark_lines(execution, *, translator): if not _has_benchmark_context(execution): - return None + return [] benchmark_symbol = str(execution.get("benchmark_symbol") or "QQQ") benchmark_price = float(execution.get("benchmark_price") or 0.0) long_trend_value = float(execution.get("long_trend_value") or 0.0) exit_line = float(execution.get("exit_line") or 0.0) - return ( - f"{benchmark_symbol}: {benchmark_price:.2f} | " - f"MA200: {long_trend_value:.2f} | Exit: {exit_line:.2f}" - ) + return [ + translator("benchmark_title", symbol=benchmark_symbol), + f" - {translator('benchmark_price', symbol=benchmark_symbol, value=f'{benchmark_price:.2f}')}", + f" - {translator('benchmark_ma200', value=f'{long_trend_value:.2f}')}", + f" - {translator('benchmark_exit', value=f'{exit_line:.2f}')}", + ] def _format_holdings_lines(portfolio_rows, market_values, *, translator) -> list[str]: @@ -147,7 +178,7 @@ def _format_holdings_lines(portfolio_rows, market_values, *, translator) -> list def _append_status_lines(lines, *, execution, translator, signal_key): status_display = _localize_notification_text(execution.get("status_display"), translator=translator) if status_display: - lines.append(translator("market_status", status=status_display)) + _append_labeled_text(lines, "market_status", status_display, translator=translator, value_key="status") deploy_ratio_text = str(execution.get("deploy_ratio_text") or "").strip() if deploy_ratio_text: @@ -163,11 +194,9 @@ def _append_status_lines(lines, *, execution, translator, signal_key): signal_display = _localize_notification_text(execution.get("signal_display"), translator=translator) if signal_display: - lines.append(translator(signal_key, msg=signal_display)) + _append_labeled_text(lines, signal_key, signal_display, translator=translator, value_key="msg") - benchmark_line = _build_benchmark_line(execution) - if benchmark_line: - lines.append(benchmark_line) + lines.extend(_build_benchmark_lines(execution, translator=translator)) diff --git a/notifications/telegram.py b/notifications/telegram.py index 8e52584..6a43b9f 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -26,10 +26,14 @@ "signal": "🎯 触发信号: {msg}", "heartbeat_title": "💓 【心跳检测】", "equity": "💰 净值: ${value}", - "cash_summary": "💵 账户现金: ${available} | 可投资现金: ${investable}", + "cash_summary": "💵 资金\n - 账户现金: ${available}\n - 可投资现金: ${investable}", "cash_label": "现金", "holdings_title": "💼 持仓", "order_logs_title": "🧾 执行明细", + "benchmark_title": "📈 {symbol} 基准", + "benchmark_price": "{symbol}: {value}", + "benchmark_ma200": "MA200: {value}", + "benchmark_exit": "退出线: {value}", "heartbeat_signal": "🎯 信号: {msg}", "no_trades": "✅ 无需调仓", "no_executable_orders": "⚠️ 本轮没有可执行订单", @@ -95,10 +99,14 @@ "signal": "🎯 Signal: {msg}", "heartbeat_title": "💓 【Heartbeat】", "equity": "💰 Equity: ${value}", - "cash_summary": "💵 Cash: ${available} | Investable cash: ${investable}", + "cash_summary": "💵 Cash\n - Account cash: ${available}\n - Investable cash: ${investable}", "cash_label": "Cash", "holdings_title": "💼 Holdings", "order_logs_title": "🧾 Execution details", + "benchmark_title": "📈 {symbol} Benchmark", + "benchmark_price": "{symbol}: {value}", + "benchmark_ma200": "MA200: {value}", + "benchmark_exit": "Exit: {value}", "heartbeat_signal": "🎯 Signal: {msg}", "no_trades": "✅ No trades needed", "no_executable_orders": "⚠️ No executable orders this cycle", diff --git a/tests/test_notifications.py b/tests/test_notifications.py index c51917e..83694df 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -30,8 +30,13 @@ class NotificationTests(unittest.TestCase): def test_build_translator_supports_chinese(self): translate = build_translator("zh") self.assertEqual(translate("equity", value="123.45"), "💰 净值: $123.45") + self.assertEqual( + translate("cash_summary", available="1.00", investable="2.00"), + "💵 资金\n - 账户现金: $1.00\n - 可投资现金: $2.00", + ) self.assertEqual(translate("holdings_title"), "💼 持仓") self.assertEqual(translate("order_logs_title"), "🧾 执行明细") + self.assertEqual(translate("benchmark_title", symbol="QQQ"), "📈 QQQ 基准") self.assertEqual(translate("market_status_blend_gate_risk_on", asset="SOXX+SOXL"), "🚀 风险开启(SOXX+SOXL)") self.assertEqual( translate( diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index c685aac..2d8068e 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -103,7 +103,8 @@ def test_append_status_lines_localizes_snapshot_guard_text_for_zh(self): signal_key="heartbeat_signal", ) - self.assertIn("📊 市场状态: 关闭执行 | 原因=缺少特征快照路径", lines) + self.assertIn("📊 市场状态: 关闭执行", lines) + self.assertIn(" - 原因=缺少特征快照路径", lines) self.assertIn("🎯 信号: 特征快照校验阻止执行", lines) def test_append_status_lines_localizes_qqq_tech_diagnostics_for_zh(self): @@ -122,9 +123,12 @@ def test_append_status_lines_localizes_qqq_tech_diagnostics_for_zh(self): ) self.assertIn( - "📊 市场状态: 市场阶段=软防御 | 市场宽度=41.2% | 目标股票仓位=60.0% | 实际股票仓位=60.0%", + "📊 市场状态: 市场阶段=软防御", lines, ) + self.assertIn(" - 市场宽度=41.2%", lines) + self.assertIn(" - 目标股票仓位=60.0%", lines) + self.assertIn(" - 实际股票仓位=60.0%", lines) self.assertIn( "🎯 触发信号: 市场阶段=软防御 市场宽度=41.2% 基准趋势=向下 " "目标股票仓位=60.0% 实际股票仓位=60.0% 入选标的数=8 前排标的=CIEN(0.92)", @@ -152,14 +156,18 @@ def test_append_status_lines_localizes_runtime_diagnostic_tail_for_zh(self): ) self.assertIn( - "📊 市场状态: 不执行 | 原因=当前不在月度执行窗口 快照日期=2026-04-10 允许日期=2026-04-13", - lines, - ) - self.assertIn( - "🎯 信号: 月度快照节奏 | 等待进入执行窗口 | 小账户提示=是 净值=$0 " - "建议最低净值=$1,000 原因=整数股和最小仓位限制可能导致实盘无法完全复现回测", + "📊 市场状态: 不执行", lines, ) + self.assertIn(" - 原因=当前不在月度执行窗口", lines) + self.assertIn(" - 快照日期=2026-04-10", lines) + self.assertIn(" - 允许日期=2026-04-13", lines) + self.assertIn("🎯 信号: 月度快照节奏", lines) + self.assertIn(" - 等待进入执行窗口", lines) + self.assertIn(" - 小账户提示=是", lines) + self.assertIn(" - 净值=$0", lines) + self.assertIn(" - 建议最低净值=$1,000", lines) + self.assertIn(" - 原因=整数股和最小仓位限制可能导致实盘无法完全复现回测", lines) def _run_strategy( self, @@ -609,6 +617,7 @@ def test_heartbeat_accepts_normalized_portfolio_and_execution_sections(self): self.assertEqual(len(sent_messages), 1) self.assertIn("💓 【心跳检测】", sent_messages[0]) self.assertIn("可投资现金", sent_messages[0]) + self.assertIn("💵 资金\n - 账户现金:", sent_messages[0]) self.assertIn("💼 持仓", sent_messages[0]) self.assertIn(" - SOXX:", sent_messages[0]) @@ -655,8 +664,9 @@ def test_hybrid_heartbeat_hides_empty_semiconductor_fields_and_shows_benchmark_l self.assertIn(" - TQQQ: $0.00", sent_messages[0]) self.assertIn(" - BOXX: $0.00", sent_messages[0]) self.assertIn(" - QQQI: $0.00", sent_messages[0]) - self.assertIn("QQQ: 588.50 | MA200: 595.25 | Exit: 573.00", sent_messages[0]) + self.assertIn("📈 QQQ 基准\n - QQQ: 588.50\n - MA200: 595.25\n - 退出线: 573.00", sent_messages[0]) self.assertIn("🎯 信号: 💤 等待信号", sent_messages[0]) + self.assertNotIn("账户现金: $0.00 | 可投资现金", sent_messages[0]) self.assertNotIn("TQQQ: $0.00 BOXX", sent_messages[0]) self.assertNotIn("📊 市场状态: ", sent_messages[0]) self.assertNotIn("💼 交易层风险仓位: ", sent_messages[0])