This commit is contained in:
aaron 2026-05-20 22:06:59 +08:00
parent fc11c73413
commit fe5c8a7c3f
5 changed files with 287 additions and 39 deletions

View File

@ -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": "锁利"},

View File

@ -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,
}

View File

@ -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)

View File

@ -13,10 +13,12 @@
<p>这里展示的是 paper trading 账本:只有系统把可买信号模拟成交后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
</div>
<div class="actions">
<button class="btn" id="sendReportBtn" onclick="sendReport()">发送模拟交易报告</button>
<button class="btn" onclick="loadAll()">刷新</button>
</div>
</div>
<div class="note" id="paperNote">模拟交易只统计已经进入 paper trading 的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
<div class="note" id="reportNote" style="display:none"></div>
<div class="kpis" id="kpis"><div class="kpi"><span>状态</span><b>加载中</b></div></div>
<div class="tabs" role="tablist" aria-label="模拟交易视图切换">
<button class="tab-btn active" id="tab-open" type="button" onclick="setTradeTab('open')" role="tab" aria-selected="true">持仓中</button>
@ -99,7 +101,9 @@ function sideText(v){return String(v||'long').toLowerCase()==='short'?'空':'多
function sideBadge(v){var s=String(v||'long').toLowerCase();return '<span class="badge '+(s==='short'?'side-short':'side-long')+'">'+sideText(s)+'</span>'}
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)+' 个持仓中'),

View File

@ -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()