From a07bf9f4b252d163a1191eb901471cc20a10bc39 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Mon, 27 Oct 2025 20:34:47 +0100 Subject: [PATCH] Add position reduction feature via Telegram - New endpoint: /api/trading/reduce-position to take partial profits - Closes specified percentage at market price - Recalculates and places new TP/SL orders for remaining size - Entry price stays the same, only size is reduced - Telegram command: /reduce [percent] (default 50%, range 10-90%) - Shows realized P&L from the closed portion - Example: /reduce 25 closes 25% and updates orders for remaining 75% --- app/api/trading/reduce-position/route.ts | 203 +++++++++++++++++++++++ telegram_command_bot.py | 95 +++++++++++ 2 files changed, 298 insertions(+) create mode 100644 app/api/trading/reduce-position/route.ts diff --git a/app/api/trading/reduce-position/route.ts b/app/api/trading/reduce-position/route.ts new file mode 100644 index 0000000..9a7b182 --- /dev/null +++ b/app/api/trading/reduce-position/route.ts @@ -0,0 +1,203 @@ +/** + * Reduce Position API Endpoint + * + * Partially closes a position and recalculates TP/SL orders + * POST /api/trading/reduce-position + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getMergedConfig } from '@/config/trading' +import { getInitializedPositionManager } from '@/lib/trading/position-manager' +import { initializeDriftService } from '@/lib/drift/client' +import { closePosition, placeExitOrders, cancelAllOrders } from '@/lib/drift/orders' + +interface ReducePositionRequest { + tradeId: string + reducePercent?: number // 25 = close 25%, 50 = close 50% +} + +interface ReducePositionResponse { + success: boolean + message: string + closedSize?: number + remainingSize?: number + closePrice?: number + realizedPnL?: number + newTP1?: number + newTP2?: number + newSL?: number +} + +export async function POST(request: NextRequest): Promise> { + try { + // Verify authorization + const authHeader = request.headers.get('authorization') + const expectedAuth = `Bearer ${process.env.API_SECRET_KEY}` + + if (!authHeader || authHeader !== expectedAuth) { + return NextResponse.json( + { + success: false, + message: 'Unauthorized', + }, + { status: 401 } + ) + } + + const body: ReducePositionRequest = await request.json() + + console.log('📉 Reducing position:', body) + + if (!body.tradeId) { + return NextResponse.json( + { + success: false, + message: 'tradeId is required', + }, + { status: 400 } + ) + } + + const reducePercent = body.reducePercent || 50 // Default: close 50% + + if (reducePercent < 10 || reducePercent > 90) { + return NextResponse.json( + { + success: false, + message: 'Reduce percent must be between 10 and 90', + }, + { status: 400 } + ) + } + + // Get current configuration + const config = getMergedConfig() + + // 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(`📊 Current position: ${trade.symbol} ${trade.direction}`) + console.log(` Entry: $${trade.entryPrice}`) + console.log(` Size: ${trade.currentSize} (${trade.positionSize} USD)`) + console.log(` Reducing by: ${reducePercent}%`) + + // Initialize Drift service + const driftService = await initializeDriftService() + + // Close portion of position at market + console.log(`💰 Closing ${reducePercent}% of position...`) + + const closeResult = await closePosition({ + symbol: trade.symbol, + percentToClose: reducePercent, + slippageTolerance: config.slippageTolerance, + }) + + if (!closeResult.success || !closeResult.closePrice) { + throw new Error(`Failed to close position: ${closeResult.error}`) + } + + console.log(`✅ Closed at $${closeResult.closePrice}`) + console.log(`💵 Realized P&L: $${closeResult.realizedPnL || 0}`) + + // Calculate remaining position size + const remainingPercent = 100 - reducePercent + const remainingSizeUSD = (trade.positionSize * remainingPercent) / 100 + + console.log(`📊 Remaining position: $${remainingSizeUSD} (${remainingPercent}%)`) + + // Cancel all existing exit orders + console.log('🗑️ Cancelling old TP/SL orders...') + try { + await cancelAllOrders(trade.symbol) + console.log('✅ Old orders cancelled') + } catch (cancelError) { + console.error('⚠️ Failed to cancel orders:', cancelError) + // Continue anyway + } + + // Calculate TP/SL prices (entry price stays the same) + const calculatePrice = (entry: number, percent: number, direction: 'long' | 'short') => { + if (direction === 'long') { + return entry * (1 + percent / 100) + } else { + return entry * (1 - percent / 100) + } + } + + const newTP1 = calculatePrice(trade.entryPrice, config.takeProfit1Percent, trade.direction) + const newTP2 = calculatePrice(trade.entryPrice, config.takeProfit2Percent, trade.direction) + const newSL = calculatePrice(trade.entryPrice, config.stopLossPercent, trade.direction) + + console.log(`🎯 New targets (same entry, reduced size):`) + console.log(` TP1: $${newTP1} (${config.takeProfit1Percent}%)`) + console.log(` TP2: $${newTP2} (${config.takeProfit2Percent}%)`) + console.log(` SL: $${newSL} (${config.stopLossPercent}%)`) + + // Place new exit orders with reduced size + console.log('📝 Placing new TP/SL orders...') + const exitOrders = await placeExitOrders({ + symbol: trade.symbol, + direction: trade.direction, + positionSizeUSD: remainingSizeUSD, + tp1Price: newTP1, + tp2Price: newTP2, + stopLossPrice: newSL, + tp1SizePercent: config.takeProfit1SizePercent, + tp2SizePercent: config.takeProfit2SizePercent, + useDualStops: config.useDualStops, + softStopPrice: config.useDualStops ? calculatePrice(trade.entryPrice, config.softStopPercent, trade.direction) : undefined, + softStopBuffer: config.useDualStops ? config.softStopBuffer : undefined, + hardStopPrice: config.useDualStops ? calculatePrice(trade.entryPrice, config.hardStopPercent, trade.direction) : undefined, + }) + + console.log(`✅ New exit orders placed`) + + // Update Position Manager with new values + trade.positionSize = remainingSizeUSD + trade.currentSize = remainingSizeUSD + trade.realizedPnL += closeResult.realizedPnL || 0 + + // Update prices (stay the same but refresh) + trade.tp1Price = newTP1 + trade.tp2Price = newTP2 + trade.stopLossPrice = newSL + + console.log(`💾 Updated Position Manager`) + + return NextResponse.json({ + success: true, + message: `Reduced position by ${reducePercent}% - Remaining: $${remainingSizeUSD.toFixed(0)}`, + closedSize: (trade.positionSize * reducePercent) / 100, + remainingSize: remainingSizeUSD, + closePrice: closeResult.closePrice, + realizedPnL: closeResult.realizedPnL, + newTP1: newTP1, + newTP2: newTP2, + newSL: newSL, + }) + + } catch (error) { + console.error('❌ Reduce position error:', error) + + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/telegram_command_bot.py b/telegram_command_bot.py index 71b8a36..077c395 100644 --- a/telegram_command_bot.py +++ b/telegram_command_bot.py @@ -191,6 +191,99 @@ async def scale_command(update: Update, context: ContextTypes.DEFAULT_TYPE): print(f"❌ Error: {e}", flush=True) await update.message.reply_text(f"❌ Error: {str(e)}") +async def reduce_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /reduce command - take partial profits and adjust TP/SL""" + + # Only process from YOUR chat + if update.message.chat_id != ALLOWED_CHAT_ID: + await update.message.reply_text("❌ Unauthorized") + return + + print(f"📉 /reduce 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 reduce") + return + + if len(positions) > 1: + await update.message.reply_text("❌ Multiple positions open. Please close extras first.") + return + + position = positions[0] + trade_id = position['id'] + + # Determine reduce percent from command argument + reduce_percent = 50 # Default + if context.args and len(context.args) > 0: + try: + reduce_percent = int(context.args[0]) + if reduce_percent < 10 or reduce_percent > 90: + await update.message.reply_text("❌ Reduce percent must be between 10 and 90") + return + except ValueError: + await update.message.reply_text("❌ Invalid reduce percent. Usage: /reduce [percent]") + return + + # Send reduce request + response = requests.post( + f"{TRADING_BOT_URL}/api/trading/reduce-position", + headers={'Authorization': f'Bearer {API_SECRET_KEY}'}, + json={'tradeId': trade_id, 'reducePercent': reduce_percent}, + 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 reduce position')}") + return + + # Build success message + message = f"✅ *Position Reduced by {reduce_percent}%*\n\n" + message += f"*{position['symbol']} {position['direction'].upper()}*\n\n" + message += f"*Closed:*\n" + message += f" Size: ${data['closedSize']:.0f}\n" + message += f" Price: ${data['closePrice']:.2f}\n" + message += f" P&L: ${data['realizedPnL']:.2f}\n\n" + message += f"*Remaining:*\n" + message += f" Size: ${data['remainingSize']:.0f}\n" + message += f" Entry: ${position['entryPrice']:.2f}\n\n" + message += f"*Updated Targets:*\n" + message += f" TP1: ${data['newTP1']:.2f}\n" + message += f" TP2: ${data['newTP2']:.2f}\n" + message += f" SL: ${data['newSL']:.2f}\n\n" + message += f"🎯 TP/SL orders updated for remaining size!" + + await update.message.reply_text(message, parse_mode='Markdown') + + print(f"✅ Position reduced: {reduce_percent}%", 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): """Handle /validate command - check if positions match settings""" @@ -331,6 +424,7 @@ def main(): print(f" /status - Show open positions", flush=True) print(f" /validate - Validate positions against settings", flush=True) print(f" /scale [percent] - Scale position (default 50%)", flush=True) + print(f" /reduce [percent] - Take partial profits (default 50%)", flush=True) print(f" /buySOL, /sellSOL", flush=True) print(f" /buyBTC, /sellBTC", flush=True) print(f" /buyETH, /sellETH", flush=True) @@ -342,6 +436,7 @@ def main(): application.add_handler(CommandHandler("status", status_command)) application.add_handler(CommandHandler("validate", validate_command)) application.add_handler(CommandHandler("scale", scale_command)) + application.add_handler(CommandHandler("reduce", reduce_command)) application.add_handler(CommandHandler("buySOL", trade_command)) application.add_handler(CommandHandler("sellSOL", trade_command)) application.add_handler(CommandHandler("buyBTC", trade_command))