fix: detect exit reason using trade state flags instead of current price

CRITICAL BUG: Position Manager was using current price to determine exit reason,
but on-chain orders filled at a DIFFERENT price in the past!

Example: LONG entry $184.55, TP1 filled at $184.66, but when Position Manager
checked later (price dropped), it saw currentPrice < TP1 and defaulted to 'SL'

Result: Profitable trades incorrectly labeled as SL exits in database

Fix:
- Use trade.tp1Hit and trade.tp2Hit flags to determine exit reason
- If no TP flags set, use realized P&L to distinguish:
  - Profit >0.5% = TP1 filled
  - Negative P&L = SL filled
- Remove duplicate P&L calculation

This ensures exit reasons match actual on-chain order fills
This commit is contained in:
mindesbunister
2025-11-03 00:02:19 +01:00
parent da960330f4
commit 0ea8773bdc

View File

@@ -407,29 +407,11 @@ export class PositionManager {
// Save currentSize before it becomes 0
const sizeBeforeClosure = trade.currentSize
// Determine exit reason based on price
// 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'
if (trade.direction === 'long') {
if (currentPrice >= trade.tp2Price) {
exitReason = 'TP2'
} else if (currentPrice >= trade.tp1Price) {
exitReason = 'TP1'
} else if (currentPrice <= trade.stopLossPrice) {
exitReason = 'HARD_SL' // Assume hard stop if below SL
}
} else {
// Short
if (currentPrice <= trade.tp2Price) {
exitReason = 'TP2'
} else if (currentPrice <= trade.tp1Price) {
exitReason = 'TP1'
} else if (currentPrice >= trade.stopLossPrice) {
exitReason = 'HARD_SL' // Assume hard stop if above SL
}
}
// Calculate final P&L using size BEFORE closure
// Calculate P&L first
const profitPercent = this.calculateProfitPercent(
trade.entryPrice,
currentPrice,
@@ -438,6 +420,27 @@ export class PositionManager {
const accountPnL = profitPercent * trade.leverage
const realizedPnL = (sizeBeforeClosure * accountPnL) / 100
// 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 = realizedPnL > 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 (realizedPnL > trade.positionSize * 0.005) {
// More than 0.5% profit - must be TP1
exitReason = 'TP1'
} else if (realizedPnL < 0) {
// Loss - must be SL
exitReason = 'SL'
}
// else: small profit/loss near breakeven, default to SL (could be manual close)
}
// Update database
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
try {