diff --git a/app/api/trading/scale-position/route.ts b/app/api/trading/scale-position/route.ts new file mode 100644 index 0000000..e2fab33 --- /dev/null +++ b/app/api/trading/scale-position/route.ts @@ -0,0 +1,224 @@ +/** + * Scale Position API Endpoint + * + * Adds to an existing position and recalculates TP/SL orders + * POST /api/trading/scale-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 { openPosition, placeExitOrders, cancelAllOrders } from '@/lib/drift/orders' + +interface ScalePositionRequest { + tradeId: string + scalePercent?: number // 50 = add 50%, 100 = double position +} + +interface ScalePositionResponse { + success: boolean + message: string + oldEntry?: number + newEntry?: number + oldSize?: number + newSize?: 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: ScalePositionRequest = await request.json() + + console.log('šŸ“ˆ Scaling position:', body) + + if (!body.tradeId) { + return NextResponse.json( + { + success: false, + message: 'tradeId is required', + }, + { status: 400 } + ) + } + + const scalePercent = body.scalePercent || 50 // Default: add 50% + + // 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(` Scaling by: ${scalePercent}%`) + + // Initialize Drift service + const driftService = await initializeDriftService() + + // Check account health before scaling + const healthData = await driftService.getAccountHealth() + const healthPercent = healthData.marginRatio + console.log(`šŸ’Š Account health: ${healthPercent}%`) + + if (healthPercent < 30) { + return NextResponse.json( + { + success: false, + message: `Account health too low (${healthPercent}%) to scale position`, + }, + { status: 400 } + ) + } + + // Calculate additional position size + const additionalSizeUSD = (trade.positionSize * scalePercent) / 100 + + console.log(`šŸ’° Adding $${additionalSizeUSD} to position...`) + + // Open additional position at market + const addResult = await openPosition({ + symbol: trade.symbol, + direction: trade.direction, + sizeUSD: additionalSizeUSD, + slippageTolerance: config.slippageTolerance, + }) + + if (!addResult.success || !addResult.fillPrice) { + throw new Error(`Failed to open additional position: ${addResult.error}`) + } + + console.log(`āœ… Additional position opened at $${addResult.fillPrice}`) + + // Calculate new average entry price + const oldTotalValue = trade.positionSize + const newTotalValue = oldTotalValue + additionalSizeUSD + const oldEntry = trade.entryPrice + const newEntryContribution = addResult.fillPrice + + // Weighted average: (old_size * old_price + new_size * new_price) / total_size + const newAvgEntry = ( + (oldTotalValue * oldEntry) + (additionalSizeUSD * newEntryContribution) + ) / newTotalValue + + console.log(`šŸ“Š New average entry: $${oldEntry} → $${newAvgEntry}`) + console.log(`šŸ“Š New position size: $${oldTotalValue} → $${newTotalValue}`) + + // 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 - might not have any orders + } + + // Calculate new TP/SL prices based on new average entry + 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(newAvgEntry, config.takeProfit1Percent, trade.direction) + const newTP2 = calculatePrice(newAvgEntry, config.takeProfit2Percent, trade.direction) + const newSL = calculatePrice(newAvgEntry, config.stopLossPercent, trade.direction) + + console.log(`šŸŽÆ New targets:`) + console.log(` TP1: $${newTP1} (${config.takeProfit1Percent}%)`) + console.log(` TP2: $${newTP2} (${config.takeProfit2Percent}%)`) + console.log(` SL: $${newSL} (${config.stopLossPercent}%)`) + + // Place new exit orders + console.log('šŸ“ Placing new TP/SL orders...') + const exitOrders = await placeExitOrders({ + symbol: trade.symbol, + direction: trade.direction, + positionSizeUSD: newTotalValue, + tp1Price: newTP1, + tp2Price: newTP2, + stopLossPrice: newSL, + tp1SizePercent: config.takeProfit1SizePercent, + tp2SizePercent: config.takeProfit2SizePercent, + useDualStops: config.useDualStops, + softStopPrice: config.useDualStops ? calculatePrice(newAvgEntry, config.softStopPercent, trade.direction) : undefined, + softStopBuffer: config.useDualStops ? config.softStopBuffer : undefined, + hardStopPrice: config.useDualStops ? calculatePrice(newAvgEntry, config.hardStopPercent, trade.direction) : undefined, + }) + + console.log(`āœ… New exit orders placed`) + + // Update Position Manager with new values + trade.entryPrice = newAvgEntry + trade.positionSize = newTotalValue + trade.currentSize = newTotalValue + trade.tp1Price = newTP1 + trade.tp2Price = newTP2 + trade.stopLossPrice = newSL + + // Reset tracking values + trade.tp1Hit = false + trade.slMovedToBreakeven = false + trade.slMovedToProfit = false + trade.peakPnL = 0 + trade.peakPrice = newAvgEntry + + console.log(`šŸ’¾ Updated Position Manager`) + + return NextResponse.json({ + success: true, + message: `Position scaled by ${scalePercent}% - New entry: $${newAvgEntry.toFixed(2)}`, + oldEntry: oldEntry, + newEntry: newAvgEntry, + oldSize: oldTotalValue, + newSize: newTotalValue, + newTP1: newTP1, + newTP2: newTP2, + newSL: newSL, + }) + + } catch (error) { + console.error('āŒ Scale 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 8e021a9..71b8a36 100644 --- a/telegram_command_bot.py +++ b/telegram_command_bot.py @@ -99,6 +99,98 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): print(f"āŒ Error: {e}", flush=True) await update.message.reply_text(f"āŒ Error: {str(e)}") +async def scale_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /scale command - add to existing position 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"šŸ“ˆ /scale 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 scale") + 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 scale percent from command argument + scale_percent = 50 # Default + if context.args and len(context.args) > 0: + try: + scale_percent = int(context.args[0]) + if scale_percent < 10 or scale_percent > 200: + await update.message.reply_text("āŒ Scale percent must be between 10 and 200") + return + except ValueError: + await update.message.reply_text("āŒ Invalid scale percent. Usage: /scale [percent]") + return + + # Send scaling request + response = requests.post( + f"{TRADING_BOT_URL}/api/trading/scale-position", + headers={'Authorization': f'Bearer {API_SECRET_KEY}'}, + json={'tradeId': trade_id, 'scalePercent': scale_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 scale position')}") + return + + # Build success message + message = f"āœ… *Position Scaled by {scale_percent}%*\n\n" + message += f"*{position['symbol']} {position['direction'].upper()}*\n\n" + message += f"*Entry Price:*\n" + message += f" Old: ${data['oldEntry']:.2f}\n" + message += f" New: ${data['newEntry']:.2f}\n\n" + message += f"*Position Size:*\n" + message += f" Old: ${data['oldSize']:.0f}\n" + message += f" New: ${data['newSize']:.0f}\n\n" + message += f"*New 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"šŸŽÆ All TP/SL orders updated!" + + await update.message.reply_text(message, parse_mode='Markdown') + + print(f"āœ… Position scaled: {scale_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""" @@ -238,6 +330,7 @@ def main(): print(f"\nāœ… Commands:", flush=True) 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" /buySOL, /sellSOL", flush=True) print(f" /buyBTC, /sellBTC", flush=True) print(f" /buyETH, /sellETH", flush=True) @@ -248,6 +341,7 @@ def main(): # Add command handlers 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("buySOL", trade_command)) application.add_handler(CommandHandler("sellSOL", trade_command)) application.add_handler(CommandHandler("buyBTC", trade_command))