Prevent repeated TP2 cleanup loops

This commit is contained in:
mindesbunister
2025-11-05 16:14:17 +01:00
parent a100945864
commit 5241920d44
2 changed files with 42 additions and 17 deletions

View File

@@ -46,6 +46,8 @@ export interface ClosePositionResult {
closePrice?: number closePrice?: number
closedSize?: number closedSize?: number
realizedPnL?: number realizedPnL?: number
fullyClosed?: boolean
remainingSize?: number
error?: string error?: string
} }
@@ -602,13 +604,21 @@ export async function closePosition(
console.log(` Closed notional: $${closedNotional.toFixed(2)}`) console.log(` Closed notional: $${closedNotional.toFixed(2)}`)
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`) console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
// If closing 100%, cancel all remaining orders for this market // Check remaining position size after close
if (params.percentToClose === 100) { const updatedPosition = await driftService.getPosition(marketConfig.driftMarketIndex)
const remainingSize = updatedPosition ? Math.abs(updatedPosition.size) : 0
const fullyClosed = !updatedPosition || remainingSize === 0
if (fullyClosed) {
console.log('🗑️ Position fully closed, cancelling remaining orders...') console.log('🗑️ Position fully closed, cancelling remaining orders...')
const cancelResult = await cancelAllOrders(params.symbol) const cancelResult = await cancelAllOrders(params.symbol)
if (cancelResult.success && cancelResult.cancelledCount! > 0) { if (cancelResult.success && (cancelResult.cancelledCount || 0) > 0) {
console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`) console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`)
} }
} else if (params.percentToClose === 100) {
console.log(
`⚠️ Requested 100% close but ${remainingSize.toFixed(4)} base remains on-chain`
)
} }
return { return {
@@ -617,6 +627,8 @@ export async function closePosition(
closePrice: oraclePrice, closePrice: oraclePrice,
closedSize: sizeToClose, closedSize: sizeToClose,
realizedPnL, realizedPnL,
fullyClosed,
remainingSize,
} }
} catch (error) { } catch (error) {

View File

@@ -682,7 +682,7 @@ export class PositionManager {
} }
// 5. Take profit 2 (remaining position) // 5. Take profit 2 (remaining position)
if (trade.tp1Hit && this.shouldTakeProfit2(currentPrice, trade)) { if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) {
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
// Calculate how much to close based on TP2 size percent // Calculate how much to close based on TP2 size percent
@@ -776,18 +776,31 @@ export class PositionManager {
return return
} }
// Update trade state const closePriceForCalc = result.closePrice || currentPrice
if (percentToClose >= 100) { const closedSizeBase = result.closedSize || 0
// Full close - remove from monitoring const closedUSD = closedSizeBase * closePriceForCalc
trade.realizedPnL += result.realizedPnL || 0 const wasForcedFullClose = !!result.fullyClosed && percentToClose < 100
const treatAsFullClose = percentToClose >= 100 || result.fullyClosed
// Update trade state
if (treatAsFullClose) {
trade.realizedPnL += result.realizedPnL || 0
trade.currentSize = 0
trade.trailingStopActive = false
if (reason === 'TP2') {
trade.tp2Hit = true
}
if (reason === 'TP1') {
trade.tp1Hit = true
}
// Save to database (only for valid exit reasons)
if (reason !== 'error') { if (reason !== 'error') {
try { try {
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000) const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
await updateTradeExit({ await updateTradeExit({
positionId: trade.positionId, positionId: trade.positionId,
exitPrice: result.closePrice || currentPrice, exitPrice: closePriceForCalc,
exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency', exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency',
realizedPnL: trade.realizedPnL, realizedPnL: trade.realizedPnL,
exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE', exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE',
@@ -802,22 +815,22 @@ export class PositionManager {
console.log('💾 Trade saved to database') console.log('💾 Trade saved to database')
} catch (dbError) { } catch (dbError) {
console.error('❌ Failed to save trade exit to database:', dbError) console.error('❌ Failed to save trade exit to database:', dbError)
// Don't fail the close if database fails
} }
} }
await this.removeTrade(trade.id) await this.removeTrade(trade.id)
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`) const closeLabel = wasForcedFullClose
? '✅ Forced full close (below Drift minimum)'
: '✅ Position closed'
console.log(`${closeLabel} | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
} else { } else {
// Partial close (TP1) // Partial close (TP1)
trade.realizedPnL += result.realizedPnL || 0 trade.realizedPnL += result.realizedPnL || 0
// result.closedSize is returned in base asset units (e.g., SOL), convert to USD using closePrice
const closePriceForCalc = result.closePrice || currentPrice
const closedSizeBase = result.closedSize || 0
const closedUSD = closedSizeBase * closePriceForCalc
trade.currentSize = Math.max(0, trade.currentSize - closedUSD) trade.currentSize = Math.max(0, trade.currentSize - closedUSD)
console.log(`✅ Partial close executed | Realized: $${(result.realizedPnL || 0).toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`) console.log(
`✅ Partial close executed | Realized: $${(result.realizedPnL || 0).toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`
)
} }
// TODO: Send notification // TODO: Send notification