Add /close command and auto-flip logic with order cleanup
- Added /close Telegram command for full position closure - Updated /reduce to accept 10-100% (was 10-90%) - Implemented auto-flip logic: automatically closes opposite position when signal reverses - Fixed risk check to allow opposite direction trades (signal flips) - Enhanced Position Manager to cancel orders when removing trades - Added startup initialization for Position Manager (restores trades on restart) - Fixed analytics to show stopped-out trades (manual DB update for orphaned trade) - Updated reduce endpoint to route 100% closes through closePosition for proper cleanup - All position closures now guarantee TP/SL order cancellation on Drift
This commit is contained in:
@@ -45,20 +45,37 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
// Check for existing positions on the same symbol
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
const existingTrades = Array.from(positionManager.getActiveTrades().values())
|
||||
const duplicatePosition = existingTrades.find(trade => trade.symbol === body.symbol)
|
||||
const existingPosition = existingTrades.find(trade => trade.symbol === body.symbol)
|
||||
|
||||
if (duplicatePosition) {
|
||||
console.log('🚫 Risk check BLOCKED: Duplicate position exists', {
|
||||
if (existingPosition) {
|
||||
// Check if it's the SAME direction (duplicate - block it)
|
||||
if (existingPosition.direction === body.direction) {
|
||||
console.log('🚫 Risk check BLOCKED: Duplicate position (same direction)', {
|
||||
symbol: body.symbol,
|
||||
existingDirection: existingPosition.direction,
|
||||
requestedDirection: body.direction,
|
||||
existingEntry: existingPosition.entryPrice,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Duplicate position',
|
||||
details: `Already have ${existingPosition.direction} position on ${body.symbol} (entry: $${existingPosition.entryPrice})`,
|
||||
})
|
||||
}
|
||||
|
||||
// OPPOSITE direction - this is a signal flip/reversal (ALLOW IT)
|
||||
console.log('🔄 Risk check: Signal flip detected', {
|
||||
symbol: body.symbol,
|
||||
existingDirection: duplicatePosition.direction,
|
||||
requestedDirection: body.direction,
|
||||
existingEntry: duplicatePosition.entryPrice,
|
||||
existingDirection: existingPosition.direction,
|
||||
newDirection: body.direction,
|
||||
note: 'Will close existing and open opposite',
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Duplicate position',
|
||||
details: `Already have ${duplicatePosition.direction} position on ${body.symbol} (entry: $${duplicatePosition.entryPrice})`,
|
||||
allowed: true,
|
||||
reason: 'Signal flip',
|
||||
details: `Signal reversed from ${existingPosition.direction} to ${body.direction} - will flip position`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +85,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
// 3. Check cooldown period
|
||||
// 4. Check account health
|
||||
|
||||
console.log(`✅ Risk check PASSED: No duplicate positions`)
|
||||
console.log(`✅ Risk check PASSED: No existing positions`)
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: true,
|
||||
|
||||
@@ -100,6 +100,38 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
)
|
||||
}
|
||||
|
||||
// AUTO-FLIP: Check for existing opposite direction position
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
const existingTrades = Array.from(positionManager.getActiveTrades().values())
|
||||
const oppositePosition = existingTrades.find(
|
||||
trade => trade.symbol === driftSymbol && trade.direction !== body.direction
|
||||
)
|
||||
|
||||
if (oppositePosition) {
|
||||
console.log(`🔄 Signal flip detected! Closing ${oppositePosition.direction} to open ${body.direction}`)
|
||||
|
||||
// Close opposite position
|
||||
const { closePosition } = await import('@/lib/drift/orders')
|
||||
const closeResult = await closePosition({
|
||||
symbol: driftSymbol,
|
||||
percentToClose: 100,
|
||||
slippageTolerance: config.slippageTolerance,
|
||||
})
|
||||
|
||||
if (!closeResult.success) {
|
||||
console.error('❌ Failed to close opposite position:', closeResult.error)
|
||||
// Continue anyway - we'll try to open the new position
|
||||
} else {
|
||||
console.log(`✅ Closed ${oppositePosition.direction} position at $${closeResult.closePrice?.toFixed(4)} (P&L: $${closeResult.realizedPnL?.toFixed(2)})`)
|
||||
|
||||
// Position Manager will handle cleanup (including order cancellation)
|
||||
// The executeExit method already removes the trade and updates database
|
||||
}
|
||||
|
||||
// Small delay to ensure position is fully closed
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = config.positionSize * config.leverage
|
||||
|
||||
@@ -211,8 +243,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
lastUpdateTime: Date.now(),
|
||||
}
|
||||
|
||||
// Add to position manager for monitoring
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
// Add to position manager for monitoring (reuse positionManager from above)
|
||||
await positionManager.addTrade(activeTrade)
|
||||
|
||||
console.log('✅ Trade added to position manager for monitoring')
|
||||
|
||||
@@ -60,16 +60,65 @@ export async function POST(request: NextRequest): Promise<NextResponse<ReducePos
|
||||
|
||||
const reducePercent = body.reducePercent || 50 // Default: close 50%
|
||||
|
||||
if (reducePercent < 10 || reducePercent > 90) {
|
||||
if (reducePercent < 10 || reducePercent > 100) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Reduce percent must be between 10 and 90',
|
||||
message: 'Reduce percent must be between 10 and 100',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// If reducing 100%, use the close endpoint logic instead
|
||||
if (reducePercent === 100) {
|
||||
// Get Position Manager
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
const activeTrades = positionManager.getActiveTrades()
|
||||
const trade = activeTrades.find(t => t.id === body.tradeId)
|
||||
|
||||
if (!trade) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: `Position ${body.tradeId} not found`,
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`🔴 Closing 100% of position: ${trade.symbol}`)
|
||||
|
||||
// Initialize Drift service
|
||||
await initializeDriftService()
|
||||
|
||||
// Close entire position (this will automatically cancel all orders)
|
||||
const closeResult = await closePosition({
|
||||
symbol: trade.symbol,
|
||||
percentToClose: 100,
|
||||
slippageTolerance: getMergedConfig().slippageTolerance,
|
||||
})
|
||||
|
||||
if (!closeResult.success) {
|
||||
throw new Error(`Failed to close position: ${closeResult.error}`)
|
||||
}
|
||||
|
||||
console.log(`✅ Position fully closed | P&L: $${closeResult.realizedPnL || 0}`)
|
||||
console.log(`✅ All TP/SL orders cancelled automatically`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Position closed 100%`,
|
||||
closedSize: trade.positionSize,
|
||||
remainingSize: 0,
|
||||
closePrice: closeResult.closePrice,
|
||||
realizedPnL: closeResult.realizedPnL,
|
||||
newTP1: 0,
|
||||
newTP2: 0,
|
||||
newSL: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Get current configuration
|
||||
const config = getMergedConfig()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user