Fix: settle delayed live results via orphan scan

This commit is contained in:
2026-03-16 13:51:41 -07:00
parent 2b9f2c5c2b
commit 1c8dec1f17

View File

@@ -13,7 +13,7 @@ export class LiveEngine {
constructor() { constructor() {
this.enabledStrategies = new Set(); this.enabledStrategies = new Set();
this.openOrders = new Map(); this.openOrders = new Map();
this.recentFills =[]; this.recentFills = [];
this.totalPnL = 0; this.totalPnL = 0;
this.wins = 0; this.wins = 0;
this.losses = 0; this.losses = 0;
@@ -25,7 +25,7 @@ export class LiveEngine {
this._dailyLossResetTime = Date.now(); this._dailyLossResetTime = Date.now();
this._lastBalance = null; this._lastBalance = null;
this._lastPortfolioValue = null; this._lastPortfolioValue = null;
this._positions =[]; this._positions = [];
} }
async init() { async init() {
@@ -42,14 +42,16 @@ export class LiveEngine {
if (row.enabledStrategies) { if (row.enabledStrategies) {
for (const s of row.enabledStrategies) this.enabledStrategies.add(s); for (const s of row.enabledStrategies) this.enabledStrategies.add(s);
} }
console.log(`[Live] Restored: PnL=$${(this.totalPnL/100).toFixed(2)}, ${this.wins}W/${this.losses}L`); console.log(
`[Live] Restored: PnL=$${(this.totalPnL / 100).toFixed(2)}, ${this.wins}W/${this.losses}L`
);
} }
const orders = await db.query( const orders = await db.query(
'SELECT * FROM live_orders WHERE status = "pending" OR status = "resting"' 'SELECT * FROM live_orders WHERE status = "pending" OR status = "resting" OR status = "filled"'
); );
for (const o of (orders[0] ||[])) { for (const o of orders[0] || []) {
this.openOrders.set(o.orderId, o); if (!o.settled) this.openOrders.set(o.orderId, o);
} }
if (this.openOrders.size) { if (this.openOrders.size) {
console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`); console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`);
@@ -115,8 +117,8 @@ export class LiveEngine {
'GET', 'GET',
'/trade-api/v2/portfolio/positions?settlement_status=unsettled&limit=200' '/trade-api/v2/portfolio/positions?settlement_status=unsettled&limit=200'
); );
const positions = data?.market_positions || data?.positions ||[]; const positions = data?.market_positions || data?.positions || [];
this._positions = Array.isArray(positions) ? positions :[]; this._positions = Array.isArray(positions) ? positions : [];
return this._positions; return this._positions;
} catch (e) { } catch (e) {
console.error('[Live] Positions fetch error:', e.message); console.error('[Live] Positions fetch error:', e.message);
@@ -161,7 +163,7 @@ export class LiveEngine {
*/ */
async _verifyOrderFills(orderId, maxAttempts = 3) { async _verifyOrderFills(orderId, maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
if (i > 0) await new Promise(r => setTimeout(r, 500 * i)); if (i > 0) await new Promise((r) => setTimeout(r, 500 * i));
try { try {
const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`); const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`);
const order = data?.order; const order = data?.order;
@@ -193,11 +195,15 @@ export class LiveEngine {
this._resetDailyLossIfNeeded(); this._resetDailyLossIfNeeded();
if (this._dailyLoss >= this._maxDailyLossCents) { if (this._dailyLoss >= this._maxDailyLossCents) {
console.log(`[Live] Daily loss limit ($${(this._maxDailyLossCents/100).toFixed(2)}) reached — pausing`); console.log(
`[Live] Daily loss limit ($${(this._maxDailyLossCents / 100).toFixed(2)}) reached — pausing`
);
this.pause(); this.pause();
await notify( await notify(
`⚠️ Daily loss limit reached ($${(this._dailyLoss/100).toFixed(2)}). Auto-paused.`, `⚠️ Daily loss limit reached ($${(this._dailyLoss / 100).toFixed(2)}). Auto-paused.`,
'Kalbot Safety', 'urgent', 'warning,octagonal_sign' 'Kalbot Safety',
'urgent',
'warning,octagonal_sign'
); );
return null; return null;
} }
@@ -213,14 +219,18 @@ export class LiveEngine {
// SAFETY: Require orderbook data before placing real money orders // SAFETY: Require orderbook data before placing real money orders
const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook); const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook);
if (bestAsk == null) { if (bestAsk == null) {
console.log(`[Live] No orderbook data for ${signal.side} side — refusing to trade blind (${signal.strategy})`); console.log(
`[Live] No orderbook data for ${signal.side} side — refusing to trade blind (${signal.strategy})`
);
return null; return null;
} }
const maxAcceptable = signal.maxPrice || (signal.price + 3); const maxAcceptable = signal.maxPrice || signal.price + 3;
if (bestAsk > maxAcceptable) { if (bestAsk > maxAcceptable) {
console.log(`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`); console.log(
`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`
);
return null; return null;
} }
@@ -244,7 +254,7 @@ export class LiveEngine {
count: contracts, count: contracts,
type: 'limit', type: 'limit',
client_order_id: clientOrderId, client_order_id: clientOrderId,
time_in_force: 'immediate_or_cancel', time_in_force: 'immediate_or_cancel'
}; };
if (side === 'yes') { if (side === 'yes') {
@@ -254,7 +264,9 @@ export class LiveEngine {
} }
try { 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 result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody);
const order = result?.order; const order = result?.order;
@@ -277,7 +289,9 @@ export class LiveEngine {
fillCount = verified.fillCount; fillCount = verified.fillCount;
fillCost = verified.fillCost; fillCost = verified.fillCost;
status = verified.status; status = verified.status;
console.log(`[Live] Verified: ${fillCount} fills, $${(fillCost/100).toFixed(2)} cost, status: ${status}`); console.log(
`[Live] Verified: ${fillCount} fills, $${(fillCost / 100).toFixed(2)} cost, status: ${status}`
);
} }
if (fillCount === 0) { if (fillCount === 0) {
@@ -320,7 +334,9 @@ 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()} ${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}`); console.log(`[Live] ${msg}`);
await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings'); await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings');
@@ -330,7 +346,9 @@ export class LiveEngine {
console.error(`[Live] Order failed: ${e.message}`); console.error(`[Live] Order failed: ${e.message}`);
await notify( await notify(
`❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`, `❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`,
'Kalbot Error', 'urgent', 'x,warning' 'Kalbot Error',
'urgent',
'x,warning'
); );
return null; return null;
} }
@@ -373,9 +391,16 @@ export class LiveEngine {
} }
const emoji = won ? '✅' : '❌'; const emoji = won ? '✅' : '❌';
const msg = `${emoji} LIVE [${order.strategy}] ${order.side.toUpperCase()} ${won ? 'WON' : 'LOST'} | PnL: $${(pnl/100).toFixed(2)}`; const msg = `${emoji} LIVE [${order.strategy}] ${order.side.toUpperCase()} ${
won ? 'WON' : 'LOST'
} | PnL: $${(pnl / 100).toFixed(2)}`;
console.log(`[Live] ${msg}`); console.log(`[Live] ${msg}`);
await notify(msg, won ? 'Live Win!' : 'Live Loss', 'high', won ? 'chart_with_upwards_trend' : 'chart_with_downwards_trend'); await notify(
msg,
won ? 'Live Win!' : 'Live Loss',
'high',
won ? 'chart_with_upwards_trend' : 'chart_with_downwards_trend'
);
settled.push(order); settled.push(order);
this.openOrders.delete(orderId); this.openOrders.delete(orderId);
@@ -385,6 +410,34 @@ export class LiveEngine {
return settled.length ? settled : null; return settled.length ? settled : null;
} }
/**
* Recover live orders when market rotates before result is available.
* Polls market endpoints for delayed settlement result and settles locally when it appears.
*/
async checkOrphans(getMarketFn) {
const tickers = this.getOpenTickers();
if (!tickers.length) return [];
const settled = [];
for (const ticker of tickers) {
try {
const market = await getMarketFn(ticker);
const result = String(market?.result || '').toLowerCase();
if (result === 'yes' || result === 'no') {
console.log(`[Live] Delayed settlement found for ${ticker}: ${result}`);
const done = await this.settle(ticker, result);
if (done?.length) settled.push(...done);
}
} catch (e) {
console.error(`[Live] Orphan check failed for ${ticker}:`, e.message);
}
}
return settled;
}
getOpenTickers() { getOpenTickers() {
const tickers = new Set(); const tickers = new Set();
for (const [, order] of this.openOrders) { for (const [, order] of this.openOrders) {
@@ -406,12 +459,13 @@ export class LiveEngine {
if (!o.settled) openList.push(o); if (!o.settled) openList.push(o);
} }
return { return {
balance: this._lastBalance != null ? (this._lastBalance / 100) : null, balance: this._lastBalance != null ? this._lastBalance / 100 : null,
portfolioValue: this._lastPortfolioValue != null ? (this._lastPortfolioValue / 100) : null, portfolioValue: this._lastPortfolioValue != null ? this._lastPortfolioValue / 100 : null,
totalPnL: parseFloat((this.totalPnL / 100).toFixed(2)), totalPnL: parseFloat((this.totalPnL / 100).toFixed(2)),
wins: this.wins, wins: this.wins,
losses: this.losses, losses: this.losses,
winRate: this.wins + this.losses > 0 winRate:
this.wins + this.losses > 0
? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1)) ? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1))
: 0, : 0,
totalTrades: this.totalTrades, totalTrades: this.totalTrades,