From 684ba9173c65915b71f464acd56d7c765e24968f Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sun, 15 Mar 2026 18:35:46 -0700 Subject: [PATCH] Fix: Add open ticker polling for delayed Kalshi results --- lib/paper/engine.js | 74 +++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/lib/paper/engine.js b/lib/paper/engine.js index d09a69a..8c16b16 100644 --- a/lib/paper/engine.js +++ b/lib/paper/engine.js @@ -140,7 +140,7 @@ export class PaperEngine { } async settle(ticker, rawResult) { - // Normalize result to lowercase — Kalshi may return "Yes"/"No"/"yes"/"no" + // Normalize result to lowercase const result = String(rawResult || '').toLowerCase(); if (result !== 'yes' && result !== 'no') { @@ -157,13 +157,9 @@ export class PaperEngine { console.log(`[Paper:${strategyName}] Settling ${positions.length} position(s) for ${ticker}, result: ${result}`); for (const pos of positions) { - // Normalize stored side too const side = String(pos.side || '').toLowerCase(); const won = side === result; - // Payout: if won, each contract pays 100¢. Cost is in dollars, price in cents. - // Contracts bought = cost / (price / 100). Payout = contracts * $1. - // Simplified: payout = cost * (100 / price) const price = pos.price > 0 ? pos.price : 50; const payout = won ? (100 / price) * pos.cost : 0; const pnl = payout - pos.cost; @@ -180,19 +176,16 @@ export class PaperEngine { else acct.losses++; try { - // Use upsert-style: try UPDATE first, if no rows matched, INSERT the settled record const updated = await db.query( `UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`, { id: pos.id, result, pnl: pos.pnl, settleTime: pos.settleTime } ); - // Check if UPDATE matched anything; if not, re-insert the settled position const rows = updated[0] || []; if (rows.length === 0) { await db.create('paper_positions', { ...pos }); } } catch (e) { console.error('[Paper] Settle DB error:', e.message); - // Fallback: try inserting the settled record directly try { await db.create('paper_positions', { ...pos }); } catch (e2) { @@ -215,20 +208,24 @@ export class PaperEngine { return allSettled.length > 0 ? allSettled : null; } - /** - * Attempt to settle any orphaned open positions by checking market status. - * Called on startup to clean up positions from previous runs. - */ - async settleOrphans(getMarketFn) { - const orphanTickers = new Set(); + getOpenTickers() { + const tickers = new Set(); for (const [, acct] of this.accounts) { for (const ticker of acct.openPositions.keys()) { - orphanTickers.add(ticker); + tickers.add(ticker); } } + return Array.from(tickers); + } - if (!orphanTickers.size) return; - console.log(`[Paper] Checking ${orphanTickers.size} ticker(s) for orphaned positions...`); + /** + * Check unresolved tickers periodically for delayed results + */ + async checkOrphans(getMarketFn) { + const orphanTickers = this.getOpenTickers(); + if (!orphanTickers.length) return { settled: [], expired: [] }; + + const results = { settled: [], expired: [] }; for (const ticker of orphanTickers) { try { @@ -237,22 +234,24 @@ export class PaperEngine { const result = market?.result; if (result) { - console.log(`[Paper] Orphan ticker ${ticker} has result "${result}" — settling now`); - await this.settle(ticker, result); - } else if (['closed', 'settled', 'expired', 'finalized'].includes(status)) { - // Market closed but no result yet — mark as expired loss - console.log(`[Paper] Orphan ticker ${ticker} status="${status}" with no result — force-settling as expired`); - await this._forceExpirePositions(ticker); - } else { - console.log(`[Paper] Orphan ticker ${ticker} still active (status="${status}") — keeping open`); + console.log(`[Paper] Delayed result found for ${ticker}: "${result}"`); + const settledPos = await this.settle(ticker, result); + if (settledPos) results.settled.push(...settledPos); + } else if (['expired', 'cancelled'].includes(status)) { + // Explicitly cancelled or expired with no valid outcome + console.log(`[Paper] Ticker ${ticker} marked as ${status} — force-settling as expired`); + const expiredPos = await this._forceExpirePositions(ticker); + if (expiredPos) results.expired.push(...expiredPos); } } catch (e) { console.error(`[Paper] Orphan check failed for ${ticker}:`, e.message); } } + return results; } async _forceExpirePositions(ticker) { + const expired = []; for (const [strategyName, acct] of this.accounts) { const positions = acct.openPositions.get(ticker); if (!positions || !positions.length) continue; @@ -280,25 +279,21 @@ export class PaperEngine { } console.log(`[Paper:${strategyName}] Force-expired position ${pos.id} for ${ticker} (lost $${pos.cost})`); + expired.push(pos); } acct.openPositions.delete(ticker); await this._saveState(acct); } + return expired; } - /** - * Reset all strategy stats and clear trade history. - * Open positions are settled as cancelled (refunded) before clearing. - */ async resetAll() { this._resetting = true; - // First, cancel all open positions by refunding their cost for (const [name, acct] of this.accounts) { for (const [ticker, positions] of acct.openPositions) { for (const pos of positions) { - // Refund the cost back to balance before we reset it acct.balance += pos.cost; pos.settled = true; pos.result = 'cancelled'; @@ -310,21 +305,13 @@ export class PaperEngine { `UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`, { id: pos.id, result: 'cancelled', pnl: 0, settleTime: pos.settleTime } ); - const rows = updated[0] || []; - if (rows.length === 0) { - // Row was already gone, that's fine for a reset - } - } catch (e) { - // ignore during reset - } - + } catch (e) {} console.log(`[Paper:${name}] Cancelled open position ${pos.id} for ${ticker} (refunded $${pos.cost})`); } } acct.openPositions.clear(); } - // Now reset all stats for (const [name, acct] of this.accounts) { acct.balance = acct.initialBalance; acct.totalPnL = 0; @@ -342,7 +329,6 @@ export class PaperEngine { console.error('[Paper] Reset DB error:', e.message); } - // Save fresh state for each account for (const [, acct] of this.accounts) { await this._saveState(acct); } @@ -350,9 +336,6 @@ export class PaperEngine { this._resetting = false; } - /** - * Get combined stats (backward compat) — aggregates all strategies. - */ getStats() { const allOpen = []; let totalBalance = 0; @@ -382,9 +365,6 @@ export class PaperEngine { }; } - /** - * Get per-strategy stats for the paper dashboard. - */ getPerStrategyStats() { const result = {}; for (const [name, acct] of this.accounts) {