diff --git a/application/rebalance_service.py b/application/rebalance_service.py index 4a516d1..eae9674 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -5,6 +5,7 @@ import os import re import traceback +from collections.abc import Mapping from datetime import datetime _ZH_REASON_REPLACEMENTS = ( @@ -167,12 +168,47 @@ def _build_benchmark_lines(execution, *, translator): ] -def _format_holdings_lines(portfolio_rows, market_values, *, translator) -> list[str]: - lines = [translator("holdings_title")] - for row in portfolio_rows: - for symbol in row: - lines.append(f" - {symbol}: ${market_values[symbol]:,.2f}") - return lines +def _normalize_cash_by_currency(raw_cash) -> dict[str, float]: + if not isinstance(raw_cash, Mapping): + return {} + cash_by_currency: dict[str, float] = {} + for currency, amount in raw_cash.items(): + normalized_currency = str(currency or "").strip().upper() + if not normalized_currency: + continue + cash_by_currency[normalized_currency] = float(amount) + return cash_by_currency + + +def _format_cash_by_currency(cash_by_currency: Mapping[str, float]) -> str: + parts = [] + for currency in sorted(cash_by_currency, key=lambda value: (value != "USD", value)): + amount = float(cash_by_currency[currency]) + if amount == 0.0: + continue + parts.append(f"{currency} {amount:,.2f}") + return ", ".join(parts) + + +def _has_positive_non_usd_cash(cash_by_currency: Mapping[str, float]) -> bool: + return any( + currency != "USD" and float(amount) > 0.0 + for currency, amount in cash_by_currency.items() + ) + + +def _format_dashboard_text(text) -> str: + return "\n".join( + line.rstrip() + for line in str(text or "").splitlines() + if line.strip() + ) + + +def _append_dashboard_lines(lines, *, execution) -> None: + dashboard_text = _format_dashboard_text(execution.get("dashboard_text")) + if dashboard_text: + lines.extend(dashboard_text.splitlines()) def _append_status_lines(lines, *, execution, translator, signal_key): @@ -317,11 +353,10 @@ def fetch_replanned_state(): quantities = dict(portfolio["quantities"]) sellable_quantities = dict(portfolio["sellable_quantities"]) target_values = dict(allocation["targets"]) - total_strategy_equity = float(portfolio["total_equity"]) available_cash = float(portfolio["liquid_cash"]) + cash_by_currency = _normalize_cash_by_currency(portfolio.get("cash_by_currency")) investable_cash = float(execution["investable_cash"]) current_min_trade = float(execution["current_min_trade"]) - portfolio_rows = tuple(portfolio["portfolio_rows"]) def record_dry_run(symbol, side, quantity, price, *, order_type): price_text = f"${price:.2f}" if price is not None else translator("order_type_market") side_key = "side_buy" if str(side).lower() == "buy" else "side_sell" @@ -439,11 +474,25 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): quantities = dict(portfolio["quantities"]) sellable_quantities = dict(portfolio["sellable_quantities"]) target_values = dict(allocation["targets"]) - total_strategy_equity = float(portfolio["total_equity"]) available_cash = float(portfolio["liquid_cash"]) + cash_by_currency = _normalize_cash_by_currency(portfolio.get("cash_by_currency")) investable_cash = float(execution["investable_cash"]) current_min_trade = float(execution["current_min_trade"]) - portfolio_rows = tuple(portfolio["portfolio_rows"]) + + if ( + available_cash <= 0.0 + and investable_cash <= 0.0 + and _has_positive_non_usd_cash(cash_by_currency) + ): + record_note_log( + note_logs, + translator=translator, + with_prefix=with_prefix, + kind="buy_deferred_non_usd_cash", + available=f"{available_cash:.2f}", + investable=f"{investable_cash:.2f}", + currencies=_format_cash_by_currency(cash_by_currency), + ) buy_candidates = [ symbol for symbol in strategy_assets @@ -552,11 +601,6 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): ) if action_done: - cash_summary = translator( - "cash_summary", - available=f"{available_cash:.2f}", - investable=f"{investable_cash:.2f}", - ) formatted_logs = "\n".join(f" - {log}" for log in [*logs, *skip_logs, *note_logs]) tg_lines = [translator("rebalance_title")] _append_strategy_line( @@ -566,7 +610,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): ) if dry_run_only: tg_lines.append(translator("dry_run_banner")) - tg_lines.append(cash_summary) + _append_dashboard_lines(tg_lines, execution=execution) _append_status_lines( tg_lines, execution=execution, @@ -583,6 +627,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): ) if dry_run_only: compact_lines.append(translator("dry_run_banner")) + _append_dashboard_lines(compact_lines, execution=execution) _append_compact_status_lines( compact_lines, execution=execution, @@ -594,13 +639,6 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): print(with_prefix(detailed_tg_message), flush=True) send_tg_message(compact_tg_message) else: - equity_text = f"{total_strategy_equity:,.2f}" - cash_summary = translator( - "cash_summary", - available=f"{available_cash:.2f}", - investable=f"{investable_cash:.2f}", - ) - holdings_lines = _format_holdings_lines(portfolio_rows, market_values, translator=translator) no_trade_lines = [ translator("heartbeat_title"), ] @@ -609,17 +647,10 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): strategy_display_name=strategy_display_name, translator=translator, ) - no_trade_lines.append(translator("equity", value=equity_text)) if dry_run_only: no_trade_lines.append(translator("dry_run_banner")) - no_trade_lines.extend( - [ - cash_summary, - separator, - *holdings_lines, - separator, - ] - ) + _append_dashboard_lines(no_trade_lines, execution=execution) + no_trade_lines.append(separator) _append_status_lines( no_trade_lines, execution=execution, @@ -653,9 +684,9 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): strategy_display_name=strategy_display_name, translator=translator, ) - compact_no_trade_lines.append(translator("equity", value=equity_text)) if dry_run_only: compact_no_trade_lines.append(translator("dry_run_banner")) + _append_dashboard_lines(compact_no_trade_lines, execution=execution) _append_compact_status_lines( compact_no_trade_lines, execution=execution, diff --git a/decision_mapper.py b/decision_mapper.py index a6c47e7..d51798c 100644 --- a/decision_mapper.py +++ b/decision_mapper.py @@ -39,6 +39,39 @@ def _build_portfolio_inputs( raise ValueError("LongBridge plan mapping requires account_state or snapshot") +def _cash_by_currency_from_account_state( + account_state: Mapping[str, Any] | None, +) -> dict[str, float]: + if account_state is None: + return {} + raw_cash = account_state.get("cash_by_currency") + if not isinstance(raw_cash, Mapping): + return {} + cash_by_currency: dict[str, float] = {} + for currency, amount in raw_cash.items(): + normalized_currency = str(currency or "").strip().upper() + if not normalized_currency: + continue + cash_by_currency[normalized_currency] = float(amount) + return cash_by_currency + + +def _cash_by_currency_from_snapshot(snapshot: Any | None) -> dict[str, float]: + metadata = getattr(snapshot, "metadata", {}) or {} + if not isinstance(metadata, Mapping): + return {} + raw_cash = metadata.get("cash_by_currency") + if not isinstance(raw_cash, Mapping): + return {} + cash_by_currency: dict[str, float] = {} + for currency, amount in raw_cash.items(): + normalized_currency = str(currency or "").strip().upper() + if not normalized_currency: + continue + cash_by_currency[normalized_currency] = float(amount) + return cash_by_currency + + def _symbol_role(symbol: str) -> str | None: normalized = str(symbol or "").strip().upper() if normalized in _SAFE_HAVEN_SYMBOLS: @@ -59,6 +92,8 @@ def _build_weight_translation_annotations( liquid_cash: float, ) -> ValueTargetExecutionAnnotations: diagnostics = dict(decision.diagnostics) + raw_annotations = diagnostics.get("execution_annotations") + execution_annotations = dict(raw_annotations) if isinstance(raw_annotations, Mapping) else {} threshold_value = _default_threshold_value(total_equity) signal_display = str( diagnostics.get("signal_description") @@ -72,12 +107,18 @@ def _build_weight_translation_annotations( or diagnostics.get("canary_status") or "" ).strip() or None + dashboard_text = str( + execution_annotations.get("dashboard_text") + or diagnostics.get("dashboard") + or "" + ).strip() or None benchmark_symbol = str(diagnostics.get("benchmark_symbol") or "").strip().upper() or None return ValueTargetExecutionAnnotations( trade_threshold_value=threshold_value, reserved_cash=0.0, signal_display=signal_display, status_display=status_display, + dashboard_text=dashboard_text, benchmark_symbol=benchmark_symbol, benchmark_price=( float(diagnostics["benchmark_price"]) @@ -185,6 +226,7 @@ def _resolve_layout(strategy_profile: str) -> tuple[str, tuple[str, ...], tuple[ "reserved_cash", "signal_display", "status_display", + "dashboard_text", "benchmark_symbol", "benchmark_price", "long_trend_value", @@ -196,6 +238,7 @@ def _resolve_layout(strategy_profile: str) -> tuple[str, tuple[str, ...], tuple[ "reserved_cash": 0.0, "signal_display": "", "status_display": "", + "dashboard_text": "", "benchmark_symbol": "QQQ", "benchmark_price": 0.0, "long_trend_value": 0.0, @@ -283,7 +326,7 @@ def map_strategy_decision_to_plan( strategy_symbols_order, portfolio_rows_layout, execution_fields, execution_defaults = _resolve_layout( canonical_profile ) - return build_value_target_runtime_plan( + plan = build_value_target_runtime_plan( normalized_decision, strategy_profile=canonical_profile, portfolio_inputs=portfolio_inputs, @@ -294,3 +337,9 @@ def map_strategy_decision_to_plan( execution_fields=execution_fields, execution_defaults=execution_defaults, ) + cash_by_currency = _cash_by_currency_from_account_state(account_state) + if not cash_by_currency: + cash_by_currency = _cash_by_currency_from_snapshot(snapshot) + if cash_by_currency: + plan["portfolio"]["cash_by_currency"] = cash_by_currency + return plan diff --git a/notifications/telegram.py b/notifications/telegram.py index 6a43b9f..5eb71be 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -28,7 +28,6 @@ "equity": "💰 净值: ${value}", "cash_summary": "💵 资金\n - 账户现金: ${available}\n - 可投资现金: ${investable}", "cash_label": "现金", - "holdings_title": "💼 持仓", "order_logs_title": "🧾 执行明细", "benchmark_title": "📈 {symbol} 基准", "benchmark_price": "{symbol}: {value}", @@ -50,6 +49,7 @@ "sell_skipped": "⚪️ [卖出跳过] {detail}", "buy_deferred": "ℹ️ [买入说明] {detail}", "buy_deferred_no_investable_cash": "账户现金 ${available} 低于策略保留阈值,可投资现金为 ${investable},本轮不发起买单", + "buy_deferred_non_usd_cash": "检测到非 USD 现金({currencies}),但美股策略可用 USD 现金为 ${available}、可投资现金为 ${investable};请先换汇或入金 USD 后再买入", "buy_deferred_small_cash": "{symbol} 目标差额 ${diff},但可投资现金 ${investable} 不足买入 1 股(价格 ${price})", "buy_deferred_cash_limit": "{symbol} 目标差额 ${diff},预算可买 {budget_qty} 股,但券商估算可买数量为 0;可能有未完成挂单、结算或购买力占用", "limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}", @@ -101,7 +101,6 @@ "equity": "💰 Equity: ${value}", "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}", @@ -123,6 +122,7 @@ "sell_skipped": "⚪️ [Sell skipped] {detail}", "buy_deferred": "ℹ️ [Buy note] {detail}", "buy_deferred_no_investable_cash": "Account cash ${available} is below the strategy reserve threshold, investable cash is ${investable}; no buy order this cycle", + "buy_deferred_non_usd_cash": "Non-USD cash is present ({currencies}), but this US-equity strategy has USD cash ${available} and investable cash ${investable}; convert or deposit USD before buying", "buy_deferred_small_cash": "{symbol} target gap ${diff}, but investable cash ${investable} is not enough for 1 share at ${price}", "buy_deferred_cash_limit": "{symbol} target gap ${diff}, budget supports {budget_qty} shares, but broker estimate returned 0; an open order, settlement, or buying-power hold may still be blocking funds", "limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}", diff --git a/requirements.txt b/requirements.txt index f23fee1..210cff1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.18 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.28 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.19 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.30 pandas requests pytz diff --git a/tests/test_decision_mapper.py b/tests/test_decision_mapper.py index 6bc2de2..ff56706 100644 --- a/tests/test_decision_mapper.py +++ b/tests/test_decision_mapper.py @@ -36,6 +36,7 @@ def test_maps_semiconductor_strategy_decision_to_execution_plan(self): "quantities": {"SOXL": 0, "SOXX": 0, "BOXX": 50, "QQQI": 10, "SPYI": 10}, "sellable_quantities": {"SOXL": 0, "SOXX": 0, "BOXX": 50, "QQQI": 10, "SPYI": 10}, "total_strategy_equity": 50000.0, + "cash_by_currency": {"USD": 10000.0, "SGD": 350.0}, } plan = map_strategy_decision_to_plan( @@ -48,6 +49,7 @@ def test_maps_semiconductor_strategy_decision_to_execution_plan(self): self.assertEqual(plan["allocation"]["strategy_symbols"], ("SOXL", "SOXX", "BOXX", "QQQI", "SPYI")) self.assertEqual(plan["allocation"]["targets"]["BOXX"], 15000.0) self.assertEqual(plan["portfolio"]["portfolio_rows"], (("SOXL", "SOXX"), ("QQQI", "SPYI"), ("BOXX",))) + self.assertEqual(plan["portfolio"]["cash_by_currency"], {"USD": 10000.0, "SGD": 350.0}) self.assertEqual(plan["portfolio"]["sellable_quantities"]["BOXX"], 50) self.assertEqual(plan["execution"]["trade_threshold_value"], 500.0) self.assertEqual(plan["execution"]["investable_cash"], 9000.0) @@ -63,6 +65,7 @@ def test_prefers_normalized_execution_annotations_when_present(self): "trade_threshold_value": 250.0, "signal_display": "signal", "status_display": "risk-on", + "dashboard_text": "strategy dashboard", "deploy_ratio_text": "60.0%", "income_ratio_text": "10.0%", "income_locked_ratio_text": "10.0%", @@ -89,6 +92,7 @@ def test_prefers_normalized_execution_annotations_when_present(self): self.assertEqual(plan["execution"]["trade_threshold_value"], 250.0) self.assertEqual(plan["execution"]["status_display"], "risk-on") self.assertEqual(plan["execution"]["signal_display"], "signal") + self.assertEqual(plan["execution"]["dashboard_text"], "strategy dashboard") self.assertEqual(plan["execution"]["investable_cash"], 9000.0) def test_maps_hybrid_decision_from_snapshot_source(self): @@ -147,6 +151,7 @@ def test_translates_weight_decision_for_tech_strategy(self): diagnostics={ "signal_description": "risk on", "status_description": "regime=soft_defense | breadth=55.0%", + "dashboard": "tech dashboard", "benchmark_symbol": "QQQ", }, ) @@ -172,9 +177,39 @@ def test_translates_weight_decision_for_tech_strategy(self): self.assertEqual(plan["allocation"]["targets"]["BOXX"], 6000.0) self.assertEqual(plan["execution"]["signal_display"], "risk on") self.assertEqual(plan["execution"]["status_display"], "regime=soft_defense | breadth=55.0%") + self.assertEqual(plan["execution"]["dashboard_text"], "tech dashboard") self.assertEqual(plan["execution"]["benchmark_symbol"], "QQQ") self.assertEqual(plan["portfolio"]["portfolio_rows"], (("AAPL", "MSFT", "BOXX"),)) + def test_keeps_cash_by_currency_from_snapshot_metadata(self): + decision = StrategyDecision( + positions=(PositionTarget(symbol="SOXL", target_value=0.0),), + diagnostics={ + "execution_annotations": { + "trade_threshold_value": 100.0, + "investable_cash": 0.0, + } + }, + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=0.0, + buying_power=0.0, + positions=(), + metadata={ + "account_hash": "longbridge-sg", + "cash_by_currency": {"USD": 0.0, "SGD": 350.0}, + }, + ) + + plan = map_strategy_decision_to_plan( + decision, + snapshot=snapshot, + strategy_profile="soxl_soxx_trend_income", + ) + + self.assertEqual(plan["portfolio"]["cash_by_currency"], {"USD": 0.0, "SGD": 350.0}) + def test_translates_weight_decision_for_global_etf_rotation(self): decision = StrategyDecision( positions=( diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 83694df..298f95a 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -34,7 +34,6 @@ def test_build_translator_supports_chinese(self): 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)") diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 75b4f87..e85a108 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -47,12 +47,37 @@ def _build_plan( available_cash, total_strategy_equity, portfolio_rows, - dashboard_text="", + dashboard_text=None, benchmark_symbol="", benchmark_price=0.0, long_trend_value=0.0, exit_line=0.0, + cash_by_currency=None, ): + if dashboard_text is None: + dashboard_lines = [ + "📌 策略账户概览", + f" - 总资产(策略标的+现金): ${float(total_strategy_equity):,.2f}", + f" - 购买力: ${float(available_cash):.2f} | 可投资现金: ${float(investable_cash):.2f}", + ] + nonzero_cash = { + currency: amount + for currency, amount in dict(cash_by_currency or {}).items() + if float(amount) != 0.0 + } + if nonzero_cash and (len(nonzero_cash) > 1 or "USD" not in nonzero_cash): + formatted_cash = ", ".join( + f"{currency} {float(nonzero_cash[currency]):,.2f}" + for currency in sorted(nonzero_cash, key=lambda value: (value != "USD", value)) + ) + dashboard_lines.append(f" - 各币种现金: {formatted_cash}") + dashboard_lines.append("💼 策略持仓") + for row in portfolio_rows: + for symbol in row: + dashboard_lines.append( + f" - {symbol}: ${float(market_values[symbol]):,.2f} / {quantities.get(symbol, 0)}股" + ) + dashboard_text = "\n".join(dashboard_lines) return { "strategy_profile": strategy_profile, "allocation": { @@ -71,6 +96,7 @@ def _build_plan( "sellable_quantities": dict(sellable_quantities), "total_equity": float(total_strategy_equity), "liquid_cash": float(available_cash), + "cash_by_currency": dict(cash_by_currency or {}), }, "execution": { "trade_threshold_value": float(trade_threshold_value), @@ -321,7 +347,7 @@ def test_buy_skip_without_orders_is_sent_in_single_heartbeat_message(self): self.assertIn("可投资现金", sent_messages[0]) self.assertIn("SOXX.US", sent_messages[0]) - def test_zero_investable_cash_is_silently_skipped(self): + def test_zero_investable_cash_reports_buying_power_without_trade_note(self): plan = _build_plan( strategy_symbols=("BOXX",), safe_haven_symbols=("BOXX",), @@ -349,7 +375,8 @@ def test_zero_investable_cash_is_silently_skipped(self): self.assertEqual(len(sent_messages), 1) self.assertNotIn("账户现金", sent_messages[0]) - self.assertNotIn("可投资现金: $0.00", sent_messages[0]) + self.assertIn("购买力: $3065.61 | 可投资现金: $0.00", sent_messages[0]) + self.assertIn("BOXX: $24,880.00 / 214股", sent_messages[0]) self.assertIn("✅ 无需调仓", sent_messages[0]) self.assertNotIn("本轮没有可执行订单", sent_messages[0]) self.assertNotIn("说明", sent_messages[0]) @@ -386,6 +413,42 @@ def test_cash_limit_zero_mentions_possible_order_hold(self): self.assertIn("券商估算可买数量为 0", sent_messages[0]) self.assertIn("可能有未完成挂单", sent_messages[0]) + def test_non_usd_cash_is_reported_when_usd_cash_is_zero(self): + plan = _build_plan( + strategy_symbols=("SOXL", "SOXX", "BOXX", "QQQI", "SPYI"), + risk_symbols=("SOXL", "SOXX"), + income_symbols=("QQQI", "SPYI"), + safe_haven_symbols=("BOXX",), + targets={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 0.0, "QQQI": 0.0, "SPYI": 0.0}, + market_values={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 0.0, "QQQI": 0.0, "SPYI": 0.0}, + sellable_quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0, "QQQI": 0, "SPYI": 0}, + quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0, "QQQI": 0, "SPYI": 0}, + current_min_trade=100.0, + trade_threshold_value=100.0, + investable_cash=0.0, + market_status="🚀 RISK-ON (SOXX+SOXL)", + deploy_ratio_text="90.0%", + income_ratio_text="0.0%", + income_locked_ratio_text="0.0%", + signal_message="SOXX 站上 140 日门槛线,持有 SOXL 70.0% + SOXX 20.0%", + available_cash=0.0, + total_strategy_equity=0.0, + portfolio_rows=(("SOXL", "SOXX"), ("QQQI", "SPYI"), ("BOXX",)), + cash_by_currency={"USD": 0.0, "SGD": 350.0}, + ) + + sent_messages, _, _ = self._run_strategy( + plan, + prices={}, + dry_run_only=True, + ) + + self.assertEqual(len(sent_messages), 1) + self.assertIn("各币种现金: SGD 350.00", sent_messages[0]) + self.assertIn("检测到非 USD 现金", sent_messages[0]) + self.assertIn("本轮没有可执行订单", sent_messages[0]) + self.assertNotIn("✅ 无需调仓", sent_messages[0]) + def test_refreshes_account_state_after_sell_and_can_place_followup_buy(self): initial_plan = _build_plan( strategy_symbols=("SOXL", "SOXX"), @@ -618,8 +681,10 @@ def test_heartbeat_accepts_normalized_portfolio_and_execution_sections(self): self.assertIn("💓 【心跳检测】", sent_messages[0]) self.assertIn("可投资现金", sent_messages[0]) self.assertNotIn("💵 资金\n - 账户现金:", sent_messages[0]) - self.assertNotIn("💼 持仓", sent_messages[0]) - self.assertNotIn(" - SOXX:", sent_messages[0]) + self.assertIn("📌 策略账户概览", sent_messages[0]) + self.assertIn("总资产(策略标的+现金): $60,000.00", sent_messages[0]) + self.assertIn("购买力: $101.95 | 可投资现金: $101.95", sent_messages[0]) + self.assertIn("SOXX: $0.00 / 0股", sent_messages[0]) def test_hybrid_heartbeat_hides_empty_semiconductor_fields_and_shows_benchmark_line(self): plan = _build_plan( @@ -660,10 +725,11 @@ def test_hybrid_heartbeat_hides_empty_semiconductor_fields_and_shows_benchmark_l self.assertIn("💓 【心跳检测】", sent_messages[0]) self.assertIn("🧭 策略: TQQQ 增长收益", sent_messages[0]) self.assertIn("🧪 模拟运行模式", sent_messages[0]) - self.assertNotIn("💼 持仓", sent_messages[0]) - self.assertNotIn(" - TQQQ: $0.00", sent_messages[0]) - self.assertNotIn(" - BOXX: $0.00", sent_messages[0]) - self.assertNotIn(" - QQQI: $0.00", sent_messages[0]) + self.assertIn("📌 策略账户概览", sent_messages[0]) + self.assertIn("TQQQ: $0.00 / 0股", sent_messages[0]) + self.assertIn("BOXX: $0.00 / 0股", sent_messages[0]) + self.assertIn("QQQI: $0.00 / 0股", sent_messages[0]) + self.assertIn("SPYI: $0.00 / 0股", sent_messages[0]) self.assertNotIn("📈 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])