mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Fix: Use IOC instead of FoK, fix count calc
This commit is contained in:
@@ -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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user