import { kalshiFetch } from '../kalshi/rest.js'; import { db } from '../db.js'; import { notify } from '../notify.js'; import crypto from 'crypto'; /** * Live Trading Engine — real money on Kalshi. * Uses fill_or_kill orders with buy_max_cost for safety. * All amounts in CENTS internally. */ export class LiveEngine { constructor() { this.enabledStrategies = new Set(); this.openOrders = new Map(); // orderId -> orderInfo this.recentFills = []; this.totalPnL = 0; this.wins = 0; this.losses = 0; this.totalTrades = 0; this._paused = false; this._maxLossPerTradeCents = 500; // $5 safety cap per trade this._maxDailyLossCents = 2000; // $20 daily loss limit this._dailyLoss = 0; this._dailyLossResetTime = Date.now(); this._lastBalance = null; this._lastPortfolioValue = null; this._positions = []; } async init() { try { const states = await db.query( 'SELECT * FROM live_engine_state ORDER BY timestamp DESC LIMIT 1' ); const row = (states[0] || [])[0]; if (row) { this.totalPnL = row.totalPnL || 0; this.wins = row.wins || 0; this.losses = row.losses || 0; this.totalTrades = row.totalTrades || 0; 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`); } const orders = await db.query( 'SELECT * FROM live_orders WHERE status = "pending" OR status = "resting"' ); for (const o of (orders[0] || [])) { this.openOrders.set(o.orderId, o); } if (this.openOrders.size) { console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`); } } catch (e) { console.error('[Live] Init error:', e.message); } } isStrategyEnabled(name) { return this.enabledStrategies.has(name); } enableStrategy(name) { this.enabledStrategies.add(name); this._saveState(); console.log(`[Live] Strategy "${name}" ENABLED`); } disableStrategy(name) { this.enabledStrategies.delete(name); this._saveState(); console.log(`[Live] Strategy "${name}" DISABLED`); } pause() { this._paused = true; console.log('[Live] ⚠️ PAUSED — no new orders will be placed'); } resume() { this._paused = false; console.log('[Live] ▶️ RESUMED'); } _resetDailyLossIfNeeded() { const now = Date.now(); const elapsed = now - this._dailyLossResetTime; if (elapsed > 24 * 60 * 60 * 1000) { this._dailyLoss = 0; this._dailyLossResetTime = now; } } async fetchBalance() { try { const data = await kalshiFetch('GET', '/trade-api/v2/portfolio/balance'); this._lastBalance = data.balance || 0; this._lastPortfolioValue = data.portfolio_value || 0; return { balance: this._lastBalance, portfolioValue: this._lastPortfolioValue }; } catch (e) { console.error('[Live] Balance fetch error:', e.message); return { balance: this._lastBalance, portfolioValue: this._lastPortfolioValue }; } } async fetchPositions() { try { const data = await kalshiFetch( 'GET', '/trade-api/v2/portfolio/positions?settlement_status=unsettled&limit=200' ); 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); return this._positions; } } async executeTrade(signal, marketState) { if (this._paused) { console.log(`[Live] PAUSED — ignoring signal from ${signal.strategy}`); return null; } if (!this.enabledStrategies.has(signal.strategy)) return null; this._resetDailyLossIfNeeded(); if (this._dailyLoss >= this._maxDailyLossCents) { 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' ); return null; } const priceCents = Math.round(signal.price); const sizeDollars = signal.size; const costCents = sizeDollars * 100; if (costCents > this._maxLossPerTradeCents) { console.log(`[Live] Trade cost $${sizeDollars} exceeds per-trade cap — skipping`); return null; } const priceInDollars = priceCents / 100; const contracts = Math.max(1, Math.floor(sizeDollars / priceInDollars)); const clientOrderId = crypto.randomUUID(); const side = signal.side.toLowerCase(); const orderBody = { ticker: signal.ticker, action: 'buy', side, count: contracts, type: 'limit', client_order_id: clientOrderId, buy_max_cost: costCents, }; if (side === 'yes') { orderBody.yes_price = priceCents; } else { orderBody.no_price = priceCents; } try { console.log(`[Live] Placing order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) | ${signal.reason}`); const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody); const order = result?.order; if (!order?.order_id) { console.error('[Live] Order response missing order_id:', JSON.stringify(result).slice(0, 300)); return null; } const orderRecord = { orderId: order.order_id, clientOrderId, strategy: signal.strategy, ticker: signal.ticker, side, priceCents, contracts, costCents, reason: signal.reason, status: order.status || 'pending', createdAt: Date.now(), settled: false, result: null, pnl: null, fillCount: order.taker_fill_count || 0, fillCost: order.taker_fill_cost || 0, marketState: { yesPct: marketState.yesPct, noPct: marketState.noPct } }; if (order.status === 'executed' || order.status === 'filled') { orderRecord.status = 'filled'; 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}`); return null; } else { this.openOrders.set(order.order_id, orderRecord); } try { await db.create('live_orders', orderRecord); } catch (e) { console.error('[Live] DB write error:', e.message); } const msg = `💰 LIVE [${signal.strategy}] ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars}) | ${signal.reason}`; console.log(`[Live] ${msg}`); await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings'); await this._saveState(); return orderRecord; } catch (e) { console.error(`[Live] Order failed: ${e.message}`); await notify( `❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`, 'Kalbot Error', 'urgent', 'x,warning' ); return null; } } async settle(ticker, rawResult) { const result = String(rawResult || '').toLowerCase(); if (result !== 'yes' && result !== 'no') return null; const settled = []; for (const [orderId, order] of this.openOrders) { if (order.ticker !== ticker) continue; const won = order.side === result; const fillCostCents = order.fillCost || order.costCents; const payout = won ? order.contracts * 100 : 0; const pnl = payout - fillCostCents; order.settled = true; order.result = result; order.pnl = pnl; order.settleTime = Date.now(); order.status = 'settled'; this.totalPnL += pnl; if (won) this.wins++; else { this.losses++; this._dailyLoss += Math.abs(pnl); } try { await db.query( 'UPDATE live_orders SET settled = true, result = $result, pnl = $pnl, settleTime = $st, status = "settled" WHERE orderId = $oid', { result, pnl, st: order.settleTime, oid: orderId } ); } catch (e) { console.error('[Live] Settle DB error:', e.message); } const emoji = won ? '✅' : '❌'; 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'); settled.push(order); this.openOrders.delete(orderId); } if (settled.length) await this._saveState(); return settled.length ? settled : null; } getOpenTickers() { const tickers = new Set(); for (const [, order] of this.openOrders) { if (!order.settled) tickers.add(order.ticker); } return Array.from(tickers); } hasOpenPositionForStrategy(strategyName) { for (const [, order] of this.openOrders) { if (order.strategy === strategyName && !order.settled) return true; } return false; } getStats() { const openList = []; for (const [, o] of this.openOrders) { if (!o.settled) openList.push(o); } return { 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, totalTrades: this.totalTrades, openOrders: openList, paused: this._paused, dailyLoss: parseFloat((this._dailyLoss / 100).toFixed(2)), maxDailyLoss: parseFloat((this._maxDailyLossCents / 100).toFixed(2)), maxPerTrade: parseFloat((this._maxLossPerTradeCents / 100).toFixed(2)), enabledStrategies: Array.from(this.enabledStrategies), positions: this._positions }; } async _saveState() { try { await db.create('live_engine_state', { totalPnL: this.totalPnL, wins: this.wins, losses: this.losses, totalTrades: this.totalTrades, enabledStrategies: Array.from(this.enabledStrategies), paused: this._paused, timestamp: Date.now() }); } catch (e) { console.error('[Live] State save error:', e.message); } } }