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
|
// trade.currentSize may already be 0 if on-chain orders closed the position before
|
||||||
// Position Manager detected it, causing zero P&L bug
|
// Position Manager detected it, causing zero P&L bug
|
||||||
// HOWEVER: If this was a phantom trade (extreme size mismatch), set P&L to 0
|
// 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)
|
// Use full position size for P&L calculation since we don't know if TP1 filled
|
||||||
// - If tp1Hit=true: Runner closure, calculate on tracked remaining size
|
// The exit reason logic will determine TP1 vs SL based on profit amount
|
||||||
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.originalPositionSize
|
const sizeForPnL = trade.originalPositionSize
|
||||||
|
|
||||||
// Check if this was a phantom trade by looking at the last known on-chain size
|
// 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
|
// 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(`📊 External closure detected - Position size tracking:`)
|
||||||
console.log(` Original size: $${trade.positionSize.toFixed(2)}`)
|
console.log(` Original size: $${trade.positionSize.toFixed(2)}`)
|
||||||
console.log(` Tracked current size: $${trade.currentSize.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)} (full position - exit reason will determine TP1 vs SL)`)
|
||||||
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (${trade.tp1Hit ? 'runner' : 'full position'})`)
|
|
||||||
if (wasPhantom) {
|
if (wasPhantom) {
|
||||||
console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`)
|
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
|
// CRITICAL: Calculate P&L for THIS close only, do NOT add previouslyRealized
|
||||||
// Previous bug: Added trade.realizedPnL which could be from prior detection cycles
|
// Previous bug: Added trade.realizedPnL which could be from prior detection cycles
|
||||||
// This caused 10x P&L inflation when same trade detected multiple times
|
// 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 runnerRealized = 0
|
||||||
let runnerProfitPercent = 0
|
let runnerProfitPercent = 0
|
||||||
if (!wasPhantom) {
|
if (!wasPhantom) {
|
||||||
@@ -797,33 +792,25 @@ export class PositionManager {
|
|||||||
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100
|
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
|
const totalRealizedPnL = runnerRealized
|
||||||
console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`)
|
console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
|
||||||
// Determine exit reason from trade state and P&L
|
if (runnerProfitPercent > 0.3) {
|
||||||
if (trade.tp2Hit) {
|
// Positive profit - was a TP order
|
||||||
// TP2 was hit, full position closed (runner stopped or hit target)
|
if (runnerProfitPercent >= 1.2) {
|
||||||
exitReason = 'TP2'
|
// Large profit (>1.2%) - TP2 range
|
||||||
} else if (trade.tp1Hit) {
|
exitReason = 'TP2'
|
||||||
// TP1 was hit, position should be 25% size, but now fully closed
|
} else {
|
||||||
// This means either TP2 filled or runner got stopped out
|
// Moderate profit (0.3-1.2%) - TP1 range
|
||||||
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
|
|
||||||
exitReason = 'TP1'
|
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!
|
// Update database - CRITICAL: Only update once per trade!
|
||||||
|
|||||||
Reference in New Issue
Block a user