fix: Use actual fill price from Drift tx logs instead of oracle price
- Added getActualFillPriceFromTx() helper using Drift SDK LogParser - Extracts OrderActionRecord from transaction logs for real fill data - P&L now calculated from exitPrice (actual fill) not oraclePrice - Fixes ~5.7% P&L over-reporting issue ($7.53 on $133 trade) Root cause: Oracle price at close time differs from actual fill price Evidence: Drift showed $133.03, database showed $140.56
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user