diff --git a/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go b/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go index 280adba7..bede415a 100644 --- a/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go +++ b/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go @@ -4,12 +4,9 @@ package main // // Unlike Polymarket (UMA on-chain events, 2h floor), Kalshi resolves via its // own internal process. Delay is measured as: -// observed_resolution_time - market.close_time -// -// where observed_resolution_time = first poll at which result != null. -// Accuracy is bounded by KALSHI_POLL_SECONDS (default 300 = 5 min), which -// is negligible for delays measured in hours. +// settlement_ts - market.close_time // +// settlement_ts is returned directly by the Kalshi API (exact, no polling error). // No auth required — uses the public /trade-api/v2/markets endpoint. import ( @@ -23,17 +20,18 @@ import ( ) const ( - kalshiMarketsURL = "https://api.elections.kalshi.com/trade-api/v2/markets" + kalshiMarketsURL = "https://api.elections.kalshi.com/trade-api/v2/markets" kalshiLookbackDays = 30 ) type kalshiMarketRecord struct { - Ticker string `json:"ticker"` - Title string `json:"title"` - CloseTime string `json:"close_time"` // RFC3339 - Status string `json:"status"` // "open", "closed", "finalized" - Result string `json:"result"` // "yes", "no", "" when unresolved - Category string `json:"category"` // "Sports", "Politics", "Crypto", etc. + Ticker string `json:"ticker"` + Title string `json:"title"` + CloseTime string `json:"close_time"` // RFC3339 + SettlementTs string `json:"settlement_ts"` // RFC3339Nano, present when settled + Status string `json:"status"` // "open", "closed", "finalized" + Result string `json:"result"` // "yes", "no", "" when unresolved + Category string `json:"category"` // "Sports", "Politics", "Crypto", etc. } type kalshiMarketsResponse struct { @@ -129,13 +127,14 @@ func (k *kalshiTracker) pollClosed() { } } -// pollFinalized fetches recently finalized markets and emits histogram -// observations for any we haven't recorded yet. +// pollFinalized fetches recently settled markets and emits histogram +// observations for any we haven't recorded yet. The API filter param is +// "settled"; the status field in the response body is "finalized". +// Delay is settlement_ts - close_time (exact, from API, no polling error). func (k *kalshiTracker) pollFinalized() { cutoff := time.Now().Add(-kalshiLookbackDays * 24 * time.Hour) - now := time.Now() - markets, _, err := k.fetchPage("finalized", "") + markets, _, err := k.fetchPage("settled", "") if err != nil { log.Printf("[kalshi-res] pollFinalized error: %v", err) return @@ -160,15 +159,23 @@ func (k *kalshiTracker) pollFinalized() { k.recorded[m.Ticker] = struct{}{} delete(k.watched, m.Ticker) - delay := now.Sub(ct).Seconds() + // Prefer settlement_ts (exact); fall back to now (first-poll proxy, ±5 min). + var settleAt time.Time + if m.SettlementTs != "" { + settleAt, _ = time.Parse(time.RFC3339Nano, m.SettlementTs) + } + if settleAt.IsZero() { + settleAt = time.Now() + } + delay := settleAt.Sub(ct).Seconds() if delay < 0 || delay > float64(kalshiLookbackDays*24*3600) { continue } cat := normalizeKalshiCategory(m.Category) resolutionDelay.WithLabelValues("kalshi", cat, "false").Observe(delay) resolutionsTotal.WithLabelValues("kalshi", cat, "false").Inc() - log.Printf("[kalshi-res] resolved ticker=%s category=%s delay=%.0fs close_time=%s result=%s", - m.Ticker, cat, delay, m.CloseTime, m.Result) + log.Printf("[kalshi-res] resolved ticker=%s category=%s delay=%.0fs close_time=%s settled=%s result=%s", + m.Ticker, cat, delay, m.CloseTime, settleAt.UTC().Format(time.RFC3339), m.Result) } }