@@ -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