Fix: Use IOC instead of FoK, fix count calc

This commit is contained in:
2026-03-16 11:47:21 -07:00
parent 9577e55c95
commit b1a442e129

View File

@@ -5,7 +5,7 @@ import crypto from 'crypto';
/** /**
* Live Trading Engine — real money on Kalshi. * Live Trading Engine — real money on Kalshi.
* Uses fill_or_kill orders with buy_max_cost for safety. * Uses IOC (Immediate-or-Cancel) orders for thin-book safety.
* All amounts in CENTS internally. * All amounts in CENTS internally.
*/ */
export class LiveEngine { export class LiveEngine {
@@ -152,12 +152,20 @@ export class LiveEngine {
return null; return null;
} }
if (priceCents <= 0 || priceCents >= 100) {
console.log(`[Live] Invalid price ${priceCents}¢ — skipping`);
return null;
}
const priceInDollars = priceCents / 100; const priceInDollars = priceCents / 100;
const contracts = Math.max(1, Math.floor(sizeDollars / priceInDollars)); const contracts = Math.max(1, Math.floor(sizeDollars / priceInDollars));
const clientOrderId = crypto.randomUUID(); const clientOrderId = crypto.randomUUID();
const side = signal.side.toLowerCase(); const side = signal.side.toLowerCase();
// Build order with IOC (Immediate-or-Cancel) instead of FoK via buy_max_cost.
// IOC partially fills whatever is available and cancels the rest,
// so thin order books won't cause a hard 409 rejection.
const orderBody = { const orderBody = {
ticker: signal.ticker, ticker: signal.ticker,
action: 'buy', action: 'buy',
@@ -165,7 +173,7 @@ export class LiveEngine {
count: contracts, count: contracts,
type: 'limit', type: 'limit',
client_order_id: clientOrderId, client_order_id: clientOrderId,
buy_max_cost: costCents, time_in_force: 'ioc',
}; };
if (side === 'yes') { if (side === 'yes') {
@@ -175,7 +183,7 @@ export class LiveEngine {
} }
try { try {
console.log(`[Live] Placing order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) | ${signal.reason}`); console.log(`[Live] Placing IOC order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) | ${signal.reason}`);
const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody); const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody);
const order = result?.order; const order = result?.order;
@@ -185,6 +193,16 @@ export class LiveEngine {
return null; return null;
} }
const fillCount = order.taker_fill_count || 0;
const fillCost = order.taker_fill_cost || 0;
const status = (order.status || '').toLowerCase();
// IOC with zero fills means nothing was available at our price
if (fillCount === 0 && (status === 'canceled' || status === 'cancelled')) {
console.log(`[Live] IOC got 0 fills — no liquidity at ${priceCents}¢ for ${signal.ticker}`);
return null;
}
const orderRecord = { const orderRecord = {
orderId: order.order_id, orderId: order.order_id,
clientOrderId, clientOrderId,
@@ -192,30 +210,28 @@ export class LiveEngine {
ticker: signal.ticker, ticker: signal.ticker,
side, side,
priceCents, priceCents,
contracts, contracts: fillCount || contracts,
costCents, costCents: fillCost || costCents,
reason: signal.reason, reason: signal.reason,
status: order.status || 'pending', status: fillCount > 0 ? 'filled' : (order.status || 'pending'),
createdAt: Date.now(), createdAt: Date.now(),
settled: false, settled: false,
result: null, result: null,
pnl: null, pnl: null,
fillCount: order.taker_fill_count || 0, fillCount,
fillCost: order.taker_fill_cost || 0, fillCost,
marketState: { marketState: {
yesPct: marketState.yesPct, yesPct: marketState.yesPct,
noPct: marketState.noPct noPct: marketState.noPct
} }
}; };
if (order.status === 'executed' || order.status === 'filled') { if (fillCount > 0) {
orderRecord.status = 'filled';
this.totalTrades++; this.totalTrades++;
} else if (order.status === 'canceled' || order.status === 'cancelled') { }
orderRecord.status = 'cancelled';
console.log(`[Live] Order cancelled (likely FoK not filled): ${order.order_id}`); // Only track as open if we actually got fills that need settling
return null; if (fillCount > 0) {
} else {
this.openOrders.set(order.order_id, orderRecord); this.openOrders.set(order.order_id, orderRecord);
} }
@@ -225,7 +241,7 @@ export class LiveEngine {
console.error('[Live] DB write error:', e.message); console.error('[Live] DB write error:', e.message);
} }
const msg = `💰 LIVE [${signal.strategy}] ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) | ${signal.reason}`; const msg = `💰 LIVE [${signal.strategy}] ${side.toUpperCase()} ${fillCount}x @ ${priceCents}¢ ($${(fillCost/100).toFixed(2)}) | ${signal.reason}`;
console.log(`[Live] ${msg}`); console.log(`[Live] ${msg}`);
await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings'); await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings');