diff --git a/app/config/system_config.py b/app/config/system_config.py index b8bc304..6d34486 100644 --- a/app/config/system_config.py +++ b/app/config/system_config.py @@ -108,6 +108,8 @@ def default_paper_trading_config(): "trailing_activate_pnl_pct": _env_float("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", 3.0), "trailing_min_lock_profit_pct": _env_float("ALPHAX_PAPER_TRAILING_MIN_LOCK_PROFIT_PCT", 0.5), "trailing_distance_pct": _env_float("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", 1.5), + "trailing_move_push_min_interval_seconds": _env_int("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_INTERVAL_SECONDS", 300), + "trailing_move_push_min_step_pct": _env_float("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_STEP_PCT", 2.0), "trailing_tiers": [ {"min_pnl_pct": 8.0, "distance_pct": 1.0, "label": "紧贴"}, {"min_pnl_pct": 5.0, "distance_pct": 1.2, "label": "锁利"}, diff --git a/app/db/paper_trading.py b/app/db/paper_trading.py index 82385e5..be78b1f 100644 --- a/app/db/paper_trading.py +++ b/app/db/paper_trading.py @@ -71,6 +71,8 @@ def _trailing_config() -> dict: "activate_pnl_pct": max(0.0, _safe_float(cfg.get("trailing_activate_pnl_pct"), 3.0)), "min_lock_profit_pct": max(0.0, _safe_float(cfg.get("trailing_min_lock_profit_pct"), 0.5)), "distance_pct": max(0.1, _safe_float(cfg.get("trailing_distance_pct"), 1.5)), + "move_push_min_interval_seconds": max(0, _safe_int(cfg.get("trailing_move_push_min_interval_seconds"), 300)), + "move_push_min_step_pct": max(0.0, _safe_float(cfg.get("trailing_move_push_min_step_pct"), 2.0)), "tiers": cfg.get("trailing_tiers") if isinstance(cfg.get("trailing_tiers"), list) else [], } @@ -87,6 +89,43 @@ def _trailing_distance_pct(pnl_pct: float, cfg: dict) -> tuple[float, str]: return distance, label +def _parse_time(value: str) -> datetime | None: + if not value: + return None + try: + return datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except Exception: + return None + + +def _should_emit_trailing_move(conn, trade: dict, new_trail: float, event_time: str, cfg: dict) -> bool: + last = conn.execute( + """ + SELECT event_time, price + FROM paper_trade_events + WHERE trade_id=%s AND event_type IN ('trailing_activate','trailing_move') + ORDER BY event_time DESC, id DESC + LIMIT 1 + """, + (trade["id"],), + ).fetchone() + if not last: + return True + + min_interval = _safe_int(cfg.get("move_push_min_interval_seconds"), 300) + if min_interval > 0: + current_ts = _parse_time(event_time or _now()) + last_ts = _parse_time(last["event_time"]) + if current_ts and last_ts and (current_ts - last_ts).total_seconds() >= min_interval: + return True + + last_trail = _safe_float(last["price"]) + min_step_pct = _safe_float(cfg.get("move_push_min_step_pct"), 2.0) + if min_step_pct <= 0 or last_trail <= 0: + return True + return (new_trail / last_trail - 1) * 100 >= min_step_pct + + def _loads_json(value, fallback=None): try: if isinstance(value, str) and value.strip(): @@ -212,6 +251,10 @@ def _card_note(content: str) -> dict: return {"tag": "note", "elements": [{"tag": "plain_text", "content": content}]} +def _card_md(content: str) -> dict: + return {"tag": "div", "text": {"tag": "lark_md", "content": content}} + + def _push_paper_card(event_type: str, symbol: str, title: str, template: str, fields: list[tuple[str, str]], note: str = "", event_time: str = "") -> None: try: elements = [] @@ -237,6 +280,13 @@ def _push_paper_card(event_type: str, symbol: str, title: str, template: str, fi pass +def _push_custom_paper_card(card: dict) -> tuple[bool, object]: + try: + return push_card(card) + except Exception as exc: + return False, str(exc) + + def _push_event_card(event_type: str, trade: dict, result: dict, event_time: str = "") -> None: symbol = str(trade.get("symbol") or "") short_symbol = symbol.replace("/USDT", "") @@ -244,14 +294,14 @@ def _push_event_card(event_type: str, trade: dict, result: dict, event_time: str _push_paper_card( event_type, symbol, - f"模拟交易开仓 - {short_symbol}", + f"交易开仓 - {short_symbol}", "blue", [ ("成交价", _fmt_price(result.get("entry_price"))), ("名义仓位", f"{_safe_float(result.get('notional_usdt')):.2f} USDT"), ("杠杆/保证金", f"{_safe_float(result.get('leverage')):.1f}x / {_safe_float(result.get('margin_usdt')):.2f} USDT"), ], - "仅用于策略收益验证,不代表真实成交。", + "策略信号已进入持仓跟踪。", event_time, ) return @@ -259,7 +309,7 @@ def _push_event_card(event_type: str, trade: dict, result: dict, event_time: str _push_paper_card( event_type, symbol, - f"模拟交易平仓 - {short_symbol}", + f"交易平仓 - {short_symbol}", "red" if _safe_float(result.get("pnl_usdt")) < 0 else "green", [ ("退出价", _fmt_price(result.get("exit_price"))), @@ -267,7 +317,7 @@ def _push_event_card(event_type: str, trade: dict, result: dict, event_time: str ("收益额", f"{_safe_float(result.get('pnl_usdt')):.2f} USDT"), ("原因", result.get("exit_reason") or "--"), ], - "收益只来自 paper_trades 模拟账本。", + "收益以交易账本记录为准。", event_time, ) return @@ -275,14 +325,14 @@ def _push_event_card(event_type: str, trade: dict, result: dict, event_time: str _push_paper_card( event_type, symbol, - f"模拟交易移动止盈{'启动' if event_type == 'trailing_activate' else '上移'} - {short_symbol}", + f"移动止盈{'启动' if event_type == 'trailing_activate' else '上移'} - {short_symbol}", "yellow", [ ("保护价", _fmt_price(result.get("trailing_stop"))), ("当前收益", _fmt_pct(result.get("pnl_pct"))), ("动作", "启动保护" if event_type == "trailing_activate" else "上移保护价"), ], - "移动止盈用于锁定浮盈,仍以模拟账本为准。", + "移动止盈用于锁定浮盈。", event_time, ) @@ -295,7 +345,7 @@ def _push_order_created_card(order: dict, event_time: str = "") -> None: _push_paper_card( "paper_order_create", symbol, - f"模拟挂单创建 - {symbol.replace('/USDT', '')}", + f"挂单创建 - {symbol.replace('/USDT', '')}", "wathet", [ ("目标价", _fmt_price(target)), @@ -303,7 +353,7 @@ def _push_order_created_card(order: dict, event_time: str = "") -> None: ("距目标", _fmt_pct(distance)), ("有效期", order.get("expires_at") or "--"), ], - "等回踩机会已进入模拟挂单,触价后才会进入模拟持仓。", + "等回踩机会已进入挂单,触价后进入持仓。", event_time, ) @@ -313,7 +363,7 @@ def _push_order_filled_card(order: dict, result: dict, event_time: str = "") -> _push_paper_card( "paper_order_fill", symbol, - f"模拟挂单成交并开仓 - {symbol.replace('/USDT', '')}", + f"挂单成交并开仓 - {symbol.replace('/USDT', '')}", "green", [ ("挂单价", _fmt_price(order.get("target_price"))), @@ -321,7 +371,7 @@ def _push_order_filled_card(order: dict, result: dict, event_time: str = "") -> ("名义仓位", f"{_safe_float(result.get('notional_usdt')):.2f} USDT"), ("来源", order.get("source_status") or "wait_pullback"), ], - "价格触达理想入场位,模拟挂单已转为 paper trade。", + "价格触达理想入场位,挂单已转为持仓。", event_time, ) @@ -386,7 +436,7 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config: "open", entry_price, 0.0, - "模拟交易开仓:仅用于策略收益验证,不代表真实成交", + "交易开仓:策略信号已进入持仓跟踪", { "notional_usdt": notional, "margin_usdt": margin, @@ -712,7 +762,7 @@ def _close_trade(conn, trade: dict, current_price: float, reason: str, event_tim "close", exit_price, pnl_pct, - f"模拟交易平仓:{reason}", + f"交易平仓:{reason}", {"realized_pnl_usdt": pnl_usdt, "fee_usdt": total_fee}, now, ) @@ -746,28 +796,31 @@ def _update_trailing_stop(conn, trade: dict, current_price: float, pnl_pct: floa event_type = "trailing_activate" if activated else "trailing_move" action_text = "激活" if activated else "上移" - message = f"模拟交易移动止盈{action_text}:保护价 {new_trail:.8g}" - _record_event( - conn, - trade["id"], - trade["recommendation_id"], - trade["symbol"], - event_type, - new_trail, - pnl_pct, - message, - { - "current_price": current_price, - "previous_trailing_stop": current_trail, - "trailing_stop": new_trail, - "activate_pnl_pct": cfg.get("activate_pnl_pct"), - "distance_pct": distance_pct, - "tier_label": tier_label, - "min_lock_profit_pct": cfg.get("min_lock_profit_pct"), - }, - event_time, - ) - _push_event_card(event_type, trade, {"trailing_stop": new_trail}, event_time) + should_emit = activated or _should_emit_trailing_move(conn, trade, new_trail, event_time, cfg) + if should_emit: + message = f"移动止盈{action_text}:保护价 {new_trail:.8g}" + _record_event( + conn, + trade["id"], + trade["recommendation_id"], + trade["symbol"], + event_type, + new_trail, + pnl_pct, + message, + { + "current_price": current_price, + "previous_trailing_stop": current_trail, + "trailing_stop": new_trail, + "activate_pnl_pct": cfg.get("activate_pnl_pct"), + "distance_pct": distance_pct, + "tier_label": tier_label, + "min_lock_profit_pct": cfg.get("min_lock_profit_pct"), + "notification_throttled": False, + }, + event_time, + ) + _push_event_card(event_type, trade, {"trailing_stop": new_trail, "pnl_pct": pnl_pct}, event_time) return new_trail, { "activated": activated, "moved": moved, @@ -775,6 +828,7 @@ def _update_trailing_stop(conn, trade: dict, current_price: float, pnl_pct: floa "previous_trailing_stop": current_trail, "distance_pct": distance_pct, "tier_label": tier_label, + "notification_emitted": should_emit, } @@ -1066,3 +1120,88 @@ def list_paper_trade_events(limit: int = 80, offset: int = 0, symbol: str = "", "offset": offset, "has_more": offset + len(items) < int(total or 0), } + + +def _report_trade_line(item: dict, closed: bool = False) -> str: + symbol = item.get("symbol") or "--" + side = "空" if str(item.get("side") or "long").lower() == "short" else "多" + pnl = _safe_float(item.get("realized_pnl_pct") if closed else item.get("pnl_pct")) + pnl_usdt = _safe_float(item.get("realized_pnl_usdt") if closed else item.get("unrealized_pnl_usdt")) + status = item.get("exit_reason") if closed else "持仓中" + return f"- **{symbol}** · {side} · {_fmt_pct(pnl)} · {pnl_usdt:+.2f} USDT · {status or '--'}" + + +def _report_order_line(item: dict) -> str: + symbol = item.get("symbol") or "--" + side = "空" if str(item.get("side") or "long").lower() == "short" else "多" + distance = _safe_float(item.get("distance_to_target_pct")) + return f"- **{symbol}** · {side} · 目标 {_fmt_price(item.get('target_price'))} · 距目标 {_fmt_pct(distance)}" + + +def send_paper_trading_report(days: int = 30) -> dict: + days = max(1, min(_safe_int(days, 30), 365)) + summary = get_paper_trading_summary(days=days) + open_trades = list_paper_trades(limit=5, status="open").get("items", []) + closed_trades = list_paper_trades(limit=5, status="closed").get("items", []) + pending_orders = list_paper_orders(limit=5, status="pending").get("items", []) + + total_pnl = _safe_float(summary.get("total_pnl_usdt")) + realized = _safe_float(summary.get("realized_pnl_usdt")) + unrealized = _safe_float(summary.get("open_unrealized_pnl_usdt")) + initial_equity = _safe_float(summary.get("initial_equity_usdt")) + current_balance = _safe_float(summary.get("current_balance_usdt")) + return_pct = _safe_float(summary.get("account_total_return_pct")) + win_rate = _safe_float(summary.get("win_rate")) + template = "green" if total_pnl >= 0 else "red" + elements = [ + { + "tag": "column_set", + "flex_mode": "none", + "background_style": "default", + "columns": [ + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("初始资金", f"{initial_equity:.2f} USDT")]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("当前资金", f"{current_balance:.2f} USDT")]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("账户收益率", _fmt_pct(return_pct))]}, + ], + }, + { + "tag": "column_set", + "flex_mode": "none", + "background_style": "default", + "columns": [ + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("总收益", f"{total_pnl:+.2f} USDT")]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("成功率", f"{_fmt_pct(win_rate)} ({summary.get('win_count', 0)}/{summary.get('closed_count', 0)})")]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("交易数量", f"持仓 {summary.get('open_count', 0)} / 平仓 {summary.get('closed_count', 0)} / 挂单 {summary.get('pending_order_count', 0)}")]}, + ], + }, + _card_md( + "**战绩概览**\n" + f"- 已实现: {realized:+.2f} USDT\n" + f"- 浮动收益: {unrealized:+.2f} USDT\n" + f"- 平均平仓收益率: {_fmt_pct(summary.get('avg_realized_pnl_pct'))}\n" + f"- 持仓名义价值: {_safe_float(summary.get('open_position_value_usdt')):.2f} USDT\n" + f"- 已占用保证金: {_safe_float(summary.get('allocated_margin_usdt')):.2f} USDT\n" + f"- 可用资金: {_safe_float(summary.get('available_equity_usdt')):.2f} USDT\n" + f"- 累计杠杆: {_safe_float(summary.get('cumulative_leverage')):.2f}x" + ), + _card_md("**当前持仓**\n" + ("\n".join(_report_trade_line(x) for x in open_trades) if open_trades else "- 暂无持仓")), + _card_md("**等待成交**\n" + ("\n".join(_report_order_line(x) for x in pending_orders) if pending_orders else "- 暂无挂单")), + _card_md("**最近平仓**\n" + ("\n".join(_report_trade_line(x, closed=True) for x in closed_trades) if closed_trades else "- 暂无平仓记录")), + _card_note(f"报告周期: 最近 {days} 天 · 发送时间: {_now()}"), + ] + ok, push_result = _push_custom_paper_card({ + "metadata": {"source": "paper_trading", "event_type": "trade_report", "symbol": "ALL"}, + "config": {"wide_screen_mode": True}, + "header": { + "template": template, + "title": {"tag": "plain_text", "content": f"交易报告 - 最近 {days} 天"}, + }, + "elements": elements, + }) + return { + "ok": bool(ok), + "days": days, + "summary": summary, + "sent_at": _now(), + "push_result": push_result, + } diff --git a/app/web/routes_paper_trading.py b/app/web/routes_paper_trading.py index 72b9860..c4dbdc3 100644 --- a/app/web/routes_paper_trading.py +++ b/app/web/routes_paper_trading.py @@ -1,6 +1,12 @@ from fastapi import APIRouter, Cookie -from app.db.paper_trading import get_paper_trading_summary, list_paper_orders, list_paper_trade_events, list_paper_trades +from app.db.paper_trading import ( + get_paper_trading_summary, + list_paper_orders, + list_paper_trade_events, + list_paper_trades, + send_paper_trading_report, +) from app.web.shared import require_admin @@ -45,3 +51,9 @@ async def api_paper_trading_events( ): require_admin(altcoin_session) return list_paper_trade_events(limit=limit, offset=offset, symbol=symbol, event_type=event_type) + + +@router.post("/api/paper-trading/report") +async def api_paper_trading_report(days: int = 30, altcoin_session: str = Cookie(default="")): + require_admin(altcoin_session) + return send_paper_trading_report(days=days) diff --git a/static/paper_trading.html b/static/paper_trading.html index 5d834cd..9bb16cd 100644 --- a/static/paper_trading.html +++ b/static/paper_trading.html @@ -13,10 +13,12 @@

