From 96f1f9359edf88e692dd57677b55ab54c0d0d9eb Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sun, 15 Mar 2026 14:54:58 -0700 Subject: [PATCH] Fix: Rank BTC events and expose candidates --- lib/kalshi/rest.js | 60 +++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/kalshi/rest.js b/lib/kalshi/rest.js index ae19d72..6681155 100644 --- a/lib/kalshi/rest.js +++ b/lib/kalshi/rest.js @@ -2,6 +2,7 @@ import { signRequest, KALSHI_API_BASE } from './auth.js'; const SERIES_TICKER = 'KXBTC15M'; const OPEN_EVENT_STATUSES = new Set(['open', 'active', 'initialized', 'trading']); +const TRADABLE_EVENT_STATUSES = new Set(['open', 'active', 'trading']); async function kalshiFetch(method, path, body = null) { const headers = signRequest(method, path); @@ -32,30 +33,33 @@ function getEventCloseTimeMs(event) { ); } -function pickBestEvent(events = []) { +function rankEvents(events = []) { const now = Date.now(); - const ranked = events + return events .filter(Boolean) .map((event) => { const status = String(event.status || '').toLowerCase(); const closeTs = getEventCloseTimeMs(event); + const openLike = OPEN_EVENT_STATUSES.has(status); + const tradableLike = TRADABLE_EVENT_STATUSES.has(status); const notClearlyExpired = closeTs == null || closeTs > now - 60_000; - return { event, openLike, closeTs, notClearlyExpired }; + + // Prefer near-future close times; heavily penalize stale/past + const delta = closeTs == null ? Number.MAX_SAFE_INTEGER : closeTs - now; + const closenessScore = delta < 0 ? Math.abs(delta) + 3_600_000 : delta; + + return { event, openLike, tradableLike, notClearlyExpired, closenessScore, closeTs }; }) - .filter((x) => x.openLike || x.notClearlyExpired); - - if (!ranked.length) return events[0] || null; - - ranked.sort((a, b) => { - if (a.openLike !== b.openLike) return a.openLike ? -1 : 1; - const aTs = a.closeTs ?? Number.MAX_SAFE_INTEGER; - const bTs = b.closeTs ?? Number.MAX_SAFE_INTEGER; - return aTs - bTs; - }); - - return ranked[0].event; + .filter((x) => x.openLike || x.notClearlyExpired) + .sort((a, b) => { + if (a.tradableLike !== b.tradableLike) return a.tradableLike ? -1 : 1; + if (a.openLike !== b.openLike) return a.openLike ? -1 : 1; + if (a.notClearlyExpired !== b.notClearlyExpired) return a.notClearlyExpired ? -1 : 1; + return a.closenessScore - b.closenessScore; + }) + .map((x) => x.event); } async function fetchEvents(series, query) { @@ -65,15 +69,19 @@ async function fetchEvents(series, query) { } /** - * Get events for the BTC 15-min series. - * Returns the currently active event + its markets. + * Return ranked candidate events for BTC 15m. */ -export async function getActiveBTCEvent() { +export async function getActiveBTCEvents(limit = 12) { const seriesCandidates = [...new Set([SERIES_TICKER, SERIES_TICKER.toLowerCase()])]; const eventMap = new Map(); for (const series of seriesCandidates) { - for (const query of ['status=open&limit=5', 'limit=25']) { + for (const query of [ + 'status=open&limit=10', + 'status=active&limit=10', + 'status=initialized&limit=10', + 'limit=50' + ]) { try { const events = await fetchEvents(series, query); for (const event of events) { @@ -83,10 +91,17 @@ export async function getActiveBTCEvent() { console.error(`[Kalshi] Event fetch failed (${series}, ${query}):`, e.message); } } - if (eventMap.size) break; } - return pickBestEvent([...eventMap.values()]); + return rankEvents([...eventMap.values()]).slice(0, limit); +} + +/** + * Backward-compatible: return single best candidate event. + */ +export async function getActiveBTCEvent() { + const events = await getActiveBTCEvents(1); + return events[0] || null; } /** @@ -94,7 +109,8 @@ export async function getActiveBTCEvent() { */ export async function getEventMarkets(eventTicker) { const data = await kalshiFetch('GET', `/trade-api/v2/events/${eventTicker}`); - return data.event?.markets || []; + const markets = data?.event?.markets ?? data?.markets ?? data?.event_markets ?? []; + return Array.isArray(markets) ? markets : []; } /**