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,
|
OrderType,
|
||||||
OrderParams,
|
OrderParams,
|
||||||
OrderTriggerCondition,
|
OrderTriggerCondition,
|
||||||
|
LogParser,
|
||||||
} from '@drift-labs/sdk'
|
} from '@drift-labs/sdk'
|
||||||
|
|
||||||
export interface OpenPositionParams {
|
export interface OpenPositionParams {
|
||||||
@@ -78,6 +79,67 @@ export interface PlaceExitOrdersOptions {
|
|||||||
hardStopPrice?: number // Hard stop trigger price (TRIGGER_MARKET)
|
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
|
* 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
|
// 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)
|
// Get leverage from user account (defaults to 10x if not found)
|
||||||
let leverage = 10
|
let leverage = 10
|
||||||
@@ -774,13 +862,13 @@ export async function closePosition(
|
|||||||
logger.log('⚠️ Could not determine leverage from account, using 10x default')
|
logger.log('⚠️ Could not determine leverage from account, using 10x default')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate closed notional value (USD)
|
// Calculate closed notional value (USD) - using exitPrice (actual fill or oracle fallback)
|
||||||
const closedNotional = sizeToClose * oraclePrice
|
const closedNotional = sizeToClose * exitPrice
|
||||||
const realizedPnL = (closedNotional * profitPercent) / 100
|
const realizedPnL = (closedNotional * profitPercent) / 100
|
||||||
const accountPnLPercent = profitPercent * leverage
|
const accountPnLPercent = profitPercent * leverage
|
||||||
|
|
||||||
logger.log(`💰 Close details:`)
|
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(` Profit %: ${profitPercent.toFixed(3)}% | Account P&L (${leverage}x): ${accountPnLPercent.toFixed(2)}%`)
|
||||||
logger.log(` Closed notional: $${closedNotional.toFixed(2)}`)
|
logger.log(` Closed notional: $${closedNotional.toFixed(2)}`)
|
||||||
logger.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
|
logger.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
|
||||||
@@ -811,7 +899,7 @@ export async function closePosition(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transactionSignature: txSig,
|
transactionSignature: txSig,
|
||||||
closePrice: oraclePrice,
|
closePrice: exitPrice, // Use actual fill price (or oracle fallback)
|
||||||
closedSize: sizeToClose,
|
closedSize: sizeToClose,
|
||||||
realizedPnL,
|
realizedPnL,
|
||||||
needsVerification: true, // Flag for Position Manager
|
needsVerification: true, // Flag for Position Manager
|
||||||
@@ -828,7 +916,7 @@ export async function closePosition(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transactionSignature: txSig,
|
transactionSignature: txSig,
|
||||||
closePrice: oraclePrice,
|
closePrice: exitPrice, // Use actual fill price (or oracle fallback)
|
||||||
closedSize: sizeToClose,
|
closedSize: sizeToClose,
|
||||||
realizedPnL,
|
realizedPnL,
|
||||||
needsVerification: confirmationTimedOut, // Keep monitoring if confirmation never arrived
|
needsVerification: confirmationTimedOut, // Keep monitoring if confirmation never arrived
|
||||||
|
|||||||
Reference in New Issue
Block a user