mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Fix: Verify fills async, reject blind orders, track ghost fills
This commit is contained in:
@@ -133,13 +133,11 @@ export class LiveEngine {
|
||||
if (!orderbook) return null;
|
||||
|
||||
if (side === 'yes') {
|
||||
// Best yes ask = 100 - highest no bid
|
||||
if (orderbook.no?.length) {
|
||||
const bestNoBid = orderbook.no[0]?.[0];
|
||||
if (bestNoBid != null) return 100 - bestNoBid;
|
||||
}
|
||||
} else {
|
||||
// Best no ask = 100 - highest yes bid
|
||||
if (orderbook.yes?.length) {
|
||||
const bestYesBid = orderbook.yes[0]?.[0];
|
||||
if (bestYesBid != null) return 100 - bestYesBid;
|
||||
@@ -148,6 +146,33 @@ export class LiveEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an order's actual fill status by polling Kalshi.
|
||||
* IOC responses can report 0 fills even when fills happened async.
|
||||
*/
|
||||
async _verifyOrderFills(orderId, maxAttempts = 3) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
if (i > 0) await new Promise(r => setTimeout(r, 500 * i));
|
||||
try {
|
||||
const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`);
|
||||
const order = data?.order;
|
||||
if (!order) continue;
|
||||
|
||||
const fillCount = order.taker_fill_count || order.fill_count || 0;
|
||||
const fillCost = order.taker_fill_cost || order.fill_cost || 0;
|
||||
const status = (order.status || '').toLowerCase();
|
||||
const isFinal = ['canceled', 'cancelled', 'executed', 'filled', 'closed'].includes(status);
|
||||
|
||||
if (isFinal || fillCount > 0) {
|
||||
return { fillCount, fillCost, status, order };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Live] Verify attempt ${i + 1} failed:`, e.message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async executeTrade(signal, marketState) {
|
||||
if (this._paused) {
|
||||
console.log(`[Live] PAUSED — ignoring signal from ${signal.strategy}`);
|
||||
@@ -176,23 +201,21 @@ export class LiveEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine actual execution price from orderbook
|
||||
// SAFETY: Require orderbook data before placing real money orders
|
||||
const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook);
|
||||
const maxAcceptable = signal.maxPrice || (signal.price + 3);
|
||||
|
||||
let execPrice;
|
||||
if (bestAsk != null) {
|
||||
if (bestAsk > maxAcceptable) {
|
||||
console.log(`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`);
|
||||
return null;
|
||||
}
|
||||
execPrice = bestAsk;
|
||||
} else {
|
||||
// No orderbook data, use signal price + small buffer
|
||||
execPrice = Math.min(signal.price + 1, maxAcceptable);
|
||||
if (bestAsk == null) {
|
||||
console.log(`[Live] No orderbook data for ${signal.side} side — refusing to trade blind (${signal.strategy})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const priceCents = Math.round(execPrice);
|
||||
const maxAcceptable = signal.maxPrice || (signal.price + 3);
|
||||
|
||||
if (bestAsk > maxAcceptable) {
|
||||
console.log(`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const priceCents = Math.round(bestAsk);
|
||||
|
||||
if (priceCents <= 0 || priceCents >= 100) {
|
||||
console.log(`[Live] Invalid price ${priceCents}¢ — skipping`);
|
||||
@@ -222,7 +245,7 @@ export class LiveEngine {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Live] Placing IOC order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) [ask: ${bestAsk ?? '?'}¢, max: ${maxAcceptable}¢] | ${signal.reason}`);
|
||||
console.log(`[Live] Placing IOC order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) [ask: ${bestAsk}¢, max: ${maxAcceptable}¢] | ${signal.reason}`);
|
||||
|
||||
const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody);
|
||||
const order = result?.order;
|
||||
@@ -232,13 +255,26 @@ export class LiveEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fillCount = order.taker_fill_count || 0;
|
||||
const fillCost = order.taker_fill_cost || 0;
|
||||
const status = (order.status || '').toLowerCase();
|
||||
let fillCount = order.taker_fill_count || 0;
|
||||
let fillCost = order.taker_fill_cost || 0;
|
||||
let status = (order.status || '').toLowerCase();
|
||||
|
||||
if (fillCount === 0 && (status === 'canceled' || status === 'cancelled')) {
|
||||
console.log(`[Live] IOC got 0 fills — no liquidity at ${priceCents}¢ for ${signal.ticker}`);
|
||||
return null;
|
||||
// If immediate response says 0 fills, verify with Kalshi to catch async fills
|
||||
if (fillCount === 0) {
|
||||
console.log(`[Live] Immediate response: 0 fills (status: ${status}). Verifying with Kalshi...`);
|
||||
const verified = await this._verifyOrderFills(order.order_id);
|
||||
|
||||
if (verified) {
|
||||
fillCount = verified.fillCount;
|
||||
fillCost = verified.fillCost;
|
||||
status = verified.status;
|
||||
console.log(`[Live] Verified: ${fillCount} fills, $${(fillCost/100).toFixed(2)} cost, status: ${status}`);
|
||||
}
|
||||
|
||||
if (fillCount === 0) {
|
||||
console.log(`[Live] Confirmed 0 fills for ${signal.ticker} @ ${priceCents}¢ — no liquidity`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const orderRecord = {
|
||||
@@ -248,10 +284,10 @@ export class LiveEngine {
|
||||
ticker: signal.ticker,
|
||||
side,
|
||||
priceCents,
|
||||
contracts: fillCount || contracts,
|
||||
contracts: fillCount,
|
||||
costCents: fillCost || costCents,
|
||||
reason: signal.reason,
|
||||
status: fillCount > 0 ? 'filled' : (order.status || 'pending'),
|
||||
status: 'filled',
|
||||
createdAt: Date.now(),
|
||||
settled: false,
|
||||
result: null,
|
||||
@@ -266,13 +302,8 @@ export class LiveEngine {
|
||||
}
|
||||
};
|
||||
|
||||
if (fillCount > 0) {
|
||||
this.totalTrades++;
|
||||
}
|
||||
|
||||
if (fillCount > 0) {
|
||||
this.openOrders.set(order.order_id, orderRecord);
|
||||
}
|
||||
this.totalTrades++;
|
||||
this.openOrders.set(order.order_id, orderRecord);
|
||||
|
||||
try {
|
||||
await db.create('live_orders', orderRecord);
|
||||
@@ -280,7 +311,7 @@ export class LiveEngine {
|
||||
console.error('[Live] DB write error:', e.message);
|
||||
}
|
||||
|
||||
const msg = `💰 LIVE [${signal.strategy}] ${side.toUpperCase()} ${fillCount}x @ ${priceCents}¢ ($${(fillCost/100).toFixed(2)}) [ask:${bestAsk ?? '?'}¢] | ${signal.reason}`;
|
||||
const msg = `💰 LIVE [${signal.strategy}] ${side.toUpperCase()} ${fillCount}x @ ${priceCents}¢ ($${(fillCost/100).toFixed(2)}) [ask:${bestAsk}¢] | ${signal.reason}`;
|
||||
console.log(`[Live] ${msg}`);
|
||||
await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user