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")
|
@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
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user