diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 83a00e5..b9041fd 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -14,6 +14,7 @@ import { OrderType, OrderParams, OrderTriggerCondition, + LogParser, } from '@drift-labs/sdk' export interface OpenPositionParams { @@ -78,6 +79,67 @@ export interface PlaceExitOrdersOptions { hardStopPrice?: number // Hard stop trigger price (TRIGGER_MARKET) } +/** + * Extract actual fill price from transaction logs using LogParser + * This provides accurate P&L calculation instead of oracle price (Jan 2026 fix) + * + * @param connection - Solana connection + * @param driftClient - Drift client with program reference + * @param txSig - Transaction signature to parse + * @returns Actual fill price or null if not found + */ +async function getActualFillPriceFromTx( + connection: any, + driftClient: any, + txSig: string +): Promise<{ fillPrice: number; baseAmount: number; quoteAmount: number } | null> { + try { + // Fetch the full transaction + const tx = await connection.getTransaction(txSig, { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0 + }) + + if (!tx) { + console.warn('⚠️ Could not fetch transaction for fill price extraction') + return null + } + + // Parse events from transaction logs + const logParser = new LogParser(driftClient.program) + const events = logParser.parseEventsFromTransaction(tx) + + // Look for OrderActionRecord events (fill events) + // Note: WrappedEvent structure has the data properties directly on the event object + for (const event of events) { + if (event.eventType === 'OrderActionRecord') { + const record = event as any // Cast to access OrderActionRecord properties + + // Check if this is a fill event with amounts + if (record.baseAssetAmountFilled && record.quoteAssetAmountFilled) { + const baseAmount = Number(record.baseAssetAmountFilled.toString()) / 1e9 // 9 decimals + const quoteAmount = Number(record.quoteAssetAmountFilled.toString()) / 1e6 // 6 decimals + + if (baseAmount > 0) { + const fillPrice = quoteAmount / baseAmount + console.log(`📊 Actual fill data from tx logs:`) + console.log(` Base filled: ${baseAmount.toFixed(6)} tokens`) + console.log(` Quote filled: $${quoteAmount.toFixed(2)}`) + console.log(` Fill price: $${fillPrice.toFixed(4)}`) + return { fillPrice, baseAmount, quoteAmount } + } + } + } + } + + console.warn('⚠️ No OrderActionRecord with fill amounts found in transaction') + return null + } catch (error: any) { + console.error('❌ Error extracting fill price from tx:', error.message) + return null + } +} + /** * Open a position with a market order */ @@ -758,9 +820,35 @@ export async function closePosition( } } - // Calculate realized P&L with leverage + // CRITICAL FIX (Jan 2026): Get actual fill price from transaction logs instead of oracle price + // This fixes P&L discrepancy where oracle price differs from actual execution price + let exitPrice = oraclePrice // Fallback to oracle price + let actualFillData: { fillPrice: number; baseAmount: number; quoteAmount: number } | null = null + + if (!confirmationTimedOut) { + try { + const connection = driftService.getConnection() + actualFillData = await getActualFillPriceFromTx(connection, driftClient, txSig) + + if (actualFillData) { + exitPrice = actualFillData.fillPrice + console.log(`✅ Using ACTUAL fill price: $${exitPrice.toFixed(4)} (oracle was: $${oraclePrice.toFixed(4)})`) + const priceDiff = ((exitPrice - oraclePrice) / oraclePrice) * 100 + console.log(` Price difference: ${priceDiff.toFixed(4)}% ${priceDiff > 0 ? '(better than oracle)' : '(worse than oracle)'}`) + } else { + console.warn(`⚠️ Could not extract fill price from tx, using oracle price: $${oraclePrice.toFixed(4)}`) + } + } catch (fillPriceError: any) { + console.error('❌ Error getting fill price from tx:', fillPriceError.message) + console.warn(` Falling back to oracle price: $${oraclePrice.toFixed(4)}`) + } + } else { + console.warn('⚠️ Confirmation timed out, using oracle price for P&L estimation') + } + + // Calculate realized P&L with leverage using actual exit price // CRITICAL: P&L must account for leverage and be calculated on USD notional, not base asset size - const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1) + const profitPercent = ((exitPrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1) // Get leverage from user account (defaults to 10x if not found) let leverage = 10 @@ -774,13 +862,13 @@ export async function closePosition( logger.log('⚠️ Could not determine leverage from account, using 10x default') } - // Calculate closed notional value (USD) - const closedNotional = sizeToClose * oraclePrice + // Calculate closed notional value (USD) - using exitPrice (actual fill or oracle fallback) + const closedNotional = sizeToClose * exitPrice const realizedPnL = (closedNotional * profitPercent) / 100 const accountPnLPercent = profitPercent * leverage logger.log(`💰 Close details:`) - logger.log(` Close price: $${oraclePrice.toFixed(4)}`) + logger.log(` Exit price: $${exitPrice.toFixed(4)}${actualFillData ? ' (from tx fill)' : ' (oracle fallback)'}`) logger.log(` Profit %: ${profitPercent.toFixed(3)}% | Account P&L (${leverage}x): ${accountPnLPercent.toFixed(2)}%`) logger.log(` Closed notional: $${closedNotional.toFixed(2)}`) logger.log(` Realized P&L: $${realizedPnL.toFixed(2)}`) @@ -811,7 +899,7 @@ export async function closePosition( return { success: true, transactionSignature: txSig, - closePrice: oraclePrice, + closePrice: exitPrice, // Use actual fill price (or oracle fallback) closedSize: sizeToClose, realizedPnL, needsVerification: true, // Flag for Position Manager @@ -828,7 +916,7 @@ export async function closePosition( return { success: true, transactionSignature: txSig, - closePrice: oraclePrice, + closePrice: exitPrice, // Use actual fill price (or oracle fallback) closedSize: sizeToClose, realizedPnL, needsVerification: confirmationTimedOut, // Keep monitoring if confirmation never arrived