Fix P&L calculation and signal flip detection
- Fix external closure P&L using tp1Hit flag instead of currentSize - Add direction change detection to prevent false TP1 on signal flips - Signal flips now recorded with accurate P&L as 'manual' exits - Add retry logic with exponential backoff for Solana RPC rate limits - Create /api/trading/cancel-orders endpoint for manual cleanup - Improves data integrity for win/loss statistics
This commit is contained in:
@@ -624,6 +624,33 @@ export async function closePosition(
|
||||
/**
|
||||
* Cancel all open orders for a specific market
|
||||
*/
|
||||
/**
|
||||
* Retry a function with exponential backoff for rate limit errors
|
||||
*/
|
||||
async function retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
baseDelay: number = 2000
|
||||
): Promise<T> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const isRateLimit = errorMessage.includes('429') || errorMessage.includes('rate limit')
|
||||
|
||||
if (!isRateLimit || attempt === maxRetries) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const delay = baseDelay * Math.pow(2, attempt)
|
||||
console.log(`⏳ Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`)
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries reached')
|
||||
}
|
||||
|
||||
export async function cancelAllOrders(
|
||||
symbol: string
|
||||
): Promise<{ success: boolean; cancelledCount?: number; error?: string }> {
|
||||
@@ -667,12 +694,14 @@ export async function cancelAllOrders(
|
||||
|
||||
console.log(`📋 Found ${ordersToCancel.length} open orders to cancel (including trigger orders)`)
|
||||
|
||||
// Cancel all orders for this market (cancels all types: LIMIT, TRIGGER_MARKET, TRIGGER_LIMIT)
|
||||
const txSig = await driftClient.cancelOrders(
|
||||
undefined, // Cancel by market type
|
||||
marketConfig.driftMarketIndex,
|
||||
undefined // No specific direction filter
|
||||
)
|
||||
// Cancel all orders with retry logic for rate limits
|
||||
const txSig = await retryWithBackoff(async () => {
|
||||
return await driftClient.cancelOrders(
|
||||
undefined, // Cancel by market type
|
||||
marketConfig.driftMarketIndex,
|
||||
undefined // No specific direction filter
|
||||
)
|
||||
})
|
||||
|
||||
console.log(`✅ Orders cancelled! Transaction: ${txSig}`)
|
||||
|
||||
|
||||
@@ -316,13 +316,16 @@ export class PositionManager {
|
||||
console.log(`⚠️ Position ${trade.symbol} was closed externally (by on-chain order)`)
|
||||
} else {
|
||||
// Position exists - check if size changed (TP1/TP2 filled)
|
||||
const positionSizeUSD = position.size * currentPrice
|
||||
// CRITICAL FIX: position.size from Drift SDK is already in USD notional value
|
||||
const positionSizeUSD = Math.abs(position.size) // Drift SDK returns negative for shorts
|
||||
const trackedSizeUSD = trade.currentSize
|
||||
const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / trackedSizeUSD * 100
|
||||
|
||||
console.log(`📊 Position check: Drift=$${positionSizeUSD.toFixed(2)} Tracked=$${trackedSizeUSD.toFixed(2)} Diff=${sizeDiffPercent.toFixed(1)}%`)
|
||||
|
||||
// If position size reduced significantly, TP orders likely filled
|
||||
if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) {
|
||||
console.log(`📊 Position size changed: tracking $${trackedSizeUSD.toFixed(2)} but found $${positionSizeUSD.toFixed(2)}`)
|
||||
console.log(`✅ Position size reduced: tracking $${trackedSizeUSD.toFixed(2)} → found $${positionSizeUSD.toFixed(2)}`)
|
||||
|
||||
// Detect which TP filled based on size reduction
|
||||
const reductionPercent = ((trackedSizeUSD - positionSizeUSD) / trade.positionSize) * 100
|
||||
@@ -424,7 +427,10 @@ export class PositionManager {
|
||||
// trade.currentSize may already be 0 if on-chain orders closed the position before
|
||||
// Position Manager detected it, causing zero P&L bug
|
||||
// HOWEVER: If this was a phantom trade (extreme size mismatch), set P&L to 0
|
||||
const sizeForPnL = trade.currentSize > 0 ? trade.currentSize : trade.positionSize
|
||||
// CRITICAL FIX: Use tp1Hit flag to determine which size to use for P&L calculation
|
||||
// - If tp1Hit=false: First closure, calculate on full position size
|
||||
// - If tp1Hit=true: Runner closure, calculate on tracked remaining size
|
||||
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.positionSize
|
||||
|
||||
// Check if this was a phantom trade by looking at the last known on-chain size
|
||||
// If last on-chain size was <50% of expected, this is a phantom
|
||||
@@ -433,7 +439,8 @@ export class PositionManager {
|
||||
console.log(`📊 External closure detected - Position size tracking:`)
|
||||
console.log(` Original size: $${trade.positionSize.toFixed(2)}`)
|
||||
console.log(` Tracked current size: $${trade.currentSize.toFixed(2)}`)
|
||||
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)}`)
|
||||
console.log(` TP1 hit: ${trade.tp1Hit}`)
|
||||
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (${trade.tp1Hit ? 'runner' : 'full position'})`)
|
||||
if (wasPhantom) {
|
||||
console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`)
|
||||
}
|
||||
@@ -511,6 +518,41 @@ export class PositionManager {
|
||||
if (position.size < trade.currentSize * 0.95) { // 5% tolerance
|
||||
console.log(`⚠️ Position size mismatch: expected ${trade.currentSize}, got ${position.size}`)
|
||||
|
||||
// CRITICAL: Check if position direction changed (signal flip, not TP1!)
|
||||
const positionDirection = position.side === 'long' ? 'long' : 'short'
|
||||
if (positionDirection !== trade.direction) {
|
||||
console.log(`🔄 DIRECTION CHANGE DETECTED: ${trade.direction} → ${positionDirection}`)
|
||||
console.log(` This is a signal flip, not TP1! Closing old position as manual.`)
|
||||
|
||||
// Calculate actual P&L on full position
|
||||
const profitPercent = this.calculateProfitPercent(trade.entryPrice, currentPrice, trade.direction)
|
||||
const actualPnL = (trade.positionSize * profitPercent) / 100
|
||||
|
||||
try {
|
||||
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: currentPrice,
|
||||
exitReason: 'manual',
|
||||
realizedPnL: actualPnL,
|
||||
exitOrderTx: 'SIGNAL_FLIP',
|
||||
holdTimeSeconds,
|
||||
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
|
||||
maxGain: Math.max(0, trade.maxFavorableExcursion),
|
||||
maxFavorableExcursion: trade.maxFavorableExcursion,
|
||||
maxAdverseExcursion: trade.maxAdverseExcursion,
|
||||
maxFavorablePrice: trade.maxFavorablePrice,
|
||||
maxAdversePrice: trade.maxAdversePrice,
|
||||
})
|
||||
console.log(`💾 Signal flip closure recorded: P&L $${actualPnL.toFixed(2)}`)
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save signal flip closure:', dbError)
|
||||
}
|
||||
|
||||
await this.removeTrade(trade.id)
|
||||
return
|
||||
}
|
||||
|
||||
// CRITICAL: If mismatch is extreme (>50%), this is a phantom trade
|
||||
const sizeRatio = (position.size * currentPrice) / trade.currentSize
|
||||
if (sizeRatio < 0.5) {
|
||||
|
||||
Reference in New Issue
Block a user