From 0d88e3e43ad443fd1c90d0c8510d31f7bcff0872 Mon Sep 17 00:00:00 2001 From: k Date: Sat, 30 May 2026 03:04:38 +0800 Subject: [PATCH] fix(performance): exclude paper trades by default (real-money tile must be real) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 --- app/api/performance.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/api/performance.py b/app/api/performance.py index 058094e..071a18b 100644 --- a/app/api/performance.py +++ b/app/api/performance.py @@ -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)