mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Fix: Handle dollar-string WS fields, fix orderbook
This commit is contained in:
@@ -5,6 +5,17 @@ import { EventEmitter } from 'events';
|
|||||||
const OPEN_MARKET_STATUSES = new Set(['open', 'active', 'initialized', 'trading']);
|
const OPEN_MARKET_STATUSES = new Set(['open', 'active', 'initialized', 'trading']);
|
||||||
const TRADABLE_MARKET_STATUSES = new Set(['open', 'active', 'trading']);
|
const TRADABLE_MARKET_STATUSES = new Set(['open', 'active', 'trading']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a dollar string like "0.4200" to cents integer (42).
|
||||||
|
* Returns null if not parseable.
|
||||||
|
*/
|
||||||
|
function dollarsToCents(val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
const n = Number(val);
|
||||||
|
if (!Number.isFinite(n)) return null;
|
||||||
|
return Math.round(n * 100);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the currently active BTC 15-min market.
|
* Tracks the currently active BTC 15-min market.
|
||||||
* Auto-rotates when the current market expires.
|
* Auto-rotates when the current market expires.
|
||||||
@@ -122,18 +133,25 @@ export class MarketTracker extends EventEmitter {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
// Try cents first (from REST API), then dollar-string fields (from WS ticker)
|
||||||
yesBid: pick('yes_bid', 'yesBid'),
|
let yesBid = pick('yes_bid', 'yesBid');
|
||||||
yesAsk: pick('yes_ask', 'yesAsk'),
|
let yesAsk = pick('yes_ask', 'yesAsk');
|
||||||
noBid: pick('no_bid', 'noBid'),
|
let noBid = pick('no_bid', 'noBid');
|
||||||
noAsk: pick('no_ask', 'noAsk'),
|
let noAsk = pick('no_ask', 'noAsk');
|
||||||
lastPrice: pick('last_price', 'lastPrice', 'yes_price', 'yesPrice')
|
let lastPrice = pick('last_price', 'lastPrice', 'yes_price', 'yesPrice');
|
||||||
};
|
|
||||||
|
// WS ticker sends dollar strings — convert to cents
|
||||||
|
if (yesBid == null) yesBid = dollarsToCents(market?.yes_bid_dollars);
|
||||||
|
if (yesAsk == null) yesAsk = dollarsToCents(market?.yes_ask_dollars);
|
||||||
|
if (noBid == null) noBid = dollarsToCents(market?.no_bid_dollars);
|
||||||
|
if (noAsk == null) noAsk = dollarsToCents(market?.no_ask_dollars);
|
||||||
|
if (lastPrice == null) lastPrice = dollarsToCents(market?.price_dollars);
|
||||||
|
|
||||||
|
return { yesBid, yesAsk, noBid, noAsk, lastPrice };
|
||||||
}
|
}
|
||||||
|
|
||||||
_normalizeBookSide(levels) {
|
_normalizeBookSide(levels) {
|
||||||
if (!Array.isArray(levels)) return [];
|
if (!Array.isArray(levels)) return [];
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
||||||
for (const level of levels) {
|
for (const level of levels) {
|
||||||
@@ -141,18 +159,34 @@ export class MarketTracker extends EventEmitter {
|
|||||||
let qty = null;
|
let qty = null;
|
||||||
|
|
||||||
if (Array.isArray(level)) {
|
if (Array.isArray(level)) {
|
||||||
price = level[0];
|
// New API format: ["0.4200", "300.00"] (dollar strings)
|
||||||
qty = level[1];
|
// Or old format: [42, 300] (cents integers)
|
||||||
|
const rawPrice = level[0];
|
||||||
|
const rawQty = level[1];
|
||||||
|
|
||||||
|
// Detect dollar-string format (contains a decimal point and is < 1.01)
|
||||||
|
if (typeof rawPrice === 'string' && rawPrice.includes('.')) {
|
||||||
|
price = dollarsToCents(rawPrice);
|
||||||
|
qty = this._num(rawQty);
|
||||||
|
} else {
|
||||||
|
price = this._num(rawPrice);
|
||||||
|
qty = this._num(rawQty);
|
||||||
|
}
|
||||||
} else if (level && typeof level === 'object') {
|
} else if (level && typeof level === 'object') {
|
||||||
price = level.price ?? level[0];
|
// Object format
|
||||||
qty = level.qty ?? level.quantity ?? level.size ?? level.count ?? level[1];
|
const rawPrice = level.price ?? level.price_dollars ?? level[0];
|
||||||
|
const rawQty = level.qty ?? level.quantity ?? level.size ?? level.count ?? level[1];
|
||||||
|
|
||||||
|
if (typeof rawPrice === 'string' && rawPrice.includes('.')) {
|
||||||
|
price = dollarsToCents(rawPrice);
|
||||||
|
} else {
|
||||||
|
price = this._num(rawPrice);
|
||||||
|
}
|
||||||
|
qty = this._num(rawQty);
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = this._num(price);
|
if (price == null || qty == null || qty <= 0) continue;
|
||||||
const q = this._num(qty);
|
out.push([price, qty]);
|
||||||
|
|
||||||
if (p == null || q == null || q <= 0) continue;
|
|
||||||
out.push([p, q]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out.sort((a, b) => b[0] - a[0]);
|
return out.sort((a, b) => b[0] - a[0]);
|
||||||
@@ -161,8 +195,9 @@ export class MarketTracker extends EventEmitter {
|
|||||||
_normalizeOrderbook(book) {
|
_normalizeOrderbook(book) {
|
||||||
const root = book?.orderbook && typeof book.orderbook === 'object' ? book.orderbook : book;
|
const root = book?.orderbook && typeof book.orderbook === 'object' ? book.orderbook : book;
|
||||||
return {
|
return {
|
||||||
yes: this._normalizeBookSide(root?.yes),
|
// Support both old fields (yes/no) and new fields (yes_dollars_fp/no_dollars_fp)
|
||||||
no: this._normalizeBookSide(root?.no)
|
yes: this._normalizeBookSide(root?.yes ?? root?.yes_dollars_fp ?? root?.yes_dollars),
|
||||||
|
no: this._normalizeBookSide(root?.no ?? root?.no_dollars_fp ?? root?.no_dollars)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,11 +360,19 @@ export class MarketTracker extends EventEmitter {
|
|||||||
if (msg.market_ticker !== this.currentTicker) return;
|
if (msg.market_ticker !== this.currentTicker) return;
|
||||||
|
|
||||||
if (msg.type === 'orderbook_snapshot') {
|
if (msg.type === 'orderbook_snapshot') {
|
||||||
|
// New format: yes_dollars_fp / no_dollars_fp
|
||||||
this.orderbook = this._normalizeOrderbook(msg);
|
this.orderbook = this._normalizeOrderbook(msg);
|
||||||
|
console.log(`[Tracker] Orderbook snapshot: ${this.orderbook.yes.length} yes levels, ${this.orderbook.no.length} no levels`);
|
||||||
} else if (msg.type === 'orderbook_delta') {
|
} else if (msg.type === 'orderbook_delta') {
|
||||||
const side = String(msg.side || '').toLowerCase();
|
const side = String(msg.side || '').toLowerCase();
|
||||||
const price = this._num(msg.price);
|
|
||||||
const delta = this._num(msg.delta);
|
// New format uses price_dollars + delta_fp
|
||||||
|
let price = this._num(msg.price);
|
||||||
|
if (price == null) price = dollarsToCents(msg.price_dollars);
|
||||||
|
|
||||||
|
let delta = this._num(msg.delta);
|
||||||
|
if (delta == null) delta = this._num(msg.delta_fp);
|
||||||
|
|
||||||
const absoluteQty = this._num(msg.qty ?? msg.quantity ?? msg.size);
|
const absoluteQty = this._num(msg.qty ?? msg.quantity ?? msg.size);
|
||||||
|
|
||||||
if ((side === 'yes' || side === 'no') && price != null) {
|
if ((side === 'yes' || side === 'no') && price != null) {
|
||||||
@@ -344,8 +387,11 @@ export class MarketTracker extends EventEmitter {
|
|||||||
|
|
||||||
this.orderbook[side] = [...map.entries()].sort((a, b) => b[0] - a[0]);
|
this.orderbook[side] = [...map.entries()].sort((a, b) => b[0] - a[0]);
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(msg.yes)) this.orderbook.yes = this._applyDelta(this.orderbook.yes, msg.yes);
|
// Batch delta arrays (old format fallback)
|
||||||
if (Array.isArray(msg.no)) this.orderbook.no = this._applyDelta(this.orderbook.no, msg.no);
|
const yesArr = msg.yes ?? msg.yes_dollars_fp;
|
||||||
|
const noArr = msg.no ?? msg.no_dollars_fp;
|
||||||
|
if (Array.isArray(yesArr)) this.orderbook.yes = this._applyDelta(this.orderbook.yes, yesArr);
|
||||||
|
if (Array.isArray(noArr)) this.orderbook.no = this._applyDelta(this.orderbook.no, noArr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,21 +402,22 @@ export class MarketTracker extends EventEmitter {
|
|||||||
if (msg.market_ticker !== this.currentTicker) return;
|
if (msg.market_ticker !== this.currentTicker) return;
|
||||||
|
|
||||||
if (this.marketData) {
|
if (this.marketData) {
|
||||||
const yesBid = this._num(msg.yes_bid);
|
// New API sends dollar-string fields; store them for _extractMarketQuotes
|
||||||
const yesAsk = this._num(msg.yes_ask);
|
const fields = [
|
||||||
const noBid = this._num(msg.no_bid);
|
'yes_bid', 'yes_ask', 'no_bid', 'no_ask', 'last_price', 'volume',
|
||||||
const noAsk = this._num(msg.no_ask);
|
'yes_bid_dollars', 'yes_ask_dollars', 'no_bid_dollars', 'no_ask_dollars',
|
||||||
const lastPrice = this._num(msg.last_price);
|
'price_dollars', 'volume_fp', 'open_interest_fp',
|
||||||
const volume = this._num(msg.volume);
|
'dollar_volume', 'dollar_open_interest'
|
||||||
|
];
|
||||||
|
|
||||||
Object.assign(this.marketData, {
|
for (const key of fields) {
|
||||||
yes_bid: yesBid ?? this.marketData.yes_bid,
|
if (msg[key] != null) this.marketData[key] = msg[key];
|
||||||
yes_ask: yesAsk ?? this.marketData.yes_ask,
|
}
|
||||||
no_bid: noBid ?? this.marketData.no_bid,
|
|
||||||
no_ask: noAsk ?? this.marketData.no_ask,
|
// Also map dollar_volume / dollar_open_interest to standard fields
|
||||||
last_price: lastPrice ?? this.marketData.last_price,
|
if (msg.dollar_volume != null) this.marketData.volume = this._num(msg.dollar_volume) ?? this.marketData.volume;
|
||||||
volume: volume ?? this.marketData.volume
|
if (msg.dollar_open_interest != null) this.marketData.open_interest = this._num(msg.dollar_open_interest) ?? this.marketData.open_interest;
|
||||||
});
|
if (msg.volume_fp != null && this.marketData.volume == null) this.marketData.volume = this._num(msg.volume_fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('update', this.getState());
|
this.emit('update', this.getState());
|
||||||
@@ -384,19 +431,28 @@ export class MarketTracker extends EventEmitter {
|
|||||||
let qty = null;
|
let qty = null;
|
||||||
|
|
||||||
if (Array.isArray(delta)) {
|
if (Array.isArray(delta)) {
|
||||||
price = delta[0];
|
const rawPrice = delta[0];
|
||||||
qty = delta[1];
|
const rawQty = delta[1];
|
||||||
|
if (typeof rawPrice === 'string' && rawPrice.includes('.')) {
|
||||||
|
price = dollarsToCents(rawPrice);
|
||||||
|
} else {
|
||||||
|
price = this._num(rawPrice);
|
||||||
|
}
|
||||||
|
qty = this._num(rawQty);
|
||||||
} else if (delta && typeof delta === 'object') {
|
} else if (delta && typeof delta === 'object') {
|
||||||
price = delta.price ?? delta[0];
|
const rawPrice = delta.price ?? delta.price_dollars ?? delta[0];
|
||||||
qty = delta.qty ?? delta.quantity ?? delta.size ?? delta[1];
|
const rawQty = delta.qty ?? delta.quantity ?? delta.size ?? delta[1];
|
||||||
|
if (typeof rawPrice === 'string' && rawPrice.includes('.')) {
|
||||||
|
price = dollarsToCents(rawPrice);
|
||||||
|
} else {
|
||||||
|
price = this._num(rawPrice);
|
||||||
|
}
|
||||||
|
qty = this._num(rawQty);
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = this._num(price);
|
if (price == null || qty == null) continue;
|
||||||
const q = this._num(qty);
|
if (qty <= 0) map.delete(price);
|
||||||
if (p == null || q == null) continue;
|
else map.set(price, qty);
|
||||||
|
|
||||||
if (q <= 0) map.delete(p);
|
|
||||||
else map.set(p, q);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...map.entries()].sort((a, b) => b[0] - a[0]);
|
return [...map.entries()].sort((a, b) => b[0] - a[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user