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
|
// Check for existing positions on the same symbol
|
||||||
const positionManager = await getInitializedPositionManager()
|
const positionManager = await getInitializedPositionManager()
|
||||||
const existingTrades = Array.from(positionManager.getActiveTrades().values())
|
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) {
|
if (existingPosition) {
|
||||||
console.log('🚫 Risk check BLOCKED: Duplicate position exists', {
|
// 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,
|
symbol: body.symbol,
|
||||||
existingDirection: duplicatePosition.direction,
|
existingDirection: existingPosition.direction,
|
||||||
requestedDirection: body.direction,
|
requestedDirection: body.direction,
|
||||||
existingEntry: duplicatePosition.entryPrice,
|
existingEntry: existingPosition.entryPrice,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: 'Duplicate position',
|
reason: 'Duplicate position',
|
||||||
details: `Already have ${duplicatePosition.direction} position on ${body.symbol} (entry: $${duplicatePosition.entryPrice})`,
|
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: existingPosition.direction,
|
||||||
|
newDirection: body.direction,
|
||||||
|
note: 'Will close existing and open opposite',
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
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
|
// 3. Check cooldown period
|
||||||
// 4. Check account health
|
// 4. Check account health
|
||||||
|
|
||||||
console.log(`✅ Risk check PASSED: No duplicate positions`)
|
console.log(`✅ Risk check PASSED: No existing positions`)
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
allowed: true,
|
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
|
// Calculate position size with leverage
|
||||||
const positionSizeUSD = config.positionSize * config.leverage
|
const positionSizeUSD = config.positionSize * config.leverage
|
||||||
|
|
||||||
@@ -211,8 +243,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to position manager for monitoring
|
// Add to position manager for monitoring (reuse positionManager from above)
|
||||||
const positionManager = await getInitializedPositionManager()
|
|
||||||
await positionManager.addTrade(activeTrade)
|
await positionManager.addTrade(activeTrade)
|
||||||
|
|
||||||
console.log('✅ Trade added to position manager for monitoring')
|
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%
|
const reducePercent = body.reducePercent || 50 // Default: close 50%
|
||||||
|
|
||||||
if (reducePercent < 10 || reducePercent > 90) {
|
if (reducePercent < 10 || reducePercent > 100) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Reduce percent must be between 10 and 90',
|
message: 'Reduce percent must be between 10 and 100',
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ 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
|
// Get current configuration
|
||||||
const config = getMergedConfig()
|
const config = getMergedConfig()
|
||||||
|
|
||||||
|
|||||||
18
instrumentation.ts
Normal file
18
instrumentation.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Next.js Instrumentation Hook
|
||||||
|
*
|
||||||
|
* This file is automatically called when the Next.js server starts
|
||||||
|
* Use it to initialize services that need to run on startup
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
console.log('🎯 Server starting - initializing services...')
|
||||||
|
|
||||||
|
// Initialize Position Manager to restore trades from database
|
||||||
|
const { initializePositionManagerOnStartup } = await import('./lib/startup/init-position-manager')
|
||||||
|
await initializePositionManagerOnStartup()
|
||||||
|
|
||||||
|
console.log('✅ Server initialization complete')
|
||||||
|
}
|
||||||
|
}
|
||||||
33
lib/startup/init-position-manager.ts
Normal file
33
lib/startup/init-position-manager.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Position Manager Startup Initialization
|
||||||
|
*
|
||||||
|
* Ensures Position Manager starts monitoring on bot startup
|
||||||
|
* This prevents orphaned trades when the bot restarts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInitializedPositionManager } from '../trading/position-manager'
|
||||||
|
|
||||||
|
let initStarted = false
|
||||||
|
|
||||||
|
export async function initializePositionManagerOnStartup() {
|
||||||
|
if (initStarted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initStarted = true
|
||||||
|
|
||||||
|
console.log('🚀 Initializing Position Manager on startup...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const manager = await getInitializedPositionManager()
|
||||||
|
const status = manager.getStatus()
|
||||||
|
|
||||||
|
console.log(`✅ Position Manager ready - ${status.activeTradesCount} active trades`)
|
||||||
|
|
||||||
|
if (status.activeTradesCount > 0) {
|
||||||
|
console.log(`📊 Monitoring: ${status.symbols.join(', ')}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to initialize Position Manager on startup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,10 +155,23 @@ export class PositionManager {
|
|||||||
/**
|
/**
|
||||||
* Remove a trade from monitoring
|
* Remove a trade from monitoring
|
||||||
*/
|
*/
|
||||||
removeTrade(tradeId: string): void {
|
async removeTrade(tradeId: string): Promise<void> {
|
||||||
const trade = this.activeTrades.get(tradeId)
|
const trade = this.activeTrades.get(tradeId)
|
||||||
if (trade) {
|
if (trade) {
|
||||||
console.log(`🗑️ Removing trade: ${trade.symbol}`)
|
console.log(`🗑️ Removing trade: ${trade.symbol}`)
|
||||||
|
|
||||||
|
// Cancel all orders for this symbol (cleanup orphaned orders)
|
||||||
|
try {
|
||||||
|
const { cancelAllOrders } = await import('../drift/orders')
|
||||||
|
const cancelResult = await cancelAllOrders(trade.symbol)
|
||||||
|
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
|
||||||
|
console.log(`✅ Cancelled ${cancelResult.cancelledCount} orphaned orders`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to cancel orders during trade removal:', error)
|
||||||
|
// Continue with removal even if cancel fails
|
||||||
|
}
|
||||||
|
|
||||||
this.activeTrades.delete(tradeId)
|
this.activeTrades.delete(tradeId)
|
||||||
|
|
||||||
// Stop monitoring if no more trades
|
// Stop monitoring if no more trades
|
||||||
@@ -474,7 +487,7 @@ export class PositionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeTrade(trade.id)
|
await this.removeTrade(trade.id)
|
||||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||||
} else {
|
} else {
|
||||||
// Partial close (TP1)
|
// Partial close (TP1)
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ async def reduce_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
if context.args and len(context.args) > 0:
|
if context.args and len(context.args) > 0:
|
||||||
try:
|
try:
|
||||||
reduce_percent = int(context.args[0])
|
reduce_percent = int(context.args[0])
|
||||||
if reduce_percent < 10 or reduce_percent > 90:
|
if reduce_percent < 10 or reduce_percent > 100:
|
||||||
await update.message.reply_text("❌ Reduce percent must be between 10 and 90")
|
await update.message.reply_text("❌ Reduce percent must be between 10 and 100")
|
||||||
return
|
return
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await update.message.reply_text("❌ Invalid reduce percent. Usage: /reduce [percent]")
|
await update.message.reply_text("❌ Invalid reduce percent. Usage: /reduce [percent]")
|
||||||
@@ -284,8 +284,91 @@ async def reduce_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
print(f"❌ Error: {e}", flush=True)
|
print(f"❌ Error: {e}", flush=True)
|
||||||
await update.message.reply_text(f"❌ Error: {str(e)}")
|
await update.message.reply_text(f"❌ Error: {str(e)}")
|
||||||
|
|
||||||
|
async def close_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Handle /close command - close entire position and cancel all orders"""
|
||||||
|
|
||||||
|
# Only process from YOUR chat
|
||||||
|
if update.message.chat_id != ALLOWED_CHAT_ID:
|
||||||
|
await update.message.reply_text("❌ Unauthorized")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"🔴 /close command received", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# First, get the current open position
|
||||||
|
pos_response = requests.get(
|
||||||
|
f"{TRADING_BOT_URL}/api/trading/positions",
|
||||||
|
headers={'Authorization': f'Bearer {API_SECRET_KEY}'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if not pos_response.ok:
|
||||||
|
await update.message.reply_text(f"❌ Error fetching positions: {pos_response.status_code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
pos_data = pos_response.json()
|
||||||
|
positions = pos_data.get('positions', [])
|
||||||
|
|
||||||
|
if not positions:
|
||||||
|
await update.message.reply_text("❌ No open positions to close")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(positions) > 1:
|
||||||
|
await update.message.reply_text("❌ Multiple positions open. Specify symbol or use /reduce")
|
||||||
|
return
|
||||||
|
|
||||||
|
position = positions[0]
|
||||||
|
symbol = position['symbol']
|
||||||
|
direction = position['direction'].upper()
|
||||||
|
entry = position['entryPrice']
|
||||||
|
size = position['currentSize']
|
||||||
|
|
||||||
|
# Close position at market (100%)
|
||||||
|
response = requests.post(
|
||||||
|
f"{TRADING_BOT_URL}/api/trading/close",
|
||||||
|
headers={'Authorization': f'Bearer {API_SECRET_KEY}'},
|
||||||
|
json={'symbol': symbol, 'percentToClose': 100},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📥 API Response: {response.status_code}", flush=True)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
data = response.json()
|
||||||
|
await update.message.reply_text(f"❌ Error: {data.get('message', 'Unknown error')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if not data.get('success'):
|
||||||
|
await update.message.reply_text(f"❌ {data.get('message', 'Failed to close position')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build success message
|
||||||
|
close_price = data.get('closePrice', 0)
|
||||||
|
realized_pnl = data.get('realizedPnL', 0)
|
||||||
|
|
||||||
|
emoji = "💚" if realized_pnl > 0 else "❤️" if realized_pnl < 0 else "💛"
|
||||||
|
|
||||||
|
message = f"{emoji} *Position Closed*\n\n"
|
||||||
|
message += f"*{symbol} {direction}*\n\n"
|
||||||
|
message += f"*Entry:* ${entry:.4f}\n"
|
||||||
|
message += f"*Exit:* ${close_price:.4f}\n"
|
||||||
|
message += f"*Size:* ${size:.2f}\n\n"
|
||||||
|
message += f"*P&L:* ${realized_pnl:.2f}\n\n"
|
||||||
|
message += f"✅ Position closed at market\n"
|
||||||
|
message += f"✅ All TP/SL orders cancelled"
|
||||||
|
|
||||||
|
await update.message.reply_text(message, parse_mode='Markdown')
|
||||||
|
|
||||||
|
print(f"✅ Position closed: {symbol} | P&L: ${realized_pnl:.2f}", flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}", flush=True)
|
||||||
|
await update.message.reply_text(f"❌ Error: {str(e)}")
|
||||||
|
|
||||||
async def validate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def validate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Handle /validate command - check if positions match settings"""
|
"""Handle /validate command - check position consistency"""
|
||||||
|
|
||||||
# Only process from YOUR chat
|
# Only process from YOUR chat
|
||||||
if update.message.chat_id != ALLOWED_CHAT_ID:
|
if update.message.chat_id != ALLOWED_CHAT_ID:
|
||||||
@@ -434,6 +517,7 @@ def main():
|
|||||||
|
|
||||||
# Add command handlers
|
# Add command handlers
|
||||||
application.add_handler(CommandHandler("status", status_command))
|
application.add_handler(CommandHandler("status", status_command))
|
||||||
|
application.add_handler(CommandHandler("close", close_command))
|
||||||
application.add_handler(CommandHandler("validate", validate_command))
|
application.add_handler(CommandHandler("validate", validate_command))
|
||||||
application.add_handler(CommandHandler("scale", scale_command))
|
application.add_handler(CommandHandler("scale", scale_command))
|
||||||
application.add_handler(CommandHandler("reduce", reduce_command))
|
application.add_handler(CommandHandler("reduce", reduce_command))
|
||||||
|
|||||||
Reference in New Issue
Block a user