Skip to content

Commit 936c276

Browse files
fix(winrate): 5 bugs in confirmation flow - syntax, clear semantics, dedupe logs, slot-drift, orphan expiry
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c564b86 commit 936c276

2 files changed

Lines changed: 38 additions & 36 deletions

File tree

src/utils/winrate_upgrade.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,22 @@ def clear(self, key: str) -> bool:
132132
logger = logging.getLogger("webhook_server_fastapi")
133133
with self.lock:
134134
exists_before = key in self._data
135-
logger.info(f"confirmation_clear_attempt: key={key} exists_before={exists_before}")
136-
if key in self._data:
135+
if not exists_before:
136+
# Key already removed (e.g. by handle() on confirmed) — this is normal, log at DEBUG
137+
logger.debug(f"confirmation_clear_noop: key={key} (already removed by handle)")
138+
return False
139+
logger.info(f"confirmation_clear_attempt: key={key} exists_before=True")
140+
try:
141+
del self._data[key]
137142
try:
138-
del self._data[key]
139-
try:
140-
self._save()
141-
except Exception:
142-
pass
143-
logger.info(f"confirmation_cleared: key={key} removed=True")
144-
return True
143+
self._save()
145144
except Exception:
146-
logger.exception(f"confirmation_cleared: key={key} removed=False (exception)")
147-
return False
148-
logger.info(f"confirmation_cleared: key={key} removed=False")
149-
return False
145+
pass
146+
logger.info(f"confirmation_cleared: key={key} removed=True")
147+
return True
148+
except Exception:
149+
logger.exception(f"confirmation_cleared: key={key} removed=False (exception)")
150+
return False
150151

151152

152153
def check_market_quality_for_entry(best_bid: Optional[float], best_ask: Optional[float], ask_size: Optional[float], settings) -> Tuple[bool, str, Dict[str, Any]]:

webhook_server_fastapi.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,24 @@ async def cleanup_task():
221221
asyncio.create_task(cleanup_task())
222222
logger.info("Background cleanup task started")
223223

224+
# Start confirmation store expiry task (cleans up orphaned pending keys)
225+
async def confirmation_expiry_task():
226+
"""Periodically expire old confirmation keys to prevent store bloat."""
227+
ttl = getattr(settings, "CONFIRMATION_TTL_SECONDS", 180)
228+
poll_interval = max(60.0, ttl / 2) # Check at half-TTL interval, minimum 60s
229+
while True:
230+
try:
231+
await asyncio.sleep(poll_interval)
232+
expired = _confirmation_store.expire_all_older_than(ttl)
233+
if expired > 0:
234+
logger.info(f"confirmation_expiry_task: expired {expired} orphaned keys (ttl={ttl}s)")
235+
except asyncio.CancelledError:
236+
break
237+
except Exception as e:
238+
logger.error(f"confirmation_expiry_task error: {e}")
239+
asyncio.create_task(confirmation_expiry_task())
240+
logger.info("Confirmation expiry task started")
241+
224242
# Rehydrate active paper trades from paper_trades.jsonl
225243
if is_paper_trading():
226244
rehydrated_count = rehydrate_paper_trades()
@@ -3540,30 +3558,13 @@ def webhook(payload: WebhookPayload):
35403558
# ─────────────────────────────────────────────
35413559
try:
35423560
if getattr(settings, "WINRATE_UPGRADE_ENABLED", False) and getattr(settings, "REQUIRE_CONFIRMATION", False):
3543-
# Build a stable confirmation key using market/token + direction + signal_id
3561+
# Build a stable confirmation key that does NOT depend on current slot/market
3562+
# (avoids slot-drift: Alert1 at 14:59 slot A, Alert2 at 15:01 slot B → key mismatch)
3563+
# Key = "pm:confirm:<direction>:<signal_id>" — signal_id is stable across both alerts
35443564
try:
3545-
now_ts = int(time.time())
3546-
slot = current_slot_start(now_ts)
3547-
slug = slug_for_slot(slot)
3548-
try:
3549-
market = fetch_market_by_slug(slug)
3550-
except Exception:
3551-
market = None
3552-
up_token, down_token = resolve_up_down_tokens(market) if market else (None, None)
3553-
# Build structured confirmation key:
3554-
# Prefer market id, else token id, include direction and signal_id to avoid collisions.
3555-
sig_id = payload.signal_id or signal_id_for_logging or "no-signal-id"
3556-
market_id = None
3557-
if market and isinstance(market, dict):
3558-
market_id = market.get("id")
3559-
token_id = up_token or down_token or None
3560-
if market_id:
3561-
conf_key = f\"pm:market:{market_id}:{sig_for_dedupe}:{sig_id}\"
3562-
elif token_id:
3563-
conf_key = f\"pm:token:{token_id}:{sig_for_dedupe}:{sig_id}\"
3564-
else:
3565-
# Fallback to slug-based key (least preferred)
3566-
conf_key = f\"pm:slug:{slug}:{sig_for_dedupe}:{sig_id}\"
3565+
sig_id = payload.signal_id or "no-signal-id"
3566+
conf_key = f"pm:confirm:{sig_for_dedupe}:{sig_id}"
3567+
35673568

35683569
# Use new high-level handle API (returns pending/expired/confirmed)
35693570
result = _confirmation_store.handle(

0 commit comments

Comments
 (0)