diff --git a/lib/database/trades.ts b/lib/database/trades.ts index 188abc8..7fe118a 100644 --- a/lib/database/trades.ts +++ b/lib/database/trades.ts @@ -67,9 +67,12 @@ export interface UpdateTradeStateParams { positionId: string currentSize: number tp1Hit: boolean + tp2Hit: boolean // CRITICAL: Track TP2 hit for runner system + trailingStopActive: boolean // CRITICAL: Track trailing stop activation slMovedToBreakeven: boolean slMovedToProfit: boolean stopLossPrice: number + peakPrice: number // CRITICAL: Track peak price for trailing stop realizedPnL: number unrealizedPnL: number peakPnL: number @@ -278,45 +281,90 @@ export async function updateTradeExit(params: UpdateTradeExitParams) { /** * Update active trade state (for Position Manager persistence) + * CRITICAL FIX (Dec 17, 2025): Bulletproof state persistence to survive container restarts */ export async function updateTradeState(params: UpdateTradeStateParams) { const prisma = getPrismaClient() try { + // STEP 1: Fetch existing trade with configSnapshot (atomic read) + const existingTrade = await prisma.trade.findUnique({ + where: { positionId: params.positionId }, + select: { configSnapshot: true }, + }) + + if (!existingTrade) { + console.error(`❌ Trade not found for state update: ${params.positionId}`) + return + } + + // STEP 2: Merge existing configSnapshot with new positionManagerState + const existingConfig = (existingTrade.configSnapshot as any) || {} + const updatedConfig = { + ...existingConfig, + positionManagerState: { + currentSize: params.currentSize, + tp1Hit: params.tp1Hit, + tp2Hit: params.tp2Hit, // CRITICAL for runner system + trailingStopActive: params.trailingStopActive, // CRITICAL for trailing stop + slMovedToBreakeven: params.slMovedToBreakeven, + slMovedToProfit: params.slMovedToProfit, + stopLossPrice: params.stopLossPrice, + peakPrice: params.peakPrice, // CRITICAL for trailing stop calculations + realizedPnL: params.realizedPnL, + unrealizedPnL: params.unrealizedPnL, + peakPnL: params.peakPnL, + lastPrice: params.lastPrice, + maxFavorableExcursion: params.maxFavorableExcursion, + maxAdverseExcursion: params.maxAdverseExcursion, + maxFavorablePrice: params.maxFavorablePrice, + maxAdversePrice: params.maxAdversePrice, + lastUpdate: new Date().toISOString(), + } + } + + // STEP 3: Update with merged config (atomic write) const trade = await prisma.trade.update({ where: { positionId: params.positionId }, data: { - // Store Position Manager state in configSnapshot - configSnapshot: { - ...(await prisma.trade.findUnique({ - where: { positionId: params.positionId }, - select: { configSnapshot: true } - }))?.configSnapshot as any, - // Add Position Manager state - positionManagerState: { - currentSize: params.currentSize, - tp1Hit: params.tp1Hit, - slMovedToBreakeven: params.slMovedToBreakeven, - slMovedToProfit: params.slMovedToProfit, - stopLossPrice: params.stopLossPrice, - realizedPnL: params.realizedPnL, - unrealizedPnL: params.unrealizedPnL, - peakPnL: params.peakPnL, - lastPrice: params.lastPrice, - maxFavorableExcursion: params.maxFavorableExcursion, - maxAdverseExcursion: params.maxAdverseExcursion, - maxFavorablePrice: params.maxFavorablePrice, - maxAdversePrice: params.maxAdversePrice, - lastUpdate: new Date().toISOString(), - } - } + configSnapshot: updatedConfig }, }) + // STEP 4: Verify state was saved (bulletproof verification) + const verified = await prisma.trade.findUnique({ + where: { positionId: params.positionId }, + select: { configSnapshot: true }, + }) + + const savedState = (verified?.configSnapshot as any)?.positionManagerState + if (!savedState) { + console.error(`❌ CRITICAL: State verification FAILED for ${params.positionId}`) + console.error(` Attempted to save: tp1Hit=${params.tp1Hit}, currentSize=${params.currentSize}`) + // Log to persistent file for investigation + const { logCriticalError } = await import('../utils/persistent-logger') + logCriticalError('Position Manager state save verification FAILED', { + positionId: params.positionId, + attemptedState: params, + verifiedConfigSnapshot: verified?.configSnapshot + }) + return + } + + // Success - state saved and verified + logger.log(`💾 Position Manager state saved & verified: ${params.positionId} (tp1Hit=${params.tp1Hit}, size=$${params.currentSize.toFixed(2)})`) return trade } catch (error) { console.error('❌ Failed to update trade state:', error) - // Don't throw - state updates are non-critical + // Log critical error to persistent file + const { logCriticalError } = await import('../utils/persistent-logger') + logCriticalError('Position Manager state update failed', { + positionId: params.positionId, + params, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }) + // Don't throw - state updates are non-critical, but log for investigation } } diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 0d105ab..423166d 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -2236,13 +2236,20 @@ export class PositionManager { positionId: trade.positionId, currentSize: trade.currentSize, tp1Hit: trade.tp1Hit, + tp2Hit: trade.tp2Hit, // CRITICAL for runner system recovery + trailingStopActive: trade.trailingStopActive, // CRITICAL for trailing stop recovery slMovedToBreakeven: trade.slMovedToBreakeven, slMovedToProfit: trade.slMovedToProfit, stopLossPrice: trade.stopLossPrice, + peakPrice: trade.peakPrice, // CRITICAL for trailing stop calculations realizedPnL: trade.realizedPnL, unrealizedPnL: trade.unrealizedPnL, peakPnL: trade.peakPnL, lastPrice: trade.lastPrice, + maxFavorableExcursion: trade.maxFavorableExcursion, + maxAdverseExcursion: trade.maxAdverseExcursion, + maxFavorablePrice: trade.maxFavorablePrice, + maxAdversePrice: trade.maxAdversePrice, }) } catch (error) { const tradeId = (trade as any).id ?? 'unknown'