From 4553a82b0d54f36f70a0fe7abd5766e2641bf52f Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Mon, 16 Mar 2026 12:58:07 -0700 Subject: [PATCH] Fix: Verify fills async, reject blind orders, track ghost fills --- lib/live/engine.js | 97 ++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/lib/live/engine.js b/lib/live/engine.js index ad62e1a..e11f5ec 100644 --- a/lib/live/engine.js +++ b/lib/live/engine.js @@ -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');