mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-17 05:51:02 +00:00
Fix: robust quote/orderbook parsing
This commit is contained in:
@@ -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];
|
||||
}
|
||||
return [...map.entries()].sort((a, b) => a[0] - b[0]);
|
||||
|
||||
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) => b[0] - a[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user