import { signRequest, KALSHI_API_BASE } from './auth.js'; const OPEN_EVENT_STATUSES = new Set(['open', 'active', 'initialized', 'trading']); async function kalshiFetch(method, path, body = null) { const headers = signRequest(method, path); const opts = { method, headers }; if (body) opts.body = JSON.stringify(body); const res = await fetch(`${KALSHI_API_BASE}${path}`, opts); if (!res.ok) { const text = await res.text(); throw new Error(`Kalshi API ${method} ${path} → ${res.status}: ${text}`); } return res.json(); } function getTimeMs(value) { if (!value) return null; const ts = new Date(value).getTime(); return Number.isFinite(ts) ? ts : null; } function getEventCloseTimeMs(event) { return ( getTimeMs(event?.close_time) || getTimeMs(event?.expiration_time) || getTimeMs(event?.settlement_time) || getTimeMs(event?.end_date) || null ); } function pickBestEvent(events = []) { const now = Date.now(); const ranked = events .filter(Boolean) .map((event) => { const status = String(event.status || '').toLowerCase(); const closeTs = getEventCloseTimeMs(event); const openLike = OPEN_EVENT_STATUSES.has(status); const notClearlyExpired = closeTs == null || closeTs > now - 60_000; return { event, openLike, closeTs, notClearlyExpired }; }) .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; } async function fetchEvents(series, query) { const path = `/trade-api/v2/events?series_ticker=${encodeURIComponent(series)}&${query}`; const data = await kalshiFetch('GET', path); return Array.isArray(data.events) ? data.events : []; } /** * Get events for the BTC 15-min series. * Returns the currently active event + its markets. */ export async function getActiveBTCEvent() { const configuredSeries = process.env.KALSHI_SERIES_TICKER || 'KXBTC15M'; const seriesCandidates = [...new Set([configuredSeries, configuredSeries.toUpperCase(), configuredSeries.toLowerCase()])]; const eventMap = new Map(); for (const series of seriesCandidates) { for (const query of ['status=open&limit=5', 'limit=25']) { try { const events = await fetchEvents(series, query); for (const event of events) { if (event?.event_ticker) eventMap.set(event.event_ticker, event); } } catch (e) { console.error(`[Kalshi] Event fetch failed (${series}, ${query}):`, e.message); } } if (eventMap.size) break; } return pickBestEvent([...eventMap.values()]); } /** * Get markets for a specific event ticker. */ export async function getEventMarkets(eventTicker) { const data = await kalshiFetch('GET', `/trade-api/v2/events/${eventTicker}`); return data.event?.markets || []; } /** * Get orderbook for a specific market ticker. */ export async function getOrderbook(ticker) { const data = await kalshiFetch('GET', `/trade-api/v2/markets/${ticker}/orderbook`); return data.orderbook || data; } /** * Get single market details. */ export async function getMarket(ticker) { const data = await kalshiFetch('GET', `/trade-api/v2/markets/${ticker}`); return data.market || data; } /** * Place a real order on Kalshi. NOT used in paper mode. */ export async function placeOrder(params) { return kalshiFetch('POST', '/trade-api/v2/portfolio/orders', params); } /** * Get wallet balance. */ export async function getBalance() { return kalshiFetch('GET', '/trade-api/v2/portfolio/balance'); } export { kalshiFetch };