- Enhanced DNS failover monitor on secondary (72.62.39.24) - Auto-promotes database: pg_ctl promote on failover - Creates DEMOTED flag on primary via SSH (split-brain protection) - Telegram notifications with database promotion status - Startup safety script ready (integration pending) - 90-second automatic recovery vs 10-30 min manual - Zero-cost 95% enterprise HA benefit Status: DEPLOYED and MONITORING (14:52 CET) Next: Controlled failover test during maintenance
230 lines
7.3 KiB
TypeScript
230 lines
7.3 KiB
TypeScript
/**
|
|
* 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)
|
|
const effectiveTp2SizePercent =
|
|
config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0
|
|
? 0
|
|
: (config.takeProfit2SizePercent ?? 0)
|
|
|
|
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,
|
|
entryPrice: newAvgEntry,
|
|
tp1Price: newTP1,
|
|
tp2Price: newTP2,
|
|
stopLossPrice: newSL,
|
|
tp1SizePercent: config.takeProfit1SizePercent,
|
|
tp2SizePercent: effectiveTp2SizePercent,
|
|
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 }
|
|
)
|
|
}
|
|
}
|