Fix: robust quote/orderbook parsing

This commit is contained in:
2026-03-15 15:06:23 -07:00
parent 3c48e2bd50
commit d7dabea20f

View File

@@ -24,16 +24,11 @@ export class MarketTracker extends EventEmitter {
async start() {
console.log('[Tracker] Starting market tracker...');
// Connect WebSocket
this.ws.connect();
this.ws.on('orderbook', (msg) => this._onOrderbook(msg));
this.ws.on('ticker', (msg) => this._onTicker(msg));
// Initial market discovery
await this._findAndSubscribe();
// Check for market rotation every 30 seconds
this.rotateInterval = setInterval(() => this._checkRotation(), 30000);
}
@@ -45,14 +40,34 @@ export class MarketTracker extends EventEmitter {
getState() {
if (!this.marketData) return null;
const yesAsk = this.orderbook.yes?.[0]?.[0] || this.marketData.yes_ask;
const noAsk = this.orderbook.no?.[0]?.[0] || this.marketData.no_ask;
const quotes = this._extractMarketQuotes(this.marketData);
const bestYesBook = this._bestBookPrice(this.orderbook.yes);
const bestNoBook = this._bestBookPrice(this.orderbook.no);
// Prices on Kalshi are in cents (1-99)
const yesPct = yesAsk || 50;
const noPct = noAsk || 50;
const yesBid = quotes.yesBid ?? bestYesBook;
const noBid = quotes.noBid ?? bestNoBook;
const yesAsk = quotes.yesAsk ?? (noBid != null ? 100 - noBid : null);
const noAsk = quotes.noAsk ?? (yesBid != null ? 100 - yesBid : null);
let yesPct = yesAsk ?? yesBid ?? bestYesBook;
let noPct = noAsk ?? noBid ?? bestNoBook;
if (yesPct == null && noPct != null) yesPct = 100 - noPct;
if (noPct == null && yesPct != null) noPct = 100 - yesPct;
if (yesPct == null && noPct == null && quotes.lastPrice != null) {
yesPct = quotes.lastPrice;
noPct = 100 - quotes.lastPrice;
}
if (yesPct == null || noPct == null) {
yesPct = 50;
noPct = 50;
}
yesPct = this._clampPct(yesPct) ?? 50;
noPct = this._clampPct(noPct) ?? 50;
// Odds = 100 / price
const yesOdds = yesPct > 0 ? (100 / yesPct).toFixed(2) : '0.00';
const noOdds = noPct > 0 ? (100 / noPct).toFixed(2) : '0.00';
@@ -65,14 +80,14 @@ export class MarketTracker extends EventEmitter {
noPct,
yesOdds: parseFloat(yesOdds),
noOdds: parseFloat(noOdds),
yesBid: this.marketData.yes_bid,
yesAsk: this.marketData.yes_ask,
noBid: this.marketData.no_bid,
noAsk: this.marketData.no_ask,
volume: this.marketData.volume || 0,
volume24h: this.marketData.volume_24h || 0,
openInterest: this.marketData.open_interest || 0,
lastPrice: this.marketData.last_price,
yesBid: this._clampPct(yesBid),
yesAsk: this._clampPct(yesAsk),
noBid: this._clampPct(noBid),
noAsk: this._clampPct(noAsk),
volume: this._num(this.marketData.volume) ?? 0,
volume24h: this._num(this.marketData.volume_24h) ?? 0,
openInterest: this._num(this.marketData.open_interest) ?? 0,
lastPrice: this._clampPct(quotes.lastPrice),
closeTime: this.marketData.close_time || this.marketData.expiration_time,
status: this.marketData.status,
result: this.marketData.result,
@@ -80,12 +95,82 @@ export class MarketTracker extends EventEmitter {
};
}
_num(value) {
if (value == null) return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
_clampPct(value) {
const n = this._num(value);
if (n == null) return null;
return Math.max(0, Math.min(100, n));
}
_toTs(value) {
if (!value) return null;
const ts = new Date(value).getTime();
return Number.isFinite(ts) ? ts : null;
}
_extractMarketQuotes(market) {
const pick = (...keys) => {
for (const key of keys) {
const v = this._num(market?.[key]);
if (v != null) return v;
}
return null;
};
return {
yesBid: pick('yes_bid', 'yesBid'),
yesAsk: pick('yes_ask', 'yesAsk'),
noBid: pick('no_bid', 'noBid'),
noAsk: pick('no_ask', 'noAsk'),
lastPrice: pick('last_price', 'lastPrice', 'yes_price', 'yesPrice')
};
}
_normalizeBookSide(levels) {
if (!Array.isArray(levels)) return [];
const out = [];
for (const level of levels) {
let price = null;
let qty = null;
if (Array.isArray(level)) {
price = level[0];
qty = level[1];
} else if (level && typeof level === 'object') {
price = level.price ?? level[0];
qty = level.qty ?? level.quantity ?? level.size ?? level.count ?? level[1];
}
const p = this._num(price);
const q = this._num(qty);
if (p == null || q == null || q <= 0) continue;
out.push([p, q]);
}
return out.sort((a, b) => b[0] - a[0]);
}
_normalizeOrderbook(book) {
const root = book?.orderbook && typeof book.orderbook === 'object' ? book.orderbook : book;
return {
yes: this._normalizeBookSide(root?.yes),
no: this._normalizeBookSide(root?.no)
};
}
_bestBookPrice(sideBook) {
if (!Array.isArray(sideBook) || !sideBook.length) return null;
return this._num(sideBook[0][0]);
}
_pickBestMarket(markets = []) {
const now = Date.now();
@@ -165,6 +250,7 @@ export class MarketTracker extends EventEmitter {
}
const newTicker = selectedMarket.ticker;
if (newTicker === this.currentTicker) {
this.currentEvent = selectedEvent.event_ticker || this.currentEvent;
this.marketData = { ...(this.marketData || {}), ...selectedMarket };
@@ -174,7 +260,6 @@ export class MarketTracker extends EventEmitter {
const oldTicker = this.currentTicker;
// Unsubscribe from old
if (oldTicker) {
console.log(`[Tracker] Rotating from ${oldTicker}${newTicker}`);
this.ws.unsubscribeTicker(oldTicker);
@@ -185,17 +270,23 @@ export class MarketTracker extends EventEmitter {
this.marketData = selectedMarket;
this.orderbook = { yes: [], no: [] };
// Fetch fresh orderbook via REST
try {
const ob = await getOrderbook(newTicker);
this.orderbook = ob;
const [freshMarket, ob] = await Promise.all([
getMarket(newTicker).catch(() => null),
getOrderbook(newTicker).catch(() => null)
]);
if (freshMarket) this.marketData = { ...selectedMarket, ...freshMarket };
if (ob) this.orderbook = this._normalizeOrderbook(ob);
} catch (e) {
console.error('[Tracker] Orderbook fetch error:', e.message);
console.error('[Tracker] Initial market bootstrap error:', e.message);
}
// Subscribe via WS
this.ws.subscribeTicker(newTicker);
console.log(`[Tracker] Now tracking: ${newTicker} (${selectedMarket.title || selectedMarket.subtitle || selectedEvent.event_ticker})`);
console.log(
`[Tracker] Now tracking: ${newTicker} (${this.marketData?.title || this.marketData?.subtitle || selectedEvent.event_ticker})`
);
this.emit('update', this.getState());
this.emit('market-rotated', { from: oldTicker, to: newTicker });
@@ -205,11 +296,10 @@ export class MarketTracker extends EventEmitter {
}
async _checkRotation() {
// Refresh market data via REST
if (this.currentTicker) {
try {
const fresh = await getMarket(this.currentTicker);
this.marketData = fresh;
this.marketData = { ...(this.marketData || {}), ...(fresh || {}) };
const state = this.getState();
this.emit('update', state);
@@ -217,8 +307,7 @@ export class MarketTracker extends EventEmitter {
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 (settledLike || 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;
@@ -236,11 +325,28 @@ export class MarketTracker extends EventEmitter {
if (msg.market_ticker !== this.currentTicker) return;
if (msg.type === 'orderbook_snapshot') {
this.orderbook = { yes: msg.yes || [], no: msg.no || [] };
this.orderbook = this._normalizeOrderbook(msg);
} else if (msg.type === 'orderbook_delta') {
// Apply delta updates
if (msg.yes) this.orderbook.yes = this._applyDelta(this.orderbook.yes, msg.yes);
if (msg.no) this.orderbook.no = this._applyDelta(this.orderbook.no, msg.no);
const side = String(msg.side || '').toLowerCase();
const price = this._num(msg.price);
const delta = this._num(msg.delta);
const absoluteQty = this._num(msg.qty ?? msg.quantity ?? msg.size);
if ((side === 'yes' || side === 'no') && price != null) {
const book = this.orderbook[side] || [];
const map = new Map(book);
const current = this._num(map.get(price)) ?? 0;
const next = delta != null ? current + delta : (absoluteQty ?? current);
if (next <= 0) map.delete(price);
else map.set(price, next);
this.orderbook[side] = [...map.entries()].sort((a, b) => b[0] - a[0]);
} else {
if (Array.isArray(msg.yes)) this.orderbook.yes = this._applyDelta(this.orderbook.yes, msg.yes);
if (Array.isArray(msg.no)) this.orderbook.no = this._applyDelta(this.orderbook.no, msg.no);
}
}
this.emit('update', this.getState());
@@ -249,15 +355,21 @@ export class MarketTracker extends EventEmitter {
_onTicker(msg) {
if (msg.market_ticker !== this.currentTicker) return;
// Merge ticker data into marketData
if (this.marketData) {
const yesBid = this._num(msg.yes_bid);
const yesAsk = this._num(msg.yes_ask);
const noBid = this._num(msg.no_bid);
const noAsk = this._num(msg.no_ask);
const lastPrice = this._num(msg.last_price);
const volume = this._num(msg.volume);
Object.assign(this.marketData, {
yes_bid: msg.yes_bid ?? this.marketData.yes_bid,
yes_ask: msg.yes_ask ?? this.marketData.yes_ask,
no_bid: msg.no_bid ?? this.marketData.no_bid,
no_ask: msg.no_ask ?? this.marketData.no_ask,
last_price: msg.last_price ?? this.marketData.last_price,
volume: msg.volume ?? this.marketData.volume
yes_bid: yesBid ?? this.marketData.yes_bid,
yes_ask: yesAsk ?? this.marketData.yes_ask,
no_bid: noBid ?? this.marketData.no_bid,
no_ask: noAsk ?? this.marketData.no_ask,
last_price: lastPrice ?? this.marketData.last_price,
volume: volume ?? this.marketData.volume
});
}
@@ -265,11 +377,28 @@ export class MarketTracker extends EventEmitter {
}
_applyDelta(book, deltas) {
const map = new Map(book);
for (const [price, qty] of deltas) {
if (qty === 0) map.delete(price);
else map.set(price, qty);
const map = new Map(book || []);
for (const delta of Array.isArray(deltas) ? deltas : []) {
let price = null;
let qty = null;
if (Array.isArray(delta)) {
price = delta[0];
qty = delta[1];
} else if (delta && typeof delta === 'object') {
price = delta.price ?? delta[0];
qty = delta.qty ?? delta.quantity ?? delta.size ?? delta[1];
}
const p = this._num(price);
const q = this._num(qty);
if (p == null || q == null) continue;
if (q <= 0) map.delete(p);
else map.set(p, q);
}
return [...map.entries()].sort((a, b) => a[0] - b[0]);
return [...map.entries()].sort((a, b) => b[0] - a[0]);
}
}