diff --git a/lib/live/engine.js b/lib/live/engine.js index 91f8ddd..f0417df 100644 --- a/lib/live/engine.js +++ b/lib/live/engine.js @@ -13,7 +13,7 @@ export class LiveEngine { constructor() { this.enabledStrategies = new Set(); this.openOrders = new Map(); - this.recentFills =[]; + this.recentFills = []; this.totalPnL = 0; this.wins = 0; this.losses = 0; @@ -25,7 +25,7 @@ export class LiveEngine { this._dailyLossResetTime = Date.now(); this._lastBalance = null; this._lastPortfolioValue = null; - this._positions =[]; + this._positions = []; } async init() { @@ -42,14 +42,16 @@ export class LiveEngine { if (row.enabledStrategies) { 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( - '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] ||[])) { - this.openOrders.set(o.orderId, o); + for (const o of orders[0] || []) { + if (!o.settled) this.openOrders.set(o.orderId, o); } if (this.openOrders.size) { console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`); @@ -115,8 +117,8 @@ export class LiveEngine { 'GET', '/trade-api/v2/portfolio/positions?settlement_status=unsettled&limit=200' ); - const positions = data?.market_positions || data?.positions ||[]; - this._positions = Array.isArray(positions) ? positions :[]; + const positions = data?.market_positions || data?.positions || []; + this._positions = Array.isArray(positions) ? positions : []; return this._positions; } catch (e) { console.error('[Live] Positions fetch error:', e.message); @@ -161,7 +163,7 @@ export class LiveEngine { */ async _verifyOrderFills(orderId, maxAttempts = 3) { 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 { const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`); const order = data?.order; @@ -193,11 +195,15 @@ export class LiveEngine { this._resetDailyLossIfNeeded(); 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(); await notify( - `⚠️ Daily loss limit reached ($${(this._dailyLoss/100).toFixed(2)}). Auto-paused.`, - 'Kalbot Safety', 'urgent', 'warning,octagonal_sign' + `⚠️ Daily loss limit reached ($${(this._dailyLoss / 100).toFixed(2)}). Auto-paused.`, + 'Kalbot Safety', + 'urgent', + 'warning,octagonal_sign' ); return null; } @@ -213,14 +219,18 @@ export class LiveEngine { // SAFETY: Require orderbook data before placing real money orders const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook); 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; } - const maxAcceptable = signal.maxPrice || (signal.price + 3); + const maxAcceptable = signal.maxPrice || signal.price + 3; 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; } @@ -244,7 +254,7 @@ export class LiveEngine { count: contracts, type: 'limit', client_order_id: clientOrderId, - time_in_force: 'immediate_or_cancel', + time_in_force: 'immediate_or_cancel' }; if (side === 'yes') { @@ -254,7 +264,9 @@ 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; @@ -277,7 +289,9 @@ export class LiveEngine { fillCount = verified.fillCount; fillCost = verified.fillCost; 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) { @@ -320,7 +334,9 @@ 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'); @@ -330,7 +346,9 @@ export class LiveEngine { console.error(`[Live] Order failed: ${e.message}`); await notify( `❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`, - 'Kalbot Error', 'urgent', 'x,warning' + 'Kalbot Error', + 'urgent', + 'x,warning' ); return null; } @@ -373,9 +391,16 @@ export class LiveEngine { } 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}`); - 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); this.openOrders.delete(orderId); @@ -385,6 +410,34 @@ export class LiveEngine { 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() { const tickers = new Set(); for (const [, order] of this.openOrders) { @@ -406,14 +459,15 @@ export class LiveEngine { if (!o.settled) openList.push(o); } return { - balance: this._lastBalance != null ? (this._lastBalance / 100) : null, - portfolioValue: this._lastPortfolioValue != null ? (this._lastPortfolioValue / 100) : null, + balance: this._lastBalance != null ? this._lastBalance / 100 : null, + portfolioValue: this._lastPortfolioValue != null ? this._lastPortfolioValue / 100 : null, totalPnL: parseFloat((this.totalPnL / 100).toFixed(2)), wins: this.wins, losses: this.losses, - winRate: this.wins + this.losses > 0 - ? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1)) - : 0, + winRate: + this.wins + this.losses > 0 + ? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1)) + : 0, totalTrades: this.totalTrades, openOrders: openList, paused: this._paused,