From 249eb77b822bf3960c93ddc713c02d8fa47a93ff Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:57:10 +0800 Subject: [PATCH] Fix Firstrade notification account overview --- notifications/telegram.py | 109 +++++++++++++++++++++++---- tests/test_notifications_telegram.py | 52 +++++++++++++ 2 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 tests/test_notifications_telegram.py diff --git a/notifications/telegram.py b/notifications/telegram.py index 2841872..fad7d53 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -108,6 +108,7 @@ def format_small_account_cash_substitution_notes( _DETAIL_FIELD_SPLIT_RE = re.compile(r",\s*(?=[A-Za-z_][\w-]*\s*=)") _STRUCTURED_PAREN_RE = re.compile(r"^(?P[A-Za-z_][\w-]*)\((?P
.*)\)$") +_DASHBOARD_POSITION_LINE_RE = re.compile(r"^[A-Z][A-Z0-9./-]{0,12}\s*:") I18N = { @@ -469,29 +470,74 @@ def _is_dashboard_signal_line(line: str) -> bool: ) -def _format_dashboard_lines( +def _is_dashboard_account_title(line: str, *, translator: Callable[..., str]) -> bool: + text = str(line or "").strip() + account_titles = { + translator("account_overview_title"), + "πŸ“Œ Strategy Account", + "πŸ“Œ Strategy portfolio", + "πŸ“Œ η­–η•₯θ΄¦ζˆ·ζ¦‚θ§ˆ", + } + return text in account_titles + + +def _is_dashboard_holdings_title(line: str, *, translator: Callable[..., str]) -> bool: + text = str(line or "").strip() + holdings_titles = { + translator("holdings_title"), + "πŸ’Ό Strategy Holdings", + "πŸ’Ό Strategy holdings", + "πŸ’Ό η­–η•₯ζŒδ»“", + } + return text in holdings_titles + + +def _is_dashboard_account_metric_line(line: str, *, translator: Callable[..., str]) -> bool: + text = str(line or "").strip() + lowered = text.lower() + if not text: + return False + if text.startswith((translator("dashboard_label"), "πŸ“Š Dashboard", "πŸ“Š θ΅„δΊ§ηœ‹ζΏ")): + return True + if text.startswith(("Income:", "ζ”Άη›Š:", "ζ”Άε…₯:")): + return True + if _DASHBOARD_POSITION_LINE_RE.match(text) and ( + "$" in text or "θ‚‘" in text or "share" in lowered + ): + return True + metric_labels = { + translator("total_assets"), + translator("buying_power"), + translator("reserved_cash"), + translator("investable_cash"), + translator("equity"), + "Total assets (strategy symbols + cash)", + "Buying power", + "Reserved cash", + "Investable cash", + "Equity", + "ζ€»θ΅„δΊ§οΌˆη­–η•₯ζ ‡ηš„+ηŽ°ι‡‘οΌ‰", + "θ΄­δΉ°εŠ›", + "ι’„η•™ηŽ°ι‡‘", + "ε―ζŠ•θ΅„ηŽ°ι‡‘", + "净值", + } + return any(label and label.lower() in lowered for label in metric_labels) + + +def _format_generated_dashboard_lines( portfolio: Mapping[str, Any], execution: Mapping[str, Any], *, translator: Callable[..., str], ) -> list[str]: - dashboard_text = str(execution.get("dashboard_text") or "").strip() - if dashboard_text: - has_signal_display = bool(str(execution.get("signal_display") or "").strip()) - lines = [] - for line in dashboard_text.splitlines(): - if not line.strip(): - continue - if has_signal_display and _is_dashboard_signal_line(line): - continue - lines.append(_base_localize_notification_text(line.rstrip(), translator=translator)) - return lines - lines = [translator("account_overview_title")] total_equity = _safe_float(portfolio.get("total_equity")) if total_equity is not None: lines.append(f" - {translator('total_assets')}: {_format_money(total_equity)}") - buying_power = _safe_float(portfolio.get("liquid_cash")) + buying_power = _safe_float(portfolio.get("buying_power")) + if buying_power is None: + buying_power = _safe_float(portfolio.get("liquid_cash")) if buying_power is not None: lines.append(f" - {translator('buying_power')}: {_format_money(buying_power)}") reserved_cash = _safe_float(execution.get("reserved_cash")) @@ -533,6 +579,41 @@ def _format_dashboard_lines( return lines +def _format_dashboard_lines( + portfolio: Mapping[str, Any], + execution: Mapping[str, Any], + *, + translator: Callable[..., str], +) -> list[str]: + generated_lines = _format_generated_dashboard_lines(portfolio, execution, translator=translator) + dashboard_text = str(execution.get("dashboard_text") or "").strip() + if not dashboard_text: + return generated_lines + + has_signal_display = bool(str(execution.get("signal_display") or "").strip()) + extra_lines = [] + skipping_dashboard_holdings = False + for raw_line in dashboard_text.splitlines(): + if not raw_line.strip(): + continue + localized = _base_localize_notification_text(raw_line.rstrip(), translator=translator) + if has_signal_display and _is_dashboard_signal_line(localized): + continue + if _is_dashboard_holdings_title(localized, translator=translator): + skipping_dashboard_holdings = True + continue + if skipping_dashboard_holdings: + if raw_line.startswith((" ", "\t", "-")): + continue + skipping_dashboard_holdings = False + if _is_dashboard_account_title(localized, translator=translator): + continue + if _is_dashboard_account_metric_line(localized, translator=translator): + continue + extra_lines.append(localized) + return [*generated_lines, *extra_lines] + + def _localize_timing_contract(contract: Any, *, translator: Callable[..., str]) -> str: value = str(contract or "").strip() if value == "same_trading_day": diff --git a/tests/test_notifications_telegram.py b/tests/test_notifications_telegram.py new file mode 100644 index 0000000..43c1841 --- /dev/null +++ b/tests/test_notifications_telegram.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from notifications.telegram import render_cycle_summary + + +def test_render_cycle_summary_dashboard_text_does_not_hide_account_overview(): + message = render_cycle_summary( + { + "account": "****1234", + "strategy_profile": "soxl_soxx_trend_income", + "strategy_display_name": "SOXL/SOXX Semiconductor Trend Income", + "dry_run_only": True, + "portfolio": { + "total_equity": 2345.67, + "liquid_cash": 456.78, + "portfolio_rows": (("SOXL", "SOXX"), ("BOXX",)), + "market_values": {"SOXL": 1000.0, "SOXX": 500.0, "BOXX": 0.0}, + "quantities": {"SOXL": 5, "SOXX": 2, "BOXX": 0}, + }, + "allocation": {"targets": {"SOXL": 1000.0, "SOXX": 500.0, "BOXX": 0.0}}, + "execution": { + "reserved_cash": 50.0, + "investable_cash": 406.78, + "dashboard_text": "\n".join( + ( + "πŸ“Œ Strategy portfolio", + " - Total assets (strategy symbols + cash): $2,000.00", + " - Buying power: $100.00", + "πŸ’Ό Strategy holdings", + " - SOXL: $1,000.00 / 5 shares", + "🎯 Signal: signal_blend_gate_risk_on: soxl_ratio=70.0%, soxx_ratio=20.0%", + "Market Regime Control: watch | route none | score n/a", + ) + ), + "signal_display": "signal_blend_gate_risk_on: soxl_ratio=70.0%, soxx_ratio=20.0%, trend_symbol=SOXX, window=140", + }, + "submitted_orders": [], + "skipped_orders": [], + }, + lang="en", + ) + + assert " - Total assets: $2,345.67" in message + assert " - Buying power: $456.78" in message + assert " - Reserved cash: $50.00" in message + assert " - Investable cash: $406.78" in message + assert " - SOXL: $1,000.00 / 5 shares" in message + assert "Market Regime Control: watch | route none | score n/a" in message + assert "Total assets (strategy symbols + cash): $2,000.00" not in message + assert "Buying power: $100.00" not in message + assert "πŸ“Œ Strategy portfolio" not in message + assert message.count("🎯 Signal:") == 1