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%
This commit is contained in:
203
app/api/trading/reduce-position/route.ts
Normal file
203
app/api/trading/reduce-position/route.ts
Normal file
@@ -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<NextResponse<ReducePositionResponse>> {
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,6 +191,99 @@ async def scale_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 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):
|
async def validate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Handle /validate command - check if positions match settings"""
|
"""Handle /validate command - check if positions match settings"""
|
||||||
|
|
||||||
@@ -331,6 +424,7 @@ def main():
|
|||||||
print(f" /status - Show open positions", flush=True)
|
print(f" /status - Show open positions", flush=True)
|
||||||
print(f" /validate - Validate positions against settings", flush=True)
|
print(f" /validate - Validate positions against settings", flush=True)
|
||||||
print(f" /scale [percent] - Scale position (default 50%)", 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" /buySOL, /sellSOL", flush=True)
|
||||||
print(f" /buyBTC, /sellBTC", flush=True)
|
print(f" /buyBTC, /sellBTC", flush=True)
|
||||||
print(f" /buyETH, /sellETH", 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("status", status_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("buySOL", trade_command))
|
application.add_handler(CommandHandler("buySOL", trade_command))
|
||||||
application.add_handler(CommandHandler("sellSOL", trade_command))
|
application.add_handler(CommandHandler("sellSOL", trade_command))
|
||||||
application.add_handler(CommandHandler("buyBTC", trade_command))
|
application.add_handler(CommandHandler("buyBTC", trade_command))
|
||||||
|
|||||||
Reference in New Issue
Block a user