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:
mindesbunister
2026-01-11 20:45:20 +01:00
parent 19d7865f6f
commit c1bff0d0f4

View File

@@ -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