diff --git a/api/main.py b/api/main.py index b1e21f4..5ac5f12 100644 --- a/api/main.py +++ b/api/main.py @@ -313,7 +313,7 @@ async def get_summary(): # unrealized_pnl_est: open positions, edge_net × net_cost − fee. # Estimated — uses model signal, not live price. Source: open trades. # realized_pnl: closed positions with known resolution. - # Exact — computed from (resolution − entry_price) × shares. + # Exact — payout − net_cost per trade (net of fee), matches logs/Telegram. # total_pnl: sum of both. "unrealized_pnl_est": latest.get("unrealized_pnl_est") or 0, "realized_pnl": latest.get("realized_pnl") or 0, diff --git a/bot/data/db.py b/bot/data/db.py index a543f0b..0f8bd9f 100644 --- a/bot/data/db.py +++ b/bot/data/db.py @@ -173,7 +173,14 @@ class Database: resolution: 1.0 if YES resolved, 0.0 if NO resolved, None if unknown (legacy closes, inversion fixes). When resolution is provided, close_pnl - is computed in SQL so it matches the stored entry_price and shares exactly. + is computed in SQL per row as payout − net_cost — NET of fee, the single + PnL definition shared with PaperExecutor.close_position() (logs/Telegram): + BUY_YES: resolution * shares − net_cost + BUY_NO: (1 − resolution) * shares − net_cost + paper.py aggregates payout − net_cost over these same open rows, so + SUM(close_pnl) per market equals the pnl it reports exactly. The + aggregate is intentionally NOT passed in as a parameter: writing it to + every row would double-count markets with more than one open trade. """ async with self._pool.acquire() as conn: # $3 is cast on every use: Postgres cannot infer the parameter type @@ -186,9 +193,9 @@ class Database: resolution = $3::double precision, close_pnl = CASE WHEN $3::double precision IS NOT NULL AND direction = 'BUY_YES' - THEN ($3::double precision - entry_price) * shares + THEN ($3::double precision * shares) - net_cost WHEN $3::double precision IS NOT NULL AND direction = 'BUY_NO' - THEN ((1.0 - $3::double precision) - entry_price) * shares + THEN ((1.0 - $3::double precision) * shares) - net_cost ELSE NULL END WHERE market_id = $1 AND closed_at IS NULL diff --git a/bot/data/schema.sql b/bot/data/schema.sql index b02d248..7b8af74 100644 --- a/bot/data/schema.sql +++ b/bot/data/schema.sql @@ -113,9 +113,10 @@ CREATE INDEX IF NOT EXISTS idx_trades_closed ON trades(closed_at) WHERE closed_a -- Fix 3: market resolution and realized P&L per trade -- -- resolution: 1.0 if YES resolved, 0.0 if NO resolved, NULL if not yet settled. --- close_pnl: realized P&L in USDC at close time. --- BUY_YES: (resolution - entry_price) * shares --- BUY_NO: ((1 - resolution) - entry_price) * shares +-- close_pnl: realized P&L in USDC at close time — NET of fee (payout − net_cost), +-- the same definition PaperExecutor.close_position() reports in logs/Telegram. +-- BUY_YES: resolution * shares - net_cost +-- BUY_NO: (1 - resolution) * shares - net_cost -- NULL if closed without a known resolution (legacy closes, inversion fixes). -- ───────────────────────────────────────────────────────────────────────────── ALTER TABLE trades ADD COLUMN IF NOT EXISTS close_pnl DOUBLE PRECISION;