fix: family_key repair, reentry guard, legacy_incomplete, trades status filter
CI/CD / build-and-push (push) Successful in 1m54s

- 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 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-04-21 09:37:45 +00:00
parent d698544f30
commit e2fb697c0c
3 changed files with 100 additions and 16 deletions
+10 -3
View File
@@ -42,9 +42,16 @@ async def get_metrics():
@app.get("/api/trades") @app.get("/api/trades")
async def get_trades(limit: int = 50): async def get_trades(limit: int = 50, status: str = "open"):
trades = await db.get_recent_trades(limit=limit) """
return {"trades": trades, "count": len(trades)} 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") @app.get("/api/summary")
+41 -3
View File
@@ -112,12 +112,50 @@ class Database:
market_id, reason, 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: async with self._pool.acquire() as conn:
rows = await conn.fetch( 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 def get_metrics_history(self, days: int = 42) -> list[dict]:
async with self._pool.acquire() as conn: async with self._pool.acquire() as conn:
+45 -6
View File
@@ -7,7 +7,7 @@ import logging
import os import os
from datetime import datetime, timezone 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.external import ExternalDataClient
from bot.data.news import NewsClient from bot.data.news import NewsClient
from bot.data.manifold import ManifoldClient from bot.data.manifold import ManifoldClient
@@ -92,8 +92,23 @@ async def run_trading_loop(
strategy.reset_cycle() strategy.reset_cycle()
# 5. Evaluate each market # 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 cycle_trades = 0
for market in markets: 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 # evaluate() returns None for all skips — reasons are logged internally
signal = await strategy.evaluate(market, ext_data, occupied_families) signal = await strategy.evaluate(market, ext_data, occupied_families)
if signal is None: if signal is None:
@@ -200,7 +215,21 @@ async def run_legacy_scan(
mid = str(pos["market_id"]) mid = str(pos["market_id"])
live_mkt = market_by_id.get(mid) live_mkt = market_by_id.get(mid)
old_fk = pos.get("family_key") or "" 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({ enriched.append({
**dict(pos), **dict(pos),
"market_id": mid, "market_id": mid,
@@ -210,8 +239,8 @@ async def run_legacy_scan(
"fk_changed": new_fk != old_fk, "fk_changed": new_fk != old_fk,
"manifold_prob_new": None, "manifold_prob_new": None,
"manifold_inverted": False, "manifold_inverted": False,
"recommendation": "OK", "recommendation": "legacy_incomplete" if is_legacy_incomplete else "OK",
"rec_reason": "no family conflict", "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 # Step 2: group by new family key — identify conflicting siblings
@@ -238,6 +267,15 @@ async def run_legacy_scan(
p["recommendation"] = "REVIEW" p["recommendation"] = "REVIEW"
p["rec_reason"] = "family key changed but no sibling conflict" 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 # Step 3: Manifold re-query for positions whose family key changed
for p in enriched: for p in enriched:
if p["live_market"] and p["fk_changed"]: if p["live_market"] and p["fk_changed"]:
@@ -267,10 +305,11 @@ async def run_legacy_scan(
n_keep = sum(1 for p in enriched if p["recommendation"] == "KEEP") 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_ok = sum(1 for p in enriched if p["recommendation"] == "OK")
n_review = sum(1 for p in enriched if p["recommendation"] == "REVIEW") 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( log.warning(
"" * 70 + "\nLEGACY SCAN — %d position(s): OK=%d KEEP=%d REVIEW=%d CLOSE_RECOMMENDED=%d", "" * 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, len(enriched), n_ok, n_keep, n_review, n_close, n_legacy,
) )
for p in enriched: for p in enriched:
log.warning( log.warning(