这里展示的是 paper trading 账本:只有系统把可买信号模拟成交后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。

+
模拟交易只统计已经进入 paper trading 的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。
+
状态加载中
@@ -99,7 +101,9 @@ function sideText(v){return String(v||'long').toLowerCase()==='short'?'空':'多 function sideBadge(v){var s=String(v||'long').toLowerCase();return ''+sideText(s)+''} function setTradeTab(tab){['open','orders','completed','events'].forEach(function(k){var on=k===tab;$('tab-'+k).classList.toggle('active',on);$('tab-'+k).setAttribute('aria-selected',on?'true':'false');$('panel-'+k).classList.toggle('active',on)})} async function api(url){var r=await fetch(url);var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d} +async function postApi(url){var r=await fetch(url,{method:'POST'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d} async function loadAll(){await Promise.all([loadSummary(),loadOrders(),loadOpenTrades(openOffset),loadCompleted(),loadEvents(eventOffset)])} +async function sendReport(){var btn=$('sendReportBtn'),note=$('reportNote');btn.disabled=true;btn.textContent='发送中...';note.style.display='block';note.textContent='正在汇总当前交易数据并发送飞书报告...';try{var d=await postApi('/api/paper-trading/report?days=30');note.textContent=d.ok?'报告已发送到飞书。':'报告生成完成,但飞书发送未成功:'+String(d.push_result||'未知原因');await loadSummary()}catch(e){note.textContent='发送失败:'+e.message}finally{btn.disabled=false;btn.textContent='发送模拟交易报告'}} async function loadSummary(){try{var d=await api('/api/paper-trading/summary?days=30');var totalPnl=Number(d.total_pnl_usdt||0),realized=Number(d.realized_pnl_usdt||0),unrealized=Number(d.open_unrealized_pnl_usdt||0),ret=Number(d.account_total_return_pct||0);$('paperNote').textContent='当前账户余额 = 初始本金 + 已实现收益 + 持仓浮动收益。累计杠杆按当前持仓名义价值 ÷ 当前账户余额计算,用来衡量账户整体暴露。';$('kpis').innerHTML=[ card('当前账户余额',fmt(d.current_balance_usdt||d.account_equity_usdt||0,2)+'U',totalPnl>=0?'green':'red','初始本金 '+fmt(d.initial_equity_usdt||d.account_equity_usdt||0,0)+'U'), card('当前持仓价值',fmt(d.open_position_value_usdt||0,0)+'U','blue',(d.open_count||0)+' 个持仓中'), diff --git a/tests/test_paper_trading.py b/tests/test_paper_trading.py index 5056c1a..aed1cdf 100644 --- a/tests/test_paper_trading.py +++ b/tests/test_paper_trading.py @@ -3,7 +3,42 @@ import json import pytest from app.db import altcoin_db -from app.db.paper_trading import get_paper_trading_summary, list_paper_orders, list_paper_trade_events, list_paper_trades, sync_recommendation +from app.db.paper_trading import ( + get_paper_trading_summary, + list_paper_orders, + list_paper_trade_events, + list_paper_trades, + send_paper_trading_report, + sync_recommendation, +) + + +def _visible_card_text(card: dict) -> str: + texts = [] + + def walk(value): + if isinstance(value, dict): + content = value.get("content") + if isinstance(content, str): + texts.append(content) + for child in value.values(): + walk(child) + elif isinstance(value, list): + for child in value: + walk(child) + + walk(card.get("header")) + walk(card.get("elements")) + return "\n".join(texts) + + +def _assert_no_paper_trading_copy(card: dict) -> None: + text = _visible_card_text(card).lower() + assert "模拟" not in text + assert "paper trading" not in text + assert "paper-trading" not in text + assert "paper trade" not in text + assert "paper_trades" not in text @pytest.fixture @@ -183,7 +218,8 @@ def test_wait_pullback_paper_order_pushes_created_card(monkeypatch): assert pushed card = pushed[0] assert card["metadata"]["event_type"] == "paper_order_create" - assert "模拟挂单创建" in card["header"]["title"]["content"] + assert "挂单创建" in card["header"]["title"]["content"] + _assert_no_paper_trading_copy(card) assert card["elements"][0]["tag"] == "column_set" @@ -330,7 +366,8 @@ def test_wait_pullback_paper_order_fill_pushes_single_combined_card(monkeypatch) event_types = [card["metadata"]["event_type"] for card in pushed] assert event_types == ["paper_order_create", "paper_order_fill"] - assert "模拟挂单成交并开仓" in pushed[1]["header"]["title"]["content"] + assert "挂单成交并开仓" in pushed[1]["header"]["title"]["content"] + _assert_no_paper_trading_copy(pushed[1]) assert "open" not in event_types @@ -343,10 +380,64 @@ def test_paper_trade_open_push_card_is_structured(monkeypatch, buy_now_rec): assert pushed card = pushed[0] assert card["metadata"]["event_type"] == "open" - assert "模拟交易开仓" in card["header"]["title"]["content"] + assert "交易开仓" in card["header"]["title"]["content"] + _assert_no_paper_trading_copy(card) assert card["elements"][0]["tag"] == "column_set" +def test_trailing_move_push_is_throttled_but_stop_still_updates(monkeypatch, buy_now_rec): + pushed = [] + rec = dict(buy_now_rec) + rec["tp1"] = 200 + rec["tp2"] = 220 + rec["entry_plan"] = { + "entry_action": "可即刻买入", + "entry_price": 100, + "stop_loss": 95, + "tp1": 200, + "tp2": 220, + "entry_trigger_confirmed": True, + "risk_reward_ok": True, + } + monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1") + monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3") + monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_INTERVAL_SECONDS", "300") + monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_STEP_PCT", "2") + monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0})) + + sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00") + activated = sync_recommendation(rec, 105, event_time="2026-05-16T10:01:00") + small_move = sync_recommendation(rec, 105.5, event_time="2026-05-16T10:01:05") + large_move = sync_recommendation(rec, 108, event_time="2026-05-16T10:01:10") + + assert small_move["moved"] is True + assert small_move["trailing_stop"] > activated["trailing_stop"] + assert small_move["notification_emitted"] is False + assert large_move["notification_emitted"] is True + assert [card["metadata"]["event_type"] for card in pushed] == ["open", "trailing_activate", "trailing_move"] + _assert_no_paper_trading_copy(pushed[-1]) + + +def test_send_paper_trading_report_pushes_performance_summary(monkeypatch, buy_now_rec): + pushed = [] + monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0})) + + sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00") + sync_recommendation(buy_now_rec, 106, event_time="2026-05-16T10:05:00") + result = send_paper_trading_report(days=30) + + assert result["ok"] is True + assert pushed[-1]["metadata"]["event_type"] == "trade_report" + text = _visible_card_text(pushed[-1]) + assert "交易报告" in text + assert "初始资金" in text + assert "当前资金" in text + assert "账户收益率" in text + assert "成功率" in text + assert "战绩概览" in text + _assert_no_paper_trading_copy(pushed[-1]) + + def test_summary_counts_pending_paper_orders(monkeypatch): monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1") altcoin_db.init_db()