From e2fb697c0c665577b12021984819edfe4d063b35 Mon Sep 17 00:00:00 2001 From: chemavx Date: Tue, 21 Apr 2026 09:37:45 +0000 Subject: [PATCH] fix: family_key repair, reentry guard, legacy_incomplete, trades status filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db: update_family_key() persists corrected family slugs for open trades - db: get_recently_closed_inverted() returns markets closed for inversion within N hours; used as reentry guard in the trading loop - db: get_recent_trades() accepts status=open|closed|None and adds a computed "status" field to every row - bot/main.py: legacy scan now computes family_key from stored question alone (dummy Market) when a position's market is no longer active — fixes NULL family_key on legacy trades like Ken Paxton (562186) - bot/main.py: legacy scan (Step 2.5) persists corrected family_keys in DB so family conflict guards work correctly on next restart - bot/main.py: positions with NULL edge_net and no live market are tagged legacy_incomplete instead of OK; counted separately in scan summary - bot/main.py: reentry_guard blocks re-entering any market closed for inversion bug within 24h; logs reentry_guard_triggered per skip - api/main.py: /api/trades now accepts ?status=open|closed|all (default open) and includes status_filter in response DB fix (applied directly): 629558 family_key politics-2026 → ohio-gubernatorial-2026; 562186 family_key NULL → texas-republican-2026 Co-Authored-By: Claude Sonnet 4.6 --- api/main.py | 13 ++++++++--- bot/data/db.py | 44 ++++++++++++++++++++++++++++++++++--- bot/main.py | 59 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/api/main.py b/api/main.py index 52d7ebc..0a88158 100644 --- a/api/main.py +++ b/api/main.py @@ -42,9 +42,16 @@ async def get_metrics(): @app.get("/api/trades") -async def get_trades(limit: int = 50): - trades = await db.get_recent_trades(limit=limit) - return {"trades": trades, "count": len(trades)} +async def get_trades(limit: int = 50, status: str = "open"): + """ + status: "open" (default) | "closed" | "all" + Each trade includes a computed "status" field. + """ + if status not in ("open", "closed", "all"): + status = "open" + filter_status = None if status == "all" else status + trades = await db.get_recent_trades(limit=limit, status=filter_status) + return {"trades": trades, "count": len(trades), "status_filter": status} @app.get("/api/summary") diff --git a/bot/data/db.py b/bot/data/db.py index 1ae0c02..db0d72f 100644 --- a/bot/data/db.py +++ b/bot/data/db.py @@ -112,12 +112,50 @@ class Database: market_id, reason, ) - async def get_recent_trades(self, limit: int = 100) -> list[dict]: + 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.""" + async with self._pool.acquire() as conn: + await conn.execute( + "UPDATE trades SET family_key = $2 WHERE market_id = $1 AND closed_at IS NULL", + market_id, new_key, + ) + + async def get_recently_closed_inverted(self, hours: int = 24) -> set[str]: + """Return market_ids closed for inversion bug within the last N hours. + + Used as a reentry guard: prevents re-entering a market that was just + closed because the signal direction was inverted. + """ + async with self._pool.acquire() as conn: + rows = await conn.fetch(""" + SELECT DISTINCT market_id FROM trades + WHERE closed_at > NOW() - ($1 || ' hours')::interval + AND close_reason ILIKE '%inversion bug%' + """, str(hours)) + return {r["market_id"] for r in rows} + + async def get_recent_trades(self, limit: int = 100, status: Optional[str] = None) -> list[dict]: + """Return trades ordered by timestamp DESC. + + status: None (all) | "open" (closed_at IS NULL) | "closed" (closed_at IS NOT NULL) + Each row includes a computed "status" field ("open" or "closed"). + """ + if status == "open": + where = "WHERE closed_at IS NULL" + elif status == "closed": + where = "WHERE closed_at IS NOT NULL" + else: + where = "" async with self._pool.acquire() as conn: rows = await conn.fetch( - "SELECT * FROM trades ORDER BY timestamp DESC LIMIT $1", limit + f"SELECT * FROM trades {where} ORDER BY timestamp DESC LIMIT $1", limit ) - return [dict(r) for r in rows] + result = [] + for r in rows: + d = dict(r) + d["status"] = "closed" if d.get("closed_at") else "open" + result.append(d) + return result async def get_metrics_history(self, days: int = 42) -> list[dict]: async with self._pool.acquire() as conn: diff --git a/bot/main.py b/bot/main.py index e29a69a..dc922e6 100644 --- a/bot/main.py +++ b/bot/main.py @@ -7,7 +7,7 @@ import logging import os from datetime import datetime, timezone -from bot.data.polymarket import PolymarketClient, market_family_key +from bot.data.polymarket import PolymarketClient, Market, market_family_key from bot.data.external import ExternalDataClient from bot.data.news import NewsClient from bot.data.manifold import ManifoldClient @@ -92,8 +92,23 @@ async def run_trading_loop( strategy.reset_cycle() # 5. Evaluate each market + # Fetch markets recently closed for inversion bug — block re-entry for 24h + inverted_guard: set[str] = await db.get_recently_closed_inverted(hours=24) + if inverted_guard: + log.info( + "Reentry guard active for %d market(s) (inversion, 24h): %s", + len(inverted_guard), sorted(inverted_guard), + ) + cycle_trades = 0 for market in markets: + if market.id in inverted_guard: + log.info( + "reentry_guard_triggered market=%s | skipping — closed for inversion within 24h | %s", + market.id, market.question[:60], + ) + continue + # evaluate() returns None for all skips — reasons are logged internally signal = await strategy.evaluate(market, ext_data, occupied_families) if signal is None: @@ -200,7 +215,21 @@ async def run_legacy_scan( mid = str(pos["market_id"]) live_mkt = market_by_id.get(mid) old_fk = pos.get("family_key") or "" - new_fk = market_family_key(live_mkt) if live_mkt else (old_fk or "unknown") + if live_mkt: + new_fk = market_family_key(live_mkt) + else: + # Market not in active list — compute from stored question alone + _dummy = Market( + id=mid, condition_id="", question=pos["question"], + yes_token_id="", no_token_id="", + yes_price=0.5, no_price=0.5, + volume_24h=0, end_date="", active=False, category="", + ) + computed = market_family_key(_dummy) + # Reject degenerate fallbacks that start with "-" (missing category + end_date) + new_fk = computed if not computed.startswith("-") else (old_fk or "unknown") + + is_legacy_incomplete = (pos.get("edge_net") is None) and (not live_mkt) enriched.append({ **dict(pos), "market_id": mid, @@ -210,8 +239,8 @@ async def run_legacy_scan( "fk_changed": new_fk != old_fk, "manifold_prob_new": None, "manifold_inverted": False, - "recommendation": "OK", - "rec_reason": "no family conflict", + "recommendation": "legacy_incomplete" if is_legacy_incomplete else "OK", + "rec_reason": "edge_net and live market unavailable" if is_legacy_incomplete else "no family conflict", }) # Step 2: group by new family key — identify conflicting siblings @@ -238,6 +267,15 @@ async def run_legacy_scan( p["recommendation"] = "REVIEW" p["rec_reason"] = "family key changed but no sibling conflict" + # Step 2.5: persist corrected family keys in DB for changed positions + for p in enriched: + if p["fk_changed"] and p["family_key_new"] not in ("unknown", ""): + await db.update_family_key(p["market_id"], p["family_key_new"]) + log.info( + "family_key updated in DB: market=%s | %s → %s", + p["market_id"], p["family_key_old"] or "none", p["family_key_new"], + ) + # Step 3: Manifold re-query for positions whose family key changed for p in enriched: if p["live_market"] and p["fk_changed"]: @@ -263,14 +301,15 @@ async def run_legacy_scan( p["rec_reason"] += f" | {note}" # Step 4: log the full scan report (before any closures) - n_close = sum(1 for p in enriched if p["recommendation"] == "CLOSE_RECOMMENDED") - n_keep = sum(1 for p in enriched if p["recommendation"] == "KEEP") - n_ok = sum(1 for p in enriched if p["recommendation"] == "OK") - n_review = sum(1 for p in enriched if p["recommendation"] == "REVIEW") + n_close = sum(1 for p in enriched if p["recommendation"] == "CLOSE_RECOMMENDED") + n_keep = sum(1 for p in enriched if p["recommendation"] == "KEEP") + n_ok = sum(1 for p in enriched if p["recommendation"] == "OK") + n_review = sum(1 for p in enriched if p["recommendation"] == "REVIEW") + n_legacy = sum(1 for p in enriched if p["recommendation"] == "legacy_incomplete") log.warning( - "━" * 70 + "\nLEGACY SCAN — %d position(s): OK=%d KEEP=%d REVIEW=%d CLOSE_RECOMMENDED=%d", - len(enriched), n_ok, n_keep, n_review, n_close, + "━" * 70 + "\nLEGACY SCAN — %d position(s): OK=%d KEEP=%d REVIEW=%d CLOSE_RECOMMENDED=%d LEGACY_INCOMPLETE=%d", + len(enriched), n_ok, n_keep, n_review, n_close, n_legacy, ) for p in enriched: log.warning(