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:
mindesbunister
2025-11-09 17:59:50 +01:00
parent 4d533ccb53
commit 22195ed34c
15 changed files with 2166 additions and 17 deletions

View File

@@ -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}`)

View File

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