From c4fc90094e135980fe41dbf3cc8006905115d415 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sun, 15 Mar 2026 17:58:14 -0700 Subject: [PATCH] Fix: Reset preserves open positions, fixes balance race --- lib/paper/engine.js | 59 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/paper/engine.js b/lib/paper/engine.js index 1626ebe..d09a69a 100644 --- a/lib/paper/engine.js +++ b/lib/paper/engine.js @@ -41,6 +41,7 @@ export class PaperEngine { constructor(initialBalancePerStrategy = 1000) { this.initialBalancePerStrategy = initialBalancePerStrategy; this.accounts = new Map(); // strategyName -> StrategyPaperAccount + this._resetting = false; } _getAccount(strategyName) { @@ -87,6 +88,8 @@ export class PaperEngine { } async executeTrade(signal, marketState) { + if (this._resetting) return null; + const acct = this._getAccount(signal.strategy); const cost = signal.size; @@ -177,12 +180,24 @@ export class PaperEngine { else acct.losses++; try { - await db.query( + // 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) { + console.error('[Paper] Settle DB fallback error:', e2.message); + } } const emoji = won ? '✅' : '❌'; @@ -252,10 +267,14 @@ export class PaperEngine { acct.losses++; try { - await db.query( + const updated = await db.query( `UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`, { id: pos.id, result: 'expired', pnl: pos.pnl, settleTime: pos.settleTime } ); + const rows = updated[0] || []; + if (rows.length === 0) { + await db.create('paper_positions', { ...pos }); + } } catch (e) { console.error('[Paper] Force-expire DB error:', e.message); } @@ -270,8 +289,42 @@ export class PaperEngine { /** * 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'; + pos.pnl = 0; + pos.settleTime = Date.now(); + + try { + const updated = await db.query( + `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 + } + + 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; @@ -293,6 +346,8 @@ export class PaperEngine { for (const [, acct] of this.accounts) { await this._saveState(acct); } + + this._resetting = false; } /**