fix: family_key repair, reentry guard, legacy_incomplete, trades status filter
CI/CD / build-and-push (push) Successful in 1m54s
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:
+10
-3
@@ -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
@@ -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:
|
||||
|
||||
+45
-6
@@ -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"]:
|
||||
@@ -267,10 +305,11 @@ async def run_legacy_scan(
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user