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
51 changes: 40 additions & 11 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import os
import re
import traceback
from datetime import datetime

Expand Down Expand Up @@ -79,6 +80,7 @@
("fail_closed", "关闭执行"),
("reason=", "原因="),
)
_DETAIL_FIELD_SPLIT_RE = re.compile(r"\s+(?=[^\s=::]+[=::])")


def _plan_portfolio(plan):
Expand Down Expand Up @@ -116,24 +118,53 @@ 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
for key in ("benchmark_price", "long_trend_value", "exit_line")
)


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]:
Expand All @@ -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:
Expand All @@ -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))



Expand Down
12 changes: 10 additions & 2 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "⚠️ 本轮没有可执行订单",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 19 additions & 9 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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])
Expand Down