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
5 changes: 5 additions & 0 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,11 @@ def acquire_submission_claim() -> bool:
"strategy_run_stage": strategy_run_stage,
"strategy_run_persisted": strategy_run_persisted,
"portfolio": plan.get("portfolio", {}),
"heartbeat_account_snapshot": {
"available_cash": snapshot.cash_balance,
"net_assets": (getattr(snapshot, "metadata", {}) or {}).get("broker_account_equity"),
"observed_at": snapshot.as_of.isoformat(),
},
"allocation": plan.get("allocation", {}),
"execution": plan.get("execution", {}),
"signal_snapshot": signal_snapshot,
Expand Down
2 changes: 2 additions & 0 deletions application/runtime_broker_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ def build_portfolio_snapshot(self) -> PortfolioSnapshot:
prefer_cash_plus_positions=bool(managed),
cash_only_execution=self.cash_only_execution,
)
broker_account_equity = _first_numeric_by_keyword_groups(balances, _TOTAL_EQUITY_KEYWORD_GROUPS)
return PortfolioSnapshot(
as_of=self.clock(),
total_equity=float(total_equity),
Expand All @@ -321,6 +322,7 @@ def build_portfolio_snapshot(self) -> PortfolioSnapshot:
"account_hash": self.account_hash or mask_account_id(self.account),
"api_kind": "unofficial-reverse-engineered",
"total_equity_source": total_equity_source,
"broker_account_equity": broker_account_equity,
"cash_only_execution": self.cash_only_execution,
"market_currency_cash": cash_balance,
"available_funds": reported_buying_power,
Expand Down
26 changes: 26 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from quant_platform_kit.common.operational_notification_localization import resolve_operational_notification_locale

import math
import re
from collections.abc import Callable, Mapping
from typing import Any
Expand Down Expand Up @@ -158,6 +159,10 @@ def format_small_account_whole_share_bootstrap_notes(
"runtime_failure_log": "策略运行失败(runtime_setup_failed)",
"rebalance_title": "🔔 【调仓指令】",
"heartbeat_title": "💓 【心跳检测】",
"heartbeat_available_cash": "💵 可用现金: {value}",
"heartbeat_account_equity": "💰 账户总权益: {value}",
"heartbeat_observed_at": "🕒 账户观察时间: {value}",
"heartbeat_unverified": "未核实",
"strategy_label": "🧭 策略: {name}",
"account_label": "🆔 账户: {account}",
"dry_run_banner": "🧪 模拟运行,本轮不提交真实订单",
Expand Down Expand Up @@ -339,6 +344,10 @@ def format_small_account_whole_share_bootstrap_notes(
"runtime_failure_log": "Strategy run failed (runtime_setup_failed)",
"rebalance_title": "🔔 【Rebalance Instruction】",
"heartbeat_title": "💓 【Heartbeat】",
"heartbeat_available_cash": "💵 Available cash: {value}",
"heartbeat_account_equity": "💰 Total account equity: {value}",
"heartbeat_observed_at": "🕒 Account observed: {value}",
"heartbeat_unverified": "Unverified",
"strategy_label": "🧭 Strategy: {name}",
"account_label": "🆔 Account: {account}",
"dry_run_banner": "🧪 Dry run only; no live orders submitted",
Expand Down Expand Up @@ -1081,6 +1090,23 @@ def render_cycle_summary(result: Mapping[str, Any], *, lang: str = "en") -> str:
lines.append(translator("strategy_label", name=strategy_name))
if account:
lines.append(translator("account_label", account=account))
if not submitted:
account_snapshot = result.get("heartbeat_account_snapshot")
account_snapshot = account_snapshot if isinstance(account_snapshot, Mapping) else {}
observed_at = str(account_snapshot.get("observed_at") or "").strip()
verified = False
for field, label in (("available_cash", "heartbeat_available_cash"), ("net_assets", "heartbeat_account_equity")):
amount = account_snapshot.get(field)
valid = (
isinstance(amount, (int, float)) and not isinstance(amount, bool)
and math.isfinite(amount) and bool(observed_at)
and (field != "net_assets" or amount > 0)
)
verified = verified or valid
value = f"USD {amount:,.2f}" if valid else translator("heartbeat_unverified")
lines.append(translator(label, value=value))
if verified:
lines.append(translator("heartbeat_observed_at", value=observed_at))
if dry_run_only:
lines.append(translator("dry_run_banner"))
if bool(result.get("execution_blocked")):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_notifications_telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
from notifications.telegram import render_cycle_summary


def test_no_trade_heartbeat_shows_broker_account_values_in_both_locales():
result = {
"account": "****1234",
"strategy_profile": "soxl_soxx_trend_income",
"portfolio": {"total_equity": 25.0, "liquid_cash": 10.0},
"execution": {},
"submitted_orders": [],
"skipped_orders": [],
"heartbeat_account_snapshot": {
"available_cash": 100.0,
"net_assets": 1234.56,
"observed_at": "2026-09-25T19:45:00+00:00",
},
}
zh = render_cycle_summary(result, lang="zh")
en = render_cycle_summary(result, lang="en")
assert "可用现金: USD 100.00" in zh
assert "账户总权益: USD 1,234.56" in zh
assert "Available cash: USD 100.00" in en
assert "Total account equity: USD 1,234.56" in en


def test_no_trade_heartbeat_marks_missing_account_values_unverified():
message = render_cycle_summary({
"account": "****1234",
"portfolio": {},
"execution": {},
"submitted_orders": [],
"skipped_orders": [],
"heartbeat_account_snapshot": {"available_cash": 0.0, "net_assets": None},
}, lang="en")
assert "Available cash: Unverified" in message
assert "Total account equity: Unverified" in message


def test_render_cycle_summary_dashboard_text_does_not_hide_account_overview():
message = render_cycle_summary(
{
Expand Down
2 changes: 2 additions & 0 deletions tests/test_runtime_broker_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def test_runtime_adapters_build_quote_and_portfolio_ports():
assert portfolio.cash_balance == 20.0
assert portfolio.positions[0].symbol == "SPY"
assert portfolio.metadata["total_equity_source"] == "cash_plus_positions"
assert portfolio.metadata["broker_account_equity"] == 120.0


def test_portfolio_snapshot_uses_account_value_balance_key():
Expand All @@ -72,6 +73,7 @@ def get_balances(self, _account):
assert portfolio.total_equity == 221.0
assert portfolio.cash_balance == 200.0
assert portfolio.metadata["total_equity_source"] == "cash_plus_positions"
assert portfolio.metadata["broker_account_equity"] == 1234.56


def test_managed_portfolio_snapshot_ignores_full_account_value_balance_key():
Expand Down
Loading