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
109 changes: 95 additions & 14 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<key>[A-Za-z_][\w-]*)\((?P<details>.*)\)$")
_DASHBOARD_POSITION_LINE_RE = re.compile(r"^[A-Z][A-Z0-9./-]{0,12}\s*:")


I18N = {
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match metric labels only at field prefixes

When dashboard_text contains a non-duplicated supplemental line that merely mentions equity/portfolio_equity (for example a small-account warning) and signal_display is empty or does not carry that same detail, this broad substring check drops the line even though the generated account overview does not recreate it. The previous behavior preserved those dashboard supplements, so the duplicate filter should only match actual metric rows such as labels at the start of the line.

Useful? React with 👍 / 👎.



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"))
Expand Down Expand Up @@ -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":
Expand Down
52 changes: 52 additions & 0 deletions tests/test_notifications_telegram.py
Original file line number Diff line number Diff line change
@@ -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