From 9a5be27532f674a04206eb07e66a70e14c47cde8 Mon Sep 17 00:00:00 2001 From: chemavx Date: Tue, 21 Apr 2026 17:34:48 +0000 Subject: [PATCH] =?UTF-8?q?feat(metrics):=20Fix=203=20=E2=80=94=20DB-compu?= =?UTF-8?q?ted=20metrics,=20stateless=20tracker,=20resolution=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api/main.py | 77 +++++++++++------ bot/data/db.py | 117 ++++++++++++++++++++++--- bot/data/schema.sql | 27 ++++++ bot/executor/paper.py | 19 +++-- bot/metrics/tracker.py | 188 ++++++++++++++++------------------------- 5 files changed, 268 insertions(+), 160 deletions(-) diff --git a/api/main.py b/api/main.py index cb77076..904c598 100644 --- a/api/main.py +++ b/api/main.py @@ -89,43 +89,68 @@ async def get_trades(limit: int = 50, status: str = "open"): @app.get("/api/summary") async def get_summary(): - """Dashboard summary card data.""" - history = await db.get_metrics_history(days=1) - open_trades, all_trades, inverted, legacy_count = await asyncio.gather( + """Dashboard summary card data. + + All portfolio counts (total_trades, open_trades_count, total_deployed, + cash_available) are computed live from the DB on every request. + + PnL and performance metrics come from the latest metrics_daily snapshot, + which is written by the bot every cycle via MetricsTracker.update_daily_summary(). + After Fix 3, that snapshot is also DB-computed — not dependent on pod restarts. + """ + latest_metrics, open_trades, all_trades, inverted, legacy_count = await asyncio.gather( + db.get_metrics_history(days=1), db.get_recent_trades(limit=500, status="open"), db.get_recent_trades(limit=500), db.get_recently_closed_inverted(hours=24), db.get_legacy_incomplete_count(), ) - latest = history[0] if history else {} + latest = latest_metrics[0] if latest_metrics else {} paper_bankroll = float(os.getenv("PAPER_BANKROLL", "10000")) total_deployed = sum(t.get("net_cost", 0) for t in open_trades) return { - "paper_mode": os.getenv("PAPER_MODE", "true") == "true", - "paper_bankroll": paper_bankroll, - "total_trades": len(all_trades), - "open_trades_count": len(open_trades), - "closed_trades_count": len(all_trades) - len(open_trades), - "total_deployed": total_deployed, - "cash_available": max(0.0, paper_bankroll - total_deployed), - "legacy_incomplete_count": legacy_count, - "reentry_guard_blocks_24h": len(inverted), - # Metrics from latest metrics_daily snapshot (computed by MetricsTracker). - # total_pnl: estimated unrealized PnL for open trades in the current bot - # session — uses edge_net × net_cost (model edge on deployed - # capital). Resets to 0 on pod restart until Fix 3 is applied. - # calibration_score: null until market resolution data is available - # (requires close_price / outcome per closed trade). - "total_pnl": latest.get("total_pnl", 0), - "win_rate": latest.get("win_rate", 0), - "sharpe_ratio": latest.get("sharpe_ratio", 0), - "calibration_score": latest.get("calibration_score"), # null if unavailable + # ── Portfolio state (live from DB) ────────────────────────────────── + "paper_mode": os.getenv("PAPER_MODE", "true") == "true", + "paper_bankroll": paper_bankroll, + "total_trades": len(all_trades), # exact, from DB + "open_trades_count": len(open_trades), # exact, from DB + "closed_trades_count": len(all_trades) - len(open_trades), # exact + "total_deployed": total_deployed, # exact, from DB + "cash_available": max(0.0, paper_bankroll - total_deployed), # exact + "legacy_incomplete_count": legacy_count, # exact, from DB + "reentry_guard_blocks_24h": len(inverted), # exact, from DB + + # ── P&L (from latest metrics_daily snapshot) ──────────────────────── + # 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. + # total_pnl: sum of both. + "unrealized_pnl_est": latest.get("unrealized_pnl_est") or 0, + "realized_pnl": latest.get("realized_pnl") or 0, + "total_pnl": latest.get("total_pnl") or 0, + + # ── Performance metrics (from latest metrics_daily snapshot) ───────── + # win_rate: fraction of resolved closed trades where close_pnl > 0. + # null if fewer than 5 resolved trades. Source: closed+resolved trades. + # sharpe_ratio: 0.0 — requires daily-return time series (not yet tracked). + # calibration_score: 1 − Brier score on resolved trades (higher = better). + # null if fewer than 10 resolved trades. Source: closed+resolved trades. + "win_rate": latest.get("win_rate"), # null if < 5 resolved + "sharpe_ratio": latest.get("sharpe_ratio") or 0, # 0.0 until tracked + "calibration_score": latest.get("calibration_score"), # null if < 10 resolved + + # ── Counters from snapshot ─────────────────────────────────────────── + "resolved_count": latest.get("resolved_count") or 0, + + # ── Promotion gate ─────────────────────────────────────────────────── + # All thresholds must pass; null metrics count as not-ready. "promotion_ready": ( - latest.get("sharpe_ratio", 0) >= 0.5 - and latest.get("win_rate", 0) >= 0.52 - and (latest.get("calibration_score") or 0) >= 0.7 # null → not ready + (latest.get("sharpe_ratio") or 0) >= 0.5 + and (latest.get("win_rate") or 0) >= 0.52 + and (latest.get("calibration_score") or 0) >= 0.7 and len(all_trades) >= 50 ), } diff --git a/bot/data/db.py b/bot/data/db.py index 3a56896..c310bc3 100644 --- a/bot/data/db.py +++ b/bot/data/db.py @@ -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. diff --git a/bot/data/schema.sql b/bot/data/schema.sql index cabea54..942b863 100644 --- a/bot/data/schema.sql +++ b/bot/data/schema.sql @@ -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; diff --git a/bot/executor/paper.py b/bot/executor/paper.py index 1daddfc..11904e3 100644 --- a/bot/executor/paper.py +++ b/bot/executor/paper.py @@ -177,16 +177,23 @@ class PaperExecutor: return cost async def close_position(self, market_id: str, resolution: float) -> Optional[float]: - """ - Close a paper position after market resolution. + """Close a paper position after market resolution. + resolution: 1.0 if YES won, 0.0 if NO won. - Returns P&L in USDC. + Persists resolution and close_pnl to DB (computed via SQL from stored + entry_price and shares). Returns approximate P&L for logging. """ if market_id not in self._portfolio.positions: return None - # This would be called by a settlement watcher (future feature) - # For now, positions auto-expire at market end date position_cost = self._portfolio.positions.pop(market_id) - log.info("Closed position in %s, resolution=%.0f", market_id, resolution) + self._portfolio.cash += position_cost * resolution # pay out winnings + + await self._db.close_paper_position( + market_id, + reason=f"market_resolved resolution={resolution:.1f}", + resolution=resolution, + ) + log.info("Closed position in %s, resolution=%.1f", market_id, resolution) + # Approximate PnL: settlement value minus cost. Exact value is in close_pnl. return position_cost * resolution - position_cost diff --git a/bot/metrics/tracker.py b/bot/metrics/tracker.py index 8543156..3b5357d 100644 --- a/bot/metrics/tracker.py +++ b/bot/metrics/tracker.py @@ -1,21 +1,27 @@ """ -Metrics Tracker — Computes trading performance metrics. +Metrics Tracker — computes and persists trading performance metrics from the DB. -Key metrics tracked: -- P&L (cumulative and daily) -- Sharpe Ratio (annualized) -- Win Rate -- Calibration Score (how accurate our probability estimates are) -- Max Drawdown -- Average Edge realized +All metrics are derived directly from the `trades` table on every cycle call. +No in-memory trade state is kept: the tracker is stateless across pod restarts. + +Metric definitions +────────────────── +unrealized_pnl_est Estimated PnL for OPEN positions: edge_net × net_cost − fee. + Source: open trades with edge_net. Estimated (model signal). +realized_pnl Exact PnL for CLOSED positions: computed from resolution. + Source: closed trades with known resolution. Exact. +total_pnl unrealized_pnl_est + realized_pnl. +win_rate Fraction of resolved closed trades with close_pnl > 0. + NULL if fewer than 5 resolved trades. +calibration_score 1 − AVG((final_prob − resolution)²) on resolved trades. + Brier score (higher = better calibration). NULL if < 10 resolved. +sharpe_ratio 0.0 — requires a daily-return time series, not yet tracked. """ import logging -import math from datetime import datetime, UTC -from typing import Optional -from bot.executor.paper import Trade from bot.data.db import Database +from bot.executor.paper import Trade log = logging.getLogger(__name__) @@ -23,119 +29,69 @@ log = logging.getLogger(__name__) class MetricsTracker: def __init__(self, db: Database) -> None: self._db = db - self._trades: list[Trade] = [] - self._daily_returns: list[float] = [] async def record_trade(self, trade: Trade) -> None: - self._trades.append(trade) + """Persist a trade to the DB. No in-memory accumulation.""" await self._db.save_trade(trade) - log.info("Trade recorded. Total trades: %d", len(self._trades)) + log.info("Trade recorded: %s", trade) async def update_daily_summary(self) -> None: - """Compute and store daily metrics snapshot.""" - if not self._trades: + """Compute metrics from DB and write a metrics_daily snapshot. + + Called every cycle by the trading loop. Safe after pod restarts: + reads the full trade history from DB, not from in-memory state. + """ + raw = await self._db.compute_metrics_from_db() + if not raw["total_trades"]: return - metrics = self.compute_metrics() + open_count = int(raw["open_count"] or 0) + closed_count = int(raw["closed_count"] or 0) + resolved = int(raw["resolved_count"] or 0) + wins = int(raw["wins_realized"] or 0) + unrealized = float(raw["unrealized_pnl_est"] or 0) + realized = float(raw["realized_pnl"] or 0) + total_deployed = float(raw["total_deployed"] or 0) + total_fees = float(raw["total_fees"] or 0) + total_pnl = unrealized + realized + + # win_rate: only over resolved closed trades; null if sample too small + win_rate = (wins / resolved) if resolved >= 5 else None + + # calibration: Brier score from DB; null if sample too small + calibration = ( + float(raw["calibration_score"]) + if raw["calibration_score"] is not None and resolved >= 10 + else None + ) + + avg_edge = total_pnl / total_deployed if total_deployed > 0 else 0.0 + + metrics = { + "timestamp": datetime.now(UTC), + "total_trades": int(raw["total_trades"]), + "open_count": open_count, + "closed_count": closed_count, + "resolved_count": resolved, + "total_deployed": total_deployed, + "total_fees": total_fees, + "unrealized_pnl_est": unrealized, + "realized_pnl": realized, + "total_pnl": total_pnl, + "win_rate": win_rate, + "avg_edge": avg_edge, + "sharpe_ratio": 0.0, # requires daily-return series (not yet tracked) + "calibration_score": calibration, + "paper_mode": True, + } await self._db.save_daily_metrics(metrics) log.info( - "Daily metrics | Trades: %d | P&L: $%.2f | Win: %.1f%% | Sharpe: %.2f", - metrics["total_trades"], - metrics["total_pnl"], - metrics["win_rate"] * 100, - metrics["sharpe_ratio"], + "Daily metrics | trades=%d (open=%d closed=%d resolved=%d) | " + "unrealized=$%.2f realized=$%.2f total=$%.2f | " + "win_rate=%s calibration=%s", + metrics["total_trades"], open_count, closed_count, resolved, + unrealized, realized, total_pnl, + f"{win_rate:.1%}" if win_rate is not None else "n/a (<5)", + f"{calibration:.3f}" if calibration is not None else "n/a (<10)", ) - - def compute_metrics(self) -> dict: - if not self._trades: - return self._empty_metrics() - - trades = self._trades - n = len(trades) - - # ── Capital: all in-session trades (open + closed this session) ──────── - # NOTE: self._trades is in-memory; resets on pod restart. - # Fix 3 (planned): replace with DB-computed metrics so restarts don't - # truncate history. Until then, these numbers reflect the current session. - total_deployed = sum(t.net_cost for t in trades) - total_fees = sum(t.fee_usdc for t in trades) - - # ── Win rate ───────────────────────────────────────────────────────── - # Proxy for open trades: fraction where edge_net > 0. - # Not a realized win rate (no market resolutions available yet). - wins = sum(1 for t in trades if t.entry_price < 0.5) - win_rate = wins / n if n > 0 else 0 - - # ── Estimated unrealized P&L (open positions only) ─────────────────── - # Formula: model_edge × deployed_capital per trade. - # Conservative bound: edge_net ∈ [-1, 1] → max PnL = net_cost per trade. - # Previous formula (0.5 − entry_price) × shares inflated BUY_NO trades - # at low entry prices by 3–10× (e.g. entry=0.158 → 3164 shares → $1072 - # PnL on $500 deployed, vs $122 with edge_net=0.2589 here). - # Trades with NULL edge_net (legacy data) contribute only −fee_usdc. - total_pnl = sum( - (t.edge_net or 0.0) * t.net_cost - t.fee_usdc - for t in trades - ) - - avg_edge = total_pnl / total_deployed if total_deployed > 0 else 0 - - sharpe = self._compute_sharpe() - - # ── Calibration score: not available ───────────────────────────────── - # Real calibration (Brier score) requires knowing how each market - # resolved (YES=1 or NO=0). Until close_price / resolution is stored - # per trade, any formula here is a proxy, not a calibration. - # Returns None so the API can surface "unavailable" rather than a - # misleading number. Will be computed from closed trades in Fix 3. - calibration = None # type: ignore[assignment] - - return { - "timestamp": datetime.now(UTC), - "total_trades": n, - "total_deployed": total_deployed, - "total_fees": total_fees, - "total_pnl": total_pnl, # estimated unrealized (open trades, current session) - "win_rate": win_rate, # proxy: fraction with entry_price < 0.5 - "avg_edge": avg_edge, - "sharpe_ratio": sharpe, - "calibration_score": calibration, # None — requires market resolution data - "paper_mode": True, - } - - def _compute_sharpe(self) -> float: - """Annualized Sharpe ratio from daily returns.""" - if len(self._daily_returns) < 2: - return 0.0 - mean_r = sum(self._daily_returns) / len(self._daily_returns) - variance = sum((r - mean_r) ** 2 for r in self._daily_returns) / len(self._daily_returns) - std_r = math.sqrt(variance) if variance > 0 else 1e-9 - return (mean_r / std_r) * math.sqrt(365) # Annualize - - def check_promotion_thresholds(self) -> tuple[bool, dict]: - """Check if metrics qualify for real money trading.""" - metrics = self.compute_metrics() - cal = metrics["calibration_score"] # may be None - checks = { - "sharpe_ratio": (metrics["sharpe_ratio"], 0.5, metrics["sharpe_ratio"] >= 0.5), - "win_rate": (metrics["win_rate"], 0.52, metrics["win_rate"] >= 0.52), - "calibration_score": (cal, 0.7, cal is not None and cal >= 0.7), - "min_trades": (metrics["total_trades"], 50, metrics["total_trades"] >= 50), - } - all_pass = all(v[2] for v in checks.values()) - return all_pass, checks - - def _empty_metrics(self) -> dict: - return { - "timestamp": datetime.now(UTC), - "total_trades": 0, - "total_deployed": 0, - "total_fees": 0, - "total_pnl": 0, - "win_rate": 0, - "avg_edge": 0, - "sharpe_ratio": 0, - "calibration_score": None, # requires market resolution data - "paper_mode": True, - }