fix: Correct TP1 detection for on-chain order fills
Problem: When TP1 order fills on-chain and runner closes quickly, Position Manager detects entire position gone but doesn't know TP1 filled. Result: Marks trade as 'SL' instead of 'TP1', closes 100% instead of partial. Root cause: Position Manager monitoring loop only knows about trade state flags (tp1Hit), not actual Drift order fill history. When both TP1 and runner close before next monitoring cycle, tp1Hit=false but position gone. Fix: Use profit percentage to infer exit reason instead of trade flags - Profit >1.2%: TP2 range - Profit 0.3-1.2%: TP1 range - Profit <0.3%: SL/breakeven range Always calculate P&L on full originalPositionSize for external closures. Exit reason logic determines what actually triggered based on P&L amount. Example from Nov 19 08:40 CET trade: - Entry $140.17, Exit $140.85 = 0.48% profit - Old: Marked as 'SL' (tp1Hit=false, didn't know TP1 filled) - New: Will mark as 'TP1' (profit in TP1 range) Lines changed: lib/trading/position-manager.ts:760-835
This commit is contained in:
@@ -760,10 +760,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
|
||||
// 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 (use originalPositionSize)
|
||||
// - If tp1Hit=true: Runner closure, calculate on tracked remaining size
|
||||
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.originalPositionSize
|
||||
|
||||
// Use full position size for P&L calculation since we don't know if TP1 filled
|
||||
// The exit reason logic will determine TP1 vs SL based on profit amount
|
||||
const sizeForPnL = 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
|
||||
@@ -772,20 +772,15 @@ 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(` TP1 hit: ${trade.tp1Hit}`)
|
||||
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (${trade.tp1Hit ? 'runner' : 'full position'})`)
|
||||
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (full position - exit reason will determine TP1 vs SL)`)
|
||||
if (wasPhantom) {
|
||||
console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`)
|
||||
}
|
||||
|
||||
// Determine exit reason based on TP flags and realized P&L
|
||||
// CRITICAL: Use trade state flags, not current price (on-chain orders filled in the past!)
|
||||
let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL'
|
||||
|
||||
// CRITICAL: Calculate P&L for THIS close only, do NOT add previouslyRealized
|
||||
// Previous bug: Added trade.realizedPnL which could be from prior detection cycles
|
||||
// This caused 10x P&L inflation when same trade detected multiple times
|
||||
// FIX: Calculate ONLY the runner P&L for this specific closure
|
||||
// FIX: Calculate ONLY the P&L for this specific closure
|
||||
let runnerRealized = 0
|
||||
let runnerProfitPercent = 0
|
||||
if (!wasPhantom) {
|
||||
@@ -797,33 +792,25 @@ export class PositionManager {
|
||||
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100
|
||||
}
|
||||
|
||||
// For external closures, we DON'T know if TP1 already hit, so calculate full position P&L
|
||||
// Database will have correct previouslyRealized if TP1 was hit
|
||||
// For external closures, we DON'T know if TP1 already hit, so calculate full position P&L
|
||||
// Database will have correct previouslyRealized if TP1 was hit
|
||||
const totalRealizedPnL = runnerRealized
|
||||
console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`)
|
||||
|
||||
|
||||
// Determine exit reason from trade state and P&L
|
||||
if (trade.tp2Hit) {
|
||||
// TP2 was hit, full position closed (runner stopped or hit target)
|
||||
exitReason = 'TP2'
|
||||
} else if (trade.tp1Hit) {
|
||||
// TP1 was hit, position should be 25% size, but now fully closed
|
||||
// This means either TP2 filled or runner got stopped out
|
||||
exitReason = totalRealizedPnL > 0 ? 'TP2' : 'SL'
|
||||
} else {
|
||||
// No TPs hit yet - either SL or TP1 filled just now
|
||||
// Use P&L to determine: positive = TP, negative = SL
|
||||
if (totalRealizedPnL > trade.positionSize * 0.005) {
|
||||
// More than 0.5% profit - must be TP1
|
||||
// Determine exit reason from P&L percentage
|
||||
// Use actual profit percent to determine what order filled
|
||||
let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL'
|
||||
|
||||
if (runnerProfitPercent > 0.3) {
|
||||
// Positive profit - was a TP order
|
||||
if (runnerProfitPercent >= 1.2) {
|
||||
// Large profit (>1.2%) - TP2 range
|
||||
exitReason = 'TP2'
|
||||
} else {
|
||||
// Moderate profit (0.3-1.2%) - TP1 range
|
||||
exitReason = 'TP1'
|
||||
} else if (totalRealizedPnL < 0) {
|
||||
// Loss - must be SL
|
||||
exitReason = 'SL'
|
||||
}
|
||||
// else: small profit/loss near breakeven, default to SL (could be manual close)
|
||||
} else {
|
||||
// Negative or tiny profit - was SL
|
||||
exitReason = 'SL'
|
||||
}
|
||||
|
||||
// Update database - CRITICAL: Only update once per trade!
|
||||
|
||||
Reference in New Issue
Block a user