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")
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")
+41 -3
View File
@@ -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:
+49 -10
View File
@@ -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(