fix(accounting): store net PnL (payout - net_cost) in close_pnl, migrate Paxton record from gross to net
CI/CD / build-and-push (push) Successful in 7s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-06-11 19:39:34 +00:00
co-authored by Claude Fable 5
parent c5ffc37820
commit 060fc89953
3 changed files with 15 additions and 7 deletions
+1 -1
View File
@@ -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,
+10 -3
View File
@@ -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
+4 -3
View File
@@ -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;