fix(performance): exclude paper trades by default (real-money tile must be real)

/performance summed paper + live P&L into one number. DashboardClient's
30d Performance tile consumes this directly and presents it as real
performance — so a user who tried paper mode then went live saw simulated
gains inflating the headline number on the homepage.

Now: exclude hl_order_id=='paper' by default. Added optional ?include_paper
query param for callers that explicitly want simulated stats. Analytics page
already filters client-side by is_paper (separate fix), so both surfaces now
agree: real-money numbers never include simulated fills.

72 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-30 03:04:38 +08:00
parent 752652f463
commit 0d88e3e43a
+15 -2
View File
@@ -23,6 +23,12 @@ async def get_performance(
wallet: str = Query(..., description="Wallet address (lower-cased internally)"),
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
include_paper: bool = Query(
False,
description="Include paper (simulated) trades. Default false — this "
"endpoint reports REAL-money performance, so paper fills "
"(hl_order_id='paper') are excluded unless explicitly asked.",
),
db: AsyncSession = Depends(get_db),
):
wallet = wallet.lower().strip()
@@ -43,13 +49,20 @@ async def get_performance(
# realization order. A trade opened before the window but closed inside it
# correctly counts; one opened inside but still open does not (closed_at IS
# NOT NULL already excludes it).
result = await db.execute(
#
# MONEY-SAFETY: by default exclude paper trades (hl_order_id == "paper").
# Mixing simulated and real P&L into one "performance" number is misleading
# — the dashboard tile that consumes this shows it as real performance.
stmt = (
select(BotTrade)
.where(BotTrade.wallet_address == wallet)
.where(BotTrade.closed_at.is_not(None))
.where(BotTrade.closed_at >= since)
.order_by(BotTrade.closed_at.asc())
)
if not include_paper:
stmt = stmt.where(BotTrade.hl_order_id != "paper")
stmt = stmt.order_by(BotTrade.closed_at.asc())
result = await db.execute(stmt)
trades = result.scalars().all()
total_trades = len(trades)