Skip to content

Commit c683905

Browse files
authored
Fix Firstrade notification account overview (#73)
1 parent 62a050b commit c683905

2 files changed

Lines changed: 147 additions & 14 deletions

File tree

notifications/telegram.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def format_small_account_cash_substitution_notes(
108108

109109
_DETAIL_FIELD_SPLIT_RE = re.compile(r",\s*(?=[A-Za-z_][\w-]*\s*=)")
110110
_STRUCTURED_PAREN_RE = re.compile(r"^(?P<key>[A-Za-z_][\w-]*)\((?P<details>.*)\)$")
111+
_DASHBOARD_POSITION_LINE_RE = re.compile(r"^[A-Z][A-Z0-9./-]{0,12}\s*:")
111112

112113

113114
I18N = {
@@ -469,29 +470,74 @@ def _is_dashboard_signal_line(line: str) -> bool:
469470
)
470471

471472

472-
def _format_dashboard_lines(
473+
def _is_dashboard_account_title(line: str, *, translator: Callable[..., str]) -> bool:
474+
text = str(line or "").strip()
475+
account_titles = {
476+
translator("account_overview_title"),
477+
"📌 Strategy Account",
478+
"📌 Strategy portfolio",
479+
"📌 策略账户概览",
480+
}
481+
return text in account_titles
482+
483+
484+
def _is_dashboard_holdings_title(line: str, *, translator: Callable[..., str]) -> bool:
485+
text = str(line or "").strip()
486+
holdings_titles = {
487+
translator("holdings_title"),
488+
"💼 Strategy Holdings",
489+
"💼 Strategy holdings",
490+
"💼 策略持仓",
491+
}
492+
return text in holdings_titles
493+
494+
495+
def _is_dashboard_account_metric_line(line: str, *, translator: Callable[..., str]) -> bool:
496+
text = str(line or "").strip()
497+
lowered = text.lower()
498+
if not text:
499+
return False
500+
if text.startswith((translator("dashboard_label"), "📊 Dashboard", "📊 资产看板")):
501+
return True
502+
if text.startswith(("Income:", "收益:", "收入:")):
503+
return True
504+
if _DASHBOARD_POSITION_LINE_RE.match(text) and (
505+
"$" in text or "股" in text or "share" in lowered
506+
):
507+
return True
508+
metric_labels = {
509+
translator("total_assets"),
510+
translator("buying_power"),
511+
translator("reserved_cash"),
512+
translator("investable_cash"),
513+
translator("equity"),
514+
"Total assets (strategy symbols + cash)",
515+
"Buying power",
516+
"Reserved cash",
517+
"Investable cash",
518+
"Equity",
519+
"总资产(策略标的+现金)",
520+
"购买力",
521+
"预留现金",
522+
"可投资现金",
523+
"净值",
524+
}
525+
return any(label and label.lower() in lowered for label in metric_labels)
526+
527+
528+
def _format_generated_dashboard_lines(
473529
portfolio: Mapping[str, Any],
474530
execution: Mapping[str, Any],
475531
*,
476532
translator: Callable[..., str],
477533
) -> list[str]:
478-
dashboard_text = str(execution.get("dashboard_text") or "").strip()
479-
if dashboard_text:
480-
has_signal_display = bool(str(execution.get("signal_display") or "").strip())
481-
lines = []
482-
for line in dashboard_text.splitlines():
483-
if not line.strip():
484-
continue
485-
if has_signal_display and _is_dashboard_signal_line(line):
486-
continue
487-
lines.append(_base_localize_notification_text(line.rstrip(), translator=translator))
488-
return lines
489-
490534
lines = [translator("account_overview_title")]
491535
total_equity = _safe_float(portfolio.get("total_equity"))
492536
if total_equity is not None:
493537
lines.append(f" - {translator('total_assets')}: {_format_money(total_equity)}")
494-
buying_power = _safe_float(portfolio.get("liquid_cash"))
538+
buying_power = _safe_float(portfolio.get("buying_power"))
539+
if buying_power is None:
540+
buying_power = _safe_float(portfolio.get("liquid_cash"))
495541
if buying_power is not None:
496542
lines.append(f" - {translator('buying_power')}: {_format_money(buying_power)}")
497543
reserved_cash = _safe_float(execution.get("reserved_cash"))
@@ -533,6 +579,41 @@ def _format_dashboard_lines(
533579
return lines
534580

535581

582+
def _format_dashboard_lines(
583+
portfolio: Mapping[str, Any],
584+
execution: Mapping[str, Any],
585+
*,
586+
translator: Callable[..., str],
587+
) -> list[str]:
588+
generated_lines = _format_generated_dashboard_lines(portfolio, execution, translator=translator)
589+
dashboard_text = str(execution.get("dashboard_text") or "").strip()
590+
if not dashboard_text:
591+
return generated_lines
592+
593+
has_signal_display = bool(str(execution.get("signal_display") or "").strip())
594+
extra_lines = []
595+
skipping_dashboard_holdings = False
596+
for raw_line in dashboard_text.splitlines():
597+
if not raw_line.strip():
598+
continue
599+
localized = _base_localize_notification_text(raw_line.rstrip(), translator=translator)
600+
if has_signal_display and _is_dashboard_signal_line(localized):
601+
continue
602+
if _is_dashboard_holdings_title(localized, translator=translator):
603+
skipping_dashboard_holdings = True
604+
continue
605+
if skipping_dashboard_holdings:
606+
if raw_line.startswith((" ", "\t", "-")):
607+
continue
608+
skipping_dashboard_holdings = False
609+
if _is_dashboard_account_title(localized, translator=translator):
610+
continue
611+
if _is_dashboard_account_metric_line(localized, translator=translator):
612+
continue
613+
extra_lines.append(localized)
614+
return [*generated_lines, *extra_lines]
615+
616+
536617
def _localize_timing_contract(contract: Any, *, translator: Callable[..., str]) -> str:
537618
value = str(contract or "").strip()
538619
if value == "same_trading_day":
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
from notifications.telegram import render_cycle_summary
4+
5+
6+
def test_render_cycle_summary_dashboard_text_does_not_hide_account_overview():
7+
message = render_cycle_summary(
8+
{
9+
"account": "****1234",
10+
"strategy_profile": "soxl_soxx_trend_income",
11+
"strategy_display_name": "SOXL/SOXX Semiconductor Trend Income",
12+
"dry_run_only": True,
13+
"portfolio": {
14+
"total_equity": 2345.67,
15+
"liquid_cash": 456.78,
16+
"portfolio_rows": (("SOXL", "SOXX"), ("BOXX",)),
17+
"market_values": {"SOXL": 1000.0, "SOXX": 500.0, "BOXX": 0.0},
18+
"quantities": {"SOXL": 5, "SOXX": 2, "BOXX": 0},
19+
},
20+
"allocation": {"targets": {"SOXL": 1000.0, "SOXX": 500.0, "BOXX": 0.0}},
21+
"execution": {
22+
"reserved_cash": 50.0,
23+
"investable_cash": 406.78,
24+
"dashboard_text": "\n".join(
25+
(
26+
"📌 Strategy portfolio",
27+
" - Total assets (strategy symbols + cash): $2,000.00",
28+
" - Buying power: $100.00",
29+
"💼 Strategy holdings",
30+
" - SOXL: $1,000.00 / 5 shares",
31+
"🎯 Signal: signal_blend_gate_risk_on: soxl_ratio=70.0%, soxx_ratio=20.0%",
32+
"Market Regime Control: watch | route none | score n/a",
33+
)
34+
),
35+
"signal_display": "signal_blend_gate_risk_on: soxl_ratio=70.0%, soxx_ratio=20.0%, trend_symbol=SOXX, window=140",
36+
},
37+
"submitted_orders": [],
38+
"skipped_orders": [],
39+
},
40+
lang="en",
41+
)
42+
43+
assert " - Total assets: $2,345.67" in message
44+
assert " - Buying power: $456.78" in message
45+
assert " - Reserved cash: $50.00" in message
46+
assert " - Investable cash: $406.78" in message
47+
assert " - SOXL: $1,000.00 / 5 shares" in message
48+
assert "Market Regime Control: watch | route none | score n/a" in message
49+
assert "Total assets (strategy symbols + cash): $2,000.00" not in message
50+
assert "Buying power: $100.00" not in message
51+
assert "📌 Strategy portfolio" not in message
52+
assert message.count("🎯 Signal:") == 1

0 commit comments

Comments
 (0)