Add position scaling feature via Telegram
- New endpoint: /api/trading/scale-position to add to existing positions - Calculates new average entry price after adding more size - Cancels old TP/SL orders and places new ones at updated levels - Telegram command: /scale [percent] (default 50%) - Example: /scale 100 doubles your position - Automatically adjusts Position Manager tracking with new values - Cleaned up stale duplicate trade from database
This commit is contained in:
224
app/api/trading/scale-position/route.ts
Normal file
224
app/api/trading/scale-position/route.ts
Normal file
@@ -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<NextResponse<ScalePositionResponse>> {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user