feat(metrics): Fix 3 — DB-computed metrics, stateless tracker, resolution tracking
CI/CD / build-and-push (push) Successful in 1m47s

schema.sql
  trades:        + close_pnl, resolution (market outcome storage)
  metrics_daily: + unrealized_pnl_est, realized_pnl, open/closed/resolved_count

db.py
  close_paper_position(): accepts resolution; computes close_pnl in SQL
    BUY_YES: (resolution − entry_price) × shares
    BUY_NO:  ((1 − resolution) − entry_price) × shares
  save_daily_metrics(): persists new columns
  compute_metrics_from_db(): single DB query for all metrics; no in-memory state

tracker.py — complete rewrite (stateless)
  Removed self._trades, self._daily_returns, compute_metrics(), _compute_sharpe(),
  check_promotion_thresholds(), _empty_metrics()
  update_daily_summary() now reads compute_metrics_from_db() every cycle
  Safe across pod restarts: always reflects full DB history

paper.py
  close_position(): passes resolution to close_paper_position()

api/main.py  /api/summary
  Added unrealized_pnl_est (estimated, open trades) and realized_pnl (exact,
  closed+resolved) as separate fields alongside total_pnl
  win_rate: null if < 5 resolved trades (was proxy on entry_price < 0.5)
  calibration_score: Brier-based, null if < 10 resolved trades
  resolved_count exposed as field
  Each field annotated with: exact/estimated, source, null conditions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-04-21 17:34:48 +00:00
parent 9b62636a3e
commit 9a5be27532
5 changed files with 268 additions and 160 deletions
+105 -12
View File
@@ -55,13 +55,26 @@ class Database:
await conn.execute("""
INSERT INTO metrics_daily (
timestamp, total_trades, total_deployed, total_fees,
total_pnl, win_rate, avg_edge, sharpe_ratio, calibration_score, paper_mode
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
unrealized_pnl_est, realized_pnl, total_pnl,
win_rate, avg_edge, sharpe_ratio, calibration_score, paper_mode,
open_count, closed_count, resolved_count
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
""",
metrics["timestamp"], metrics["total_trades"], metrics["total_deployed"],
metrics["total_fees"], metrics["total_pnl"], metrics["win_rate"],
metrics["avg_edge"], metrics["sharpe_ratio"], metrics["calibration_score"],
metrics["timestamp"],
metrics["total_trades"],
metrics["total_deployed"],
metrics["total_fees"],
metrics["unrealized_pnl_est"],
metrics["realized_pnl"],
metrics["total_pnl"],
metrics["win_rate"],
metrics["avg_edge"],
metrics["sharpe_ratio"],
metrics["calibration_score"],
metrics["paper_mode"],
metrics["open_count"],
metrics["closed_count"],
metrics["resolved_count"],
)
async def get_open_positions(self) -> dict[str, float]:
@@ -103,14 +116,30 @@ class Database:
""")
return [dict(r) for r in rows]
async def close_paper_position(self, market_id: str, reason: str = "") -> None:
"""Mark a paper position as closed (sets closed_at timestamp)."""
async def close_paper_position(
self, market_id: str, reason: str = "", resolution: Optional[float] = None
) -> None:
"""Mark a paper position as closed.
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.
"""
async with self._pool.acquire() as conn:
await conn.execute(
"UPDATE trades SET closed_at = NOW(), close_reason = $2 "
"WHERE market_id = $1 AND closed_at IS NULL",
market_id, reason,
)
await conn.execute("""
UPDATE trades
SET closed_at = NOW(),
close_reason = $2,
resolution = $3,
close_pnl = CASE
WHEN $3 IS NOT NULL AND direction = 'BUY_YES'
THEN ($3::double precision - entry_price) * shares
WHEN $3 IS NOT NULL AND direction = 'BUY_NO'
THEN ((1.0 - $3::double precision) - entry_price) * shares
ELSE NULL
END
WHERE market_id = $1 AND closed_at IS NULL
""", market_id, reason, resolution)
async def update_family_key(self, market_id: str, new_key: str) -> None:
"""Persist a corrected family_key for all open trades of a market."""
@@ -142,6 +171,70 @@ class Database:
""", str(hours))
return {r["market_id"] for r in rows}
async def compute_metrics_from_db(self) -> dict:
"""Compute all trading metrics directly from the trades table.
This is the single source of truth for MetricsTracker — no in-memory
state required. Safe to call after pod restarts: always reflects the
full DB history.
Returns a dict with keys:
total_trades, open_count, closed_count, resolved_count,
total_deployed, total_fees,
unrealized_pnl_est — estimated, open trades with edge_net
realized_pnl — exact, closed trades with resolution
wins_realized — closed trades where close_pnl > 0
calibration_score — Brier-based (1 MSE), null if resolved < 10
"""
async with self._pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT
COUNT(*) AS total_trades,
COUNT(*) FILTER (WHERE closed_at IS NULL) AS open_count,
COUNT(*) FILTER (WHERE closed_at IS NOT NULL) AS closed_count,
COUNT(*) FILTER (WHERE resolution IS NOT NULL
AND final_prob IS NOT NULL) AS resolved_count,
COALESCE(SUM(net_cost)
FILTER (WHERE closed_at IS NULL), 0) AS total_deployed,
COALESCE(SUM(fee_usdc), 0) AS total_fees,
-- Estimated unrealized PnL: open trades with known edge.
-- Formula: edge_net × net_cost fee_usdc.
-- Trades with NULL edge_net (legacy data) are excluded.
COALESCE(SUM(edge_net * net_cost - fee_usdc)
FILTER (WHERE closed_at IS NULL
AND edge_net IS NOT NULL), 0) AS unrealized_pnl_est,
-- Realized PnL: closed trades with a known resolution.
-- close_pnl is computed at close time from actual resolution.
COALESCE(SUM(close_pnl)
FILTER (WHERE closed_at IS NOT NULL
AND close_pnl IS NOT NULL), 0) AS realized_pnl,
COUNT(*) FILTER (WHERE closed_at IS NOT NULL
AND close_pnl IS NOT NULL
AND close_pnl > 0) AS wins_realized,
-- Calibration (Brier score transformed to higher-is-better):
-- 1 AVG((final_prob resolution)²) on resolved trades.
-- final_prob is the model's estimated YES probability at entry.
-- resolution is 1.0 (YES won) or 0.0 (NO won).
-- Perfect calibration → 1.0 | Random → ~0.75 | Worst → 0.0
-- Returns NULL if fewer than 10 resolved trades with final_prob.
CASE
WHEN COUNT(*) FILTER (WHERE resolution IS NOT NULL
AND final_prob IS NOT NULL) >= 10
THEN 1.0 - AVG((final_prob - resolution) * (final_prob - resolution))
FILTER (WHERE resolution IS NOT NULL
AND final_prob IS NOT NULL)
ELSE NULL
END AS calibration_score
FROM trades
""")
return dict(row)
async def get_recent_trades(self, limit: int = 100, status: Optional[str] = None) -> list[dict]:
"""Return trades ordered by timestamp DESC.
+27
View File
@@ -108,3 +108,30 @@ ALTER TABLE trades ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
ALTER TABLE trades ADD COLUMN IF NOT EXISTS close_reason TEXT;
CREATE INDEX IF NOT EXISTS idx_trades_closed ON trades(closed_at) WHERE closed_at IS NOT NULL;
-- ─────────────────────────────────────────────────────────────────────────────
-- 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
-- NULL if closed without a known resolution (legacy closes, inversion fixes).
-- ─────────────────────────────────────────────────────────────────────────────
ALTER TABLE trades ADD COLUMN IF NOT EXISTS close_pnl DOUBLE PRECISION;
ALTER TABLE trades ADD COLUMN IF NOT EXISTS resolution DOUBLE PRECISION;
-- ─────────────────────────────────────────────────────────────────────────────
-- Fix 3: extended metrics_daily columns for DB-computed metrics
--
-- unrealized_pnl_est: SUM(edge_net * net_cost - fee) on open trades with edge_net.
-- Estimated — uses model's own edge signal, not live market price.
-- realized_pnl: SUM(close_pnl) on closed trades with a known resolution.
-- Exact — derived from actual market outcome.
-- open_count / closed_count / resolved_count: trade counts at snapshot time.
-- ─────────────────────────────────────────────────────────────────────────────
ALTER TABLE metrics_daily ADD COLUMN IF NOT EXISTS unrealized_pnl_est DOUBLE PRECISION;
ALTER TABLE metrics_daily ADD COLUMN IF NOT EXISTS realized_pnl DOUBLE PRECISION;
ALTER TABLE metrics_daily ADD COLUMN IF NOT EXISTS open_count INTEGER;
ALTER TABLE metrics_daily ADD COLUMN IF NOT EXISTS closed_count INTEGER;
ALTER TABLE metrics_daily ADD COLUMN IF NOT EXISTS resolved_count INTEGER;