diff --git a/app/db/paper_trading.py b/app/db/paper_trading.py index be78b1f..1630974 100644 --- a/app/db/paper_trading.py +++ b/app/db/paper_trading.py @@ -1138,6 +1138,39 @@ def _report_order_line(item: dict) -> str: return f"- **{symbol}** · {side} · 目标 {_fmt_price(item.get('target_price'))} · 距目标 {_fmt_pct(distance)}" +def _paper_running_days() -> float: + conn = get_conn() + try: + row = conn.execute( + """ + SELECT MIN(first_at) AS first_at + FROM ( + SELECT MIN(opened_at) AS first_at FROM paper_trades + UNION ALL + SELECT MIN(created_at) AS first_at FROM paper_orders + ) x + WHERE first_at IS NOT NULL AND first_at <> '' + """ + ).fetchone() + finally: + conn.close() + first_at = _parse_time(row["first_at"] if row else "") + if not first_at: + return 0.0 + now = datetime.now(first_at.tzinfo) if first_at.tzinfo else datetime.now() + seconds = max(0.0, (now - first_at).total_seconds()) + return max(1.0, seconds / 86400) + + +def _periodized_return_pct(total_return_pct: float, running_days: float, period_days: float) -> float: + if running_days <= 0: + return 0.0 + growth = 1 + total_return_pct / 100 + if growth <= 0: + return -100.0 + return round((growth ** (period_days / running_days) - 1) * 100, 4) + + 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) @@ -1152,6 +1185,9 @@ def send_paper_trading_report(days: int = 30) -> dict: 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")) + running_days = _paper_running_days() + monthly_return_pct = _periodized_return_pct(return_pct, running_days, 30) + annualized_return_pct = _periodized_return_pct(return_pct, running_days, 365) template = "green" if total_pnl >= 0 else "red" elements = [ { @@ -1161,7 +1197,17 @@ def send_paper_trading_report(days: int = 30) -> dict: "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("运行天数", f"{running_days:.1f} 天")]}, + ], + }, + { + "tag": "column_set", + "flex_mode": "none", + "background_style": "default", + "columns": [ {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("账户收益率", _fmt_pct(return_pct))]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("月化收益率", _fmt_pct(monthly_return_pct))]}, + {"tag": "column", "width": "weighted", "weight": 1, "elements": [_card_field("年化收益率", _fmt_pct(annualized_return_pct))]}, ], }, { @@ -1201,6 +1247,9 @@ def send_paper_trading_report(days: int = 30) -> dict: return { "ok": bool(ok), "days": days, + "running_days": running_days, + "monthly_return_pct": monthly_return_pct, + "annualized_return_pct": annualized_return_pct, "summary": summary, "sent_at": _now(), "push_result": push_result, diff --git a/tests/test_paper_trading.py b/tests/test_paper_trading.py index aed1cdf..e14f257 100644 --- a/tests/test_paper_trading.py +++ b/tests/test_paper_trading.py @@ -427,12 +427,18 @@ def test_send_paper_trading_report_pushes_performance_summary(monkeypatch, buy_n result = send_paper_trading_report(days=30) assert result["ok"] is True + assert result["running_days"] >= 1 + assert "monthly_return_pct" in result + assert "annualized_return_pct" in result 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 "年化收益率" in text assert "成功率" in text assert "战绩概览" in text _assert_no_paper_trading_copy(pushed[-1])