This commit is contained in:
aaron 2026-05-22 11:41:42 +08:00
parent 1b8c1310bb
commit c781dfef08
4 changed files with 140 additions and 2 deletions

View File

@ -1256,6 +1256,98 @@ def get_paper_trading_summary(days: int = 30) -> dict:
}
def get_paper_trading_performance(days: int = 30) -> dict:
days = max(1, min(_safe_int(days, 30), 365))
cfg = paper_trading_config()
initial_equity = default_account_equity_usdt(cfg)
today = datetime.now().date()
start_date = today - timedelta(days=days - 1)
conn = get_conn()
try:
closed_rows = conn.execute(
"""
SELECT closed_at, realized_pnl_usdt
FROM paper_trades
WHERE status='closed' AND closed_at IS NOT NULL
ORDER BY closed_at ASC, id ASC
"""
).fetchall()
open_unrealized = conn.execute(
"""
SELECT COALESCE(SUM(notional_usdt * pnl_pct / 100.0), 0)
FROM paper_trades
WHERE status='open'
"""
).fetchone()[0]
finally:
conn.close()
daily_realized = {}
realized_before = 0.0
for row in closed_rows:
closed_at = _parse_time(row["closed_at"])
pnl = _safe_float(row["realized_pnl_usdt"])
if not closed_at:
continue
closed_date = closed_at.date()
if closed_date < start_date:
realized_before += pnl
continue
if closed_date > today:
continue
key = closed_date.isoformat()
daily_realized[key] = daily_realized.get(key, 0.0) + pnl
points = []
cumulative_realized = realized_before
prev_equity = initial_equity + realized_before
peak_equity = max(initial_equity, prev_equity)
max_drawdown_pct = 0.0
max_drawdown_usdt = 0.0
open_unrealized = _safe_float(open_unrealized)
for idx in range(days):
day = start_date + timedelta(days=idx)
key = day.isoformat()
realized = round(daily_realized.get(key, 0.0), 8)
cumulative_realized += realized
unrealized = open_unrealized if day == today else 0.0
equity = round(initial_equity + cumulative_realized + unrealized, 8)
daily_pnl = round(equity - prev_equity, 8)
daily_return_pct = round(daily_pnl / prev_equity * 100, 6) if prev_equity > 0 else 0.0
peak_equity = max(peak_equity, equity)
drawdown_usdt = round(max(0.0, peak_equity - equity), 8)
drawdown_pct = round(drawdown_usdt / peak_equity * 100, 6) if peak_equity > 0 else 0.0
max_drawdown_pct = max(max_drawdown_pct, drawdown_pct)
max_drawdown_usdt = max(max_drawdown_usdt, drawdown_usdt)
points.append(
{
"date": key,
"equity_usdt": equity,
"daily_pnl_usdt": daily_pnl,
"daily_return_pct": daily_return_pct,
"realized_pnl_usdt": round(cumulative_realized, 8),
"unrealized_pnl_usdt": round(unrealized, 8),
"return_pct": _account_return_pct(equity - initial_equity, initial_equity, cfg),
"drawdown_usdt": drawdown_usdt,
"drawdown_pct": drawdown_pct,
}
)
prev_equity = equity
total_pnl = round((points[-1]["equity_usdt"] - initial_equity) if points else 0.0, 8)
return {
"days": days,
"initial_equity_usdt": initial_equity,
"current_equity_usdt": points[-1]["equity_usdt"] if points else initial_equity,
"total_pnl_usdt": total_pnl,
"total_return_pct": _account_return_pct(total_pnl, initial_equity, cfg),
"max_drawdown_usdt": round(max_drawdown_usdt, 8),
"max_drawdown_pct": round(max_drawdown_pct, 6),
"points": points,
}
def list_paper_trades(limit: int = 50, offset: int = 0, status: str = "") -> dict:
limit = max(1, min(_safe_int(limit, 50), 200))
offset = max(0, _safe_int(offset, 0))

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter, Cookie
from app.db.paper_trading import (
get_paper_trading_performance,
get_paper_trading_summary,
list_paper_orders,
list_paper_trade_events,
@ -19,6 +20,12 @@ async def api_paper_trading_summary(days: int = 30, altcoin_session: str = Cooki
return get_paper_trading_summary(days=days)
@router.get("/api/paper-trading/performance")
async def api_paper_trading_performance(days: int = 30, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_paper_trading_performance(days=days)
@router.get("/api/paper-trading/trades")
async def api_paper_trading_trades(
limit: int = 50,

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,11 @@
import json
from datetime import datetime
import pytest
from app.db import altcoin_db
from app.db.paper_trading import (
get_paper_trading_performance,
get_paper_trading_summary,
list_paper_orders,
list_paper_trade_events,
@ -136,6 +138,26 @@ def test_paper_margin_is_derived_from_notional_and_leverage(monkeypatch):
assert summary["margin_usdt"] == pytest.approx(1000.0)
def test_paper_performance_returns_daily_equity_curve(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
now = datetime.now().replace(microsecond=0).isoformat()
sync_recommendation(buy_now_rec, 100, event_time=now)
sync_recommendation(buy_now_rec, 106, event_time=now)
curve = get_paper_trading_performance(days=7)
assert curve["points"]
assert curve["current_equity_usdt"] == pytest.approx(20006.0)
assert curve["total_pnl_usdt"] == pytest.approx(6.0)
assert curve["total_return_pct"] == pytest.approx(0.03)
assert curve["max_drawdown_pct"] >= 0
assert curve["points"][-1]["daily_pnl_usdt"] == pytest.approx(6.0)
assert curve["points"][-1]["equity_usdt"] == pytest.approx(20006.0)
def test_observation_does_not_open_paper_trade(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()