diff --git a/lib/market/tracker.js b/lib/market/tracker.js index e75ea2e..e94f8c5 100644 --- a/lib/market/tracker.js +++ b/lib/market/tracker.js @@ -1,8 +1,9 @@ -import { getActiveBTCEvent, getEventMarkets, getOrderbook, getMarket } from '../kalshi/rest.js'; +import { getActiveBTCEvents, getEventMarkets, getOrderbook, getMarket } from '../kalshi/rest.js'; import { KalshiWS } from '../kalshi/websocket.js'; import { EventEmitter } from 'events'; const OPEN_MARKET_STATUSES = new Set(['open', 'active', 'initialized', 'trading']); +const TRADABLE_MARKET_STATUSES = new Set(['open', 'active', 'trading']); /** * Tracks the currently active BTC 15-min market. @@ -79,35 +80,95 @@ export class MarketTracker extends EventEmitter { }; } + _toTs(value) { + if (!value) return null; + const ts = new Date(value).getTime(); + return Number.isFinite(ts) ? ts : null; + } + + _pickBestMarket(markets = []) { + const now = Date.now(); + + const ranked = markets + .filter(Boolean) + .map((market) => { + const status = String(market?.status || '').toLowerCase(); + const closeTs = + this._toTs(market?.close_time) || + this._toTs(market?.expiration_time) || + this._toTs(market?.settlement_time) || + null; + + const tradable = TRADABLE_MARKET_STATUSES.has(status); + const openLike = OPEN_MARKET_STATUSES.has(status); + const notClearlyExpired = closeTs == null || closeTs > now - 60_000; + + return { market, tradable, openLike, notClearlyExpired, closeTs }; + }) + .filter((x) => x.openLike || x.notClearlyExpired); + + if (!ranked.length) return markets[0] || null; + + ranked.sort((a, b) => { + if (a.tradable !== b.tradable) return a.tradable ? -1 : 1; + if (a.openLike !== b.openLike) return a.openLike ? -1 : 1; + if (a.notClearlyExpired !== b.notClearlyExpired) return a.notClearlyExpired ? -1 : 1; + const aTs = a.closeTs ?? Number.MAX_SAFE_INTEGER; + const bTs = b.closeTs ?? Number.MAX_SAFE_INTEGER; + return aTs - bTs; + }); + + return ranked[0].market; + } + async _findAndSubscribe() { try { - const event = await getActiveBTCEvent(); + const candidates = await getActiveBTCEvents(12); - if (!event) { + if (!candidates.length) { if (!this.currentTicker) this.emit('update', null); console.log('[Tracker] No active BTC 15m event found. Retrying in 30s...'); return; } - const inlineMarkets = Array.isArray(event.markets) ? event.markets : []; - const markets = inlineMarkets.length ? inlineMarkets : await getEventMarkets(event.event_ticker); + let selectedEvent = null; + let selectedMarket = null; - const market = markets.find((m) => OPEN_MARKET_STATUSES.has(String(m?.status || '').toLowerCase())) || markets[0]; + for (const event of candidates) { + const eventTicker = event?.event_ticker; + if (!eventTicker) continue; - if (!market) { - if (!this.currentTicker) { - this.currentEvent = event.event_ticker || null; - this.marketData = null; - this.orderbook = { yes: [], no: [] }; - this.emit('update', null); + let markets = Array.isArray(event.markets) ? event.markets : []; + if (!markets.length) { + try { + markets = await getEventMarkets(eventTicker); + } catch (e) { + console.error(`[Tracker] Failed loading markets for ${eventTicker}:`, e.message); + continue; + } } - console.log(`[Tracker] Event ${event.event_ticker} has no active market yet. Retrying...`); + + if (!markets.length) continue; + + const market = this._pickBestMarket(markets); + if (!market?.ticker) continue; + + selectedEvent = event; + selectedMarket = market; + break; + } + + if (!selectedEvent || !selectedMarket) { + if (!this.currentTicker) this.emit('update', null); + console.log('[Tracker] No tradable BTC 15m market found yet. Retrying...'); return; } - const newTicker = market.ticker; + const newTicker = selectedMarket.ticker; if (newTicker === this.currentTicker) { - this.currentEvent = event.event_ticker; + this.currentEvent = selectedEvent.event_ticker || this.currentEvent; + this.marketData = { ...(this.marketData || {}), ...selectedMarket }; + this.emit('update', this.getState()); return; } @@ -120,8 +181,8 @@ export class MarketTracker extends EventEmitter { } this.currentTicker = newTicker; - this.currentEvent = event.event_ticker; - this.marketData = market; + this.currentEvent = selectedEvent.event_ticker; + this.marketData = selectedMarket; this.orderbook = { yes: [], no: [] }; // Fetch fresh orderbook via REST @@ -134,7 +195,7 @@ export class MarketTracker extends EventEmitter { // Subscribe via WS this.ws.subscribeTicker(newTicker); - console.log(`[Tracker] Now tracking: ${newTicker} (${market.title || market.subtitle})`); + console.log(`[Tracker] Now tracking: ${newTicker} (${selectedMarket.title || selectedMarket.subtitle || selectedEvent.event_ticker})`); this.emit('update', this.getState()); this.emit('market-rotated', { from: oldTicker, to: newTicker }); @@ -153,8 +214,11 @@ export class MarketTracker extends EventEmitter { const state = this.getState(); this.emit('update', state); + const status = String(fresh?.status || '').toLowerCase(); + const settledLike = status === 'closed' || status === 'settled' || status === 'expired' || status === 'finalized'; + // If market closed/settled, find the next one - if (fresh.status === 'closed' || fresh.status === 'settled' || fresh.result) { + if (settledLike || fresh.result) { console.log(`[Tracker] Market ${this.currentTicker} settled (result: ${fresh.result}). Rotating...`); this.emit('settled', { ticker: this.currentTicker, result: fresh.result }); this.currentTicker = null;