critical: Fix P&L calculation and TP1 false detection bugs

- Add originalPositionSize tracking to prevent stale size usage
- Add price validation to TP1 detection (prevents manual closes misidentified as TP1)
- Fix external closure P&L to use originalPositionSize not currentSize
- Add handleManualClosure method for proper exit reason detection
- Add isPriceAtTarget helper for TP/SL price validation (0.2% tolerance)
- Update all ActiveTrade creation points (execute, test, sync-positions, test-db)

Bug fixes:
- Manual close at 42.34 was detected as TP1 (target 40.71) - FIXED
- P&L showed -$1.71 instead of actual -$2.92 - FIXED
- Exit reason showed SL instead of manual - FIXED

Root cause: Position Manager detected size reduction without validating
price was actually at TP1 level. Used stale currentSize for P&L calculation.

Files modified:
- lib/trading/position-manager.ts (core fixes)
- app/api/trading/execute/route.ts
- app/api/trading/test/route.ts
- app/api/trading/sync-positions/route.ts
- app/api/trading/test-db/route.ts
This commit is contained in:
mindesbunister
2025-11-17 15:10:15 +01:00
parent 84bd2e27f0
commit 3aeb00f998
6 changed files with 133 additions and 11 deletions

4
.env
View File

@@ -388,7 +388,7 @@ NEW_RELIC_LICENSE_KEY=
# - PHASE_2_COMPLETE_REPORT.md - Feature summary
USE_TRAILING_STOP=true
TRAILING_STOP_PERCENT=0.3
TRAILING_STOP_PERCENT=0.5
TRAILING_STOP_ACTIVATION=0.4
MIN_QUALITY_SCORE=60
SOLANA_ENABLED=true
@@ -411,5 +411,5 @@ TRAILING_STOP_MIN_PERCENT=0.25
TRAILING_STOP_MAX_PERCENT=0.9
USE_PERCENTAGE_SIZE=false
BREAKEVEN_TRIGGER_PERCENT=0
BREAKEVEN_TRIGGER_PERCENT=0.4
ATR_MULTIPLIER_FOR_TP2=2

View File

@@ -620,6 +620,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
tp2Price,
emergencyStopPrice,
currentSize: positionSizeUSD,
originalPositionSize: positionSizeUSD, // Store original size for accurate P&L
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,

View File

@@ -117,6 +117,9 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
tp2Price: tp2Price,
emergencyStopPrice: emergencyStopPrice,
currentSize: positionSizeUSD,
originalPositionSize: positionSizeUSD, // Store original size for P&L
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,

View File

@@ -170,6 +170,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
tp2Price,
emergencyStopPrice,
currentSize: positionSizeUSD,
originalPositionSize: positionSizeUSD, // Store original size for P&L
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,

View File

@@ -221,6 +221,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
tp2Price,
emergencyStopPrice,
currentSize: actualPositionSizeUSD,
originalPositionSize: actualPositionSizeUSD, // Store original size for P&L
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,

View File

@@ -32,6 +32,9 @@ export interface ActiveTrade {
// State
currentSize: number // Changes after TP1
originalPositionSize: number // Original entry size for accurate P&L on manual closes
takeProfitPrice1?: number // TP1 price for validation
takeProfitPrice2?: number // TP2 price for validation
tp1Hit: boolean
tp2Hit: boolean
slMovedToBreakeven: boolean
@@ -119,6 +122,9 @@ export class PositionManager {
tp2Price: dbTrade.takeProfit2Price,
emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02),
currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD,
originalPositionSize: dbTrade.positionSizeUSD, // Store original size for P&L
takeProfitPrice1: dbTrade.takeProfit1Price,
takeProfitPrice2: dbTrade.takeProfit2Price,
tp1Hit: pmState?.tp1Hit ?? false,
tp2Hit: pmState?.tp2Hit ?? false,
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
@@ -155,6 +161,84 @@ export class PositionManager {
this.initialized = true
}
/**
* Handle manual closures with proper exit reason detection
* Called when size reduction detected but price NOT at TP1 level
*/
private async handleManualClosure(
trade: ActiveTrade,
currentPrice: number,
remainingSize: number
): Promise<void> {
console.log(`👤 Processing manual closure for ${trade.symbol}`)
// Determine exit reason based on price levels
let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency' = 'manual'
const profitPercent = this.calculateProfitPercent(trade.entryPrice, currentPrice, trade.direction)
// Check if price is at TP2 or SL levels
const isAtTP2 = this.isPriceAtTarget(currentPrice, trade.takeProfitPrice2 || 0)
const isAtSL = this.isPriceAtTarget(currentPrice, trade.stopLossPrice || 0)
if (isAtTP2 && trade.tp1Hit) {
exitReason = 'TP2'
console.log(`✅ Manual closure was TP2 (price at target)`)
} else if (isAtSL) {
exitReason = 'SL'
console.log(`🛑 Manual closure was SL (price at target)`)
} else {
console.log(`👤 Manual closure confirmed (price not at any target)`)
console.log(` Current: $${currentPrice.toFixed(4)}, TP1: $${trade.takeProfitPrice1?.toFixed(4)}, TP2: $${trade.takeProfitPrice2?.toFixed(4)}, SL: $${trade.stopLossPrice?.toFixed(4)}`)
}
// CRITICAL: Calculate P&L using originalPositionSize for accuracy
const realizedPnL = (trade.originalPositionSize * profitPercent) / 100
console.log(`💰 Manual close P&L: ${profitPercent.toFixed(2)}% on $${trade.originalPositionSize.toFixed(2)} = $${realizedPnL.toFixed(2)}`)
// Remove from monitoring FIRST (prevent race conditions)
this.activeTrades.delete(trade.id)
// Update database
try {
await updateTradeExit({
positionId: trade.positionId,
exitPrice: currentPrice,
exitReason,
realizedPnL,
exitOrderTx: 'Manual closure detected',
holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000),
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(`✅ Manual closure recorded: ${trade.symbol} ${exitReason} P&L: $${realizedPnL.toFixed(2)}`)
// Send Telegram notification
await sendPositionClosedNotification({
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
exitPrice: currentPrice,
positionSize: trade.originalPositionSize,
realizedPnL,
exitReason,
holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000),
maxGain: Math.max(0, trade.maxFavorableExcursion),
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
})
} catch (error) {
console.error('❌ Failed to save manual closure:', error)
}
if (this.activeTrades.size === 0) {
this.stopMonitoring()
}
}
/**
* Add a new trade to monitor
*/
@@ -295,15 +379,17 @@ export class PositionManager {
private async handleExternalClosure(trade: ActiveTrade, reason: string): Promise<void> {
console.log(`🧹 Handling external closure: ${trade.symbol} (${reason})`)
// Calculate approximate P&L using last known price
// CRITICAL: Calculate P&L using originalPositionSize for accuracy
// currentSize may be stale if Drift propagation was interrupted
const profitPercent = this.calculateProfitPercent(
trade.entryPrice,
trade.lastPrice,
trade.direction
)
const estimatedPnL = (trade.currentSize * profitPercent) / 100
const sizeForPnL = trade.originalPositionSize // Use original, not currentSize
const estimatedPnL = (sizeForPnL * profitPercent) / 100
console.log(`💰 Estimated P&L: ${profitPercent.toFixed(2)}% → $${estimatedPnL.toFixed(2)}`)
console.log(`💰 Estimated P&L: ${profitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)}$${estimatedPnL.toFixed(2)}`)
// Remove from monitoring FIRST to prevent race conditions
const tradeId = trade.id
@@ -507,8 +593,21 @@ export class PositionManager {
const reductionPercent = ((trackedSizeUSD - positionSizeUSD) / trade.positionSize) * 100
if (!trade.tp1Hit && reductionPercent >= (this.config.takeProfit1SizePercent * 0.8)) {
// TP1 fired (should be ~75% reduction)
console.log(`🎯 TP1 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`)
// CRITICAL: Validate price is actually at TP1 before marking as TP1 hit
const isPriceAtTP1 = this.isPriceAtTarget(currentPrice, trade.takeProfitPrice1 || 0, 0.002)
if (!isPriceAtTP1) {
console.log(`⚠️ Size reduction detected (${reductionPercent.toFixed(1)}%) but price NOT at TP1`)
console.log(` Current: $${currentPrice.toFixed(4)}, TP1: $${trade.takeProfitPrice1?.toFixed(4) || 'N/A'}`)
console.log(` This is likely a MANUAL CLOSE or external order, not TP1`)
// Handle as external closure with proper exit reason detection
await this.handleManualClosure(trade, currentPrice, positionSizeUSD)
return
}
// TP1 fired (price validated at target)
console.log(`🎯 TP1 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%, price at TP1 target`)
trade.tp1Hit = true
trade.currentSize = positionSizeUSD
@@ -660,9 +759,9 @@ export class PositionManager {
// Position Manager detected it, causing zero P&L bug
// HOWEVER: If this was a phantom trade (extreme size mismatch), set P&L to 0
// 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=false: First closure, calculate on full position size (use originalPositionSize)
// - If tp1Hit=true: Runner closure, calculate on tracked remaining size
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.positionSize
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.originalPositionSize
// 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
@@ -683,6 +782,7 @@ export class PositionManager {
// Include any previously realized profit (e.g., from TP1 partial close)
const previouslyRealized = trade.realizedPnL
let runnerRealized = 0
let runnerProfitPercent = 0
if (!wasPhantom) {
@@ -696,7 +796,7 @@ export class PositionManager {
const totalRealizedPnL = previouslyRealized + runnerRealized
trade.realizedPnL = totalRealizedPnL
console.log(` Realized P&L snapshot → Previous: $${previouslyRealized.toFixed(2)} | Runner: $${runnerRealized.toFixed(2)}${runnerProfitPercent.toFixed(2)}%) | Total: $${totalRealizedPnL.toFixed(2)}`)
console.log(` Realized P&L snapshot → Previous: $${previouslyRealized.toFixed(2)} | Runner: $${runnerRealized.toFixed(2)}${runnerProfitPercent.toFixed(2)}%) on $${sizeForPnL.toFixed(2)} | Total: $${totalRealizedPnL.toFixed(2)}`)
// Determine exit reason from trade state and P&L
if (trade.tp2Hit) {
@@ -1324,6 +1424,16 @@ export class PositionManager {
}
}
/**
* Check if current price is at a target price within tolerance
* Used to validate TP/SL hits vs manual closes
*/
private isPriceAtTarget(currentPrice: number, targetPrice: number, tolerance: number = 0.002): boolean {
if (!targetPrice || targetPrice === 0) return false
const diff = Math.abs(currentPrice - targetPrice) / targetPrice
return diff <= tolerance
}
private shouldStopLoss(price: number, trade: ActiveTrade): boolean {
if (trade.direction === 'long') {
return price <= trade.stopLossPrice