- 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
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
/**
|
||
* Sync Positions API Endpoint
|
||
*
|
||
* Re-synchronizes Position Manager with actual Drift positions
|
||
* Useful when:
|
||
* - Partial fills cause tracking issues
|
||
* - Bot restarts and loses in-memory state
|
||
* - Manual interventions on Drift
|
||
* - Database gets out of sync
|
||
*
|
||
* POST /api/trading/sync-positions
|
||
*/
|
||
|
||
import { NextRequest, NextResponse } from 'next/server'
|
||
import { initializeDriftService, getDriftService } from '@/lib/drift/client'
|
||
import { getInitializedPositionManager } from '@/lib/trading/position-manager'
|
||
import { getPrismaClient } from '@/lib/database/trades'
|
||
import { getMergedConfig } from '@/config/trading'
|
||
|
||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||
try {
|
||
console.log('🔄 Position sync requested...')
|
||
|
||
const config = getMergedConfig()
|
||
const driftService = await initializeDriftService()
|
||
const positionManager = await getInitializedPositionManager()
|
||
const prisma = getPrismaClient()
|
||
|
||
// Get all current Drift positions
|
||
const driftPositions = await driftService.getAllPositions()
|
||
console.log(`📊 Found ${driftPositions.length} positions on Drift`)
|
||
|
||
// Get all currently tracked positions
|
||
let trackedTrades = Array.from(positionManager.getActiveTrades().values())
|
||
console.log(`📋 Position Manager tracking ${trackedTrades.length} trades`)
|
||
|
||
const syncResults = {
|
||
drift_positions: driftPositions.length,
|
||
tracked_positions: trackedTrades.length,
|
||
added: [] as string[],
|
||
removed: [] as string[],
|
||
unchanged: [] as string[],
|
||
errors: [] as string[],
|
||
}
|
||
|
||
// Step 1: Remove tracked positions that don't exist on Drift
|
||
for (const trade of trackedTrades) {
|
||
const existsOnDrift = driftPositions.some(p => p.symbol === trade.symbol)
|
||
|
||
if (!existsOnDrift) {
|
||
console.log(`🗑️ Removing ${trade.symbol} (not on Drift)`)
|
||
await positionManager.removeTrade(trade.id)
|
||
syncResults.removed.push(trade.symbol)
|
||
|
||
// Mark as closed in database
|
||
try {
|
||
await prisma.trade.update({
|
||
where: { positionId: trade.positionId },
|
||
data: {
|
||
status: 'closed',
|
||
exitReason: 'sync_cleanup',
|
||
exitTime: new Date(),
|
||
},
|
||
})
|
||
} catch (dbError) {
|
||
console.error(`❌ Failed to update database for ${trade.symbol}:`, dbError)
|
||
}
|
||
} else {
|
||
syncResults.unchanged.push(trade.symbol)
|
||
}
|
||
}
|
||
|
||
// Step 2: Add Drift positions that aren't being tracked
|
||
trackedTrades = Array.from(positionManager.getActiveTrades().values())
|
||
for (const driftPos of driftPositions) {
|
||
const isTracked = trackedTrades.some(t => t.symbol === driftPos.symbol)
|
||
|
||
if (!isTracked) {
|
||
console.log(`➕ Adding ${driftPos.symbol} to Position Manager`)
|
||
|
||
try {
|
||
// Get current oracle price for this market
|
||
const currentPrice = await driftService.getOraclePrice(driftPos.marketIndex)
|
||
|
||
// Calculate targets based on current config
|
||
const direction = driftPos.side
|
||
const entryPrice = driftPos.entryPrice
|
||
|
||
// Calculate TP/SL prices
|
||
const calculatePrice = (entry: number, percent: number, dir: 'long' | 'short') => {
|
||
if (dir === 'long') {
|
||
return entry * (1 + percent / 100)
|
||
} else {
|
||
return entry * (1 - percent / 100)
|
||
}
|
||
}
|
||
|
||
const stopLossPrice = calculatePrice(entryPrice, config.stopLossPercent, direction)
|
||
const tp1Price = calculatePrice(entryPrice, config.takeProfit1Percent, direction)
|
||
const tp2Price = calculatePrice(entryPrice, config.takeProfit2Percent, direction)
|
||
const emergencyStopPrice = calculatePrice(entryPrice, config.emergencyStopPercent, direction)
|
||
|
||
// Calculate position size in USD (Drift size is tokens)
|
||
const positionSizeUSD = Math.abs(driftPos.size) * currentPrice
|
||
|
||
// Try to find an existing open trade in the database for this symbol
|
||
const existingTrade = await prisma.trade.findFirst({
|
||
where: {
|
||
symbol: driftPos.symbol,
|
||
status: 'open',
|
||
},
|
||
orderBy: { entryTime: 'desc' },
|
||
})
|
||
|
||
const normalizeDirection = (dir: string): 'long' | 'short' =>
|
||
dir === 'long' ? 'long' : 'short'
|
||
|
||
const buildActiveTradeFromDb = (dbTrade: any): any => {
|
||
const pmState = (dbTrade.configSnapshot as any)?.positionManagerState
|
||
|
||
return {
|
||
id: dbTrade.id,
|
||
positionId: dbTrade.positionId,
|
||
symbol: dbTrade.symbol,
|
||
direction: normalizeDirection(dbTrade.direction),
|
||
entryPrice: dbTrade.entryPrice,
|
||
entryTime: dbTrade.entryTime.getTime(),
|
||
positionSize: dbTrade.positionSizeUSD,
|
||
leverage: dbTrade.leverage,
|
||
stopLossPrice: pmState?.stopLossPrice ?? dbTrade.stopLossPrice,
|
||
tp1Price: dbTrade.takeProfit1Price,
|
||
tp2Price: dbTrade.takeProfit2Price,
|
||
emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02),
|
||
currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD,
|
||
originalPositionSize: dbTrade.positionSizeUSD,
|
||
takeProfitPrice1: dbTrade.takeProfit1Price,
|
||
takeProfitPrice2: dbTrade.takeProfit2Price,
|
||
tp1Hit: pmState?.tp1Hit ?? false,
|
||
tp2Hit: pmState?.tp2Hit ?? false,
|
||
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
|
||
slMovedToProfit: pmState?.slMovedToProfit ?? false,
|
||
trailingStopActive: pmState?.trailingStopActive ?? false,
|
||
realizedPnL: pmState?.realizedPnL ?? 0,
|
||
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
||
peakPnL: pmState?.peakPnL ?? 0,
|
||
peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice,
|
||
maxFavorableExcursion: pmState?.maxFavorableExcursion ?? 0,
|
||
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
|
||
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
|
||
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
|
||
originalAdx: dbTrade.adxAtEntry,
|
||
timesScaled: pmState?.timesScaled ?? 0,
|
||
totalScaleAdded: pmState?.totalScaleAdded ?? 0,
|
||
atrAtEntry: dbTrade.atrAtEntry,
|
||
runnerTrailingPercent: pmState?.runnerTrailingPercent,
|
||
priceCheckCount: 0,
|
||
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
||
lastUpdateTime: Date.now(),
|
||
}
|
||
}
|
||
|
||
let activeTrade
|
||
|
||
if (existingTrade) {
|
||
console.log(`🔗 Found existing open trade in DB for ${driftPos.symbol}, attaching to Position Manager`)
|
||
activeTrade = buildActiveTradeFromDb(existingTrade)
|
||
} else {
|
||
console.warn(`⚠️ No open DB trade found for ${driftPos.symbol}. Creating synced placeholder to restore protection.`)
|
||
const now = new Date()
|
||
const syntheticPositionId = `sync-${now.getTime()}-${driftPos.marketIndex}`
|
||
|
||
const placeholderTrade = await prisma.trade.create({
|
||
data: {
|
||
positionId: syntheticPositionId,
|
||
symbol: driftPos.symbol,
|
||
direction,
|
||
entryPrice,
|
||
entryTime: now,
|
||
positionSizeUSD,
|
||
collateralUSD: positionSizeUSD / config.leverage,
|
||
leverage: config.leverage,
|
||
stopLossPrice,
|
||
takeProfit1Price: tp1Price,
|
||
takeProfit2Price: tp2Price,
|
||
tp1SizePercent: config.takeProfit1SizePercent,
|
||
tp2SizePercent:
|
||
config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0
|
||
? 0
|
||
: (config.takeProfit2SizePercent ?? 0),
|
||
status: 'open',
|
||
signalSource: 'drift_sync',
|
||
timeframe: 'sync',
|
||
configSnapshot: {
|
||
source: 'sync-positions',
|
||
syncedAt: now.toISOString(),
|
||
positionManagerState: {
|
||
currentSize: positionSizeUSD,
|
||
tp1Hit: false,
|
||
slMovedToBreakeven: false,
|
||
slMovedToProfit: false,
|
||
stopLossPrice,
|
||
realizedPnL: 0,
|
||
unrealizedPnL: driftPos.unrealizedPnL ?? 0,
|
||
peakPnL: driftPos.unrealizedPnL ?? 0,
|
||
lastPrice: currentPrice,
|
||
maxFavorableExcursion: 0,
|
||
maxAdverseExcursion: 0,
|
||
maxFavorablePrice: entryPrice,
|
||
maxAdversePrice: entryPrice,
|
||
lastUpdate: now.toISOString(),
|
||
},
|
||
},
|
||
entryOrderTx: syntheticPositionId,
|
||
},
|
||
})
|
||
const verifiedPlaceholder = await prisma.trade.findUnique({ where: { positionId: syntheticPositionId } })
|
||
|
||
if (!verifiedPlaceholder) {
|
||
throw new Error(`Placeholder trade not persisted for ${driftPos.symbol} (positionId=${syntheticPositionId})`)
|
||
}
|
||
|
||
activeTrade = buildActiveTradeFromDb(verifiedPlaceholder)
|
||
}
|
||
|
||
await positionManager.addTrade(activeTrade)
|
||
syncResults.added.push(driftPos.symbol)
|
||
|
||
console.log(`✅ Added ${driftPos.symbol} to monitoring`)
|
||
} catch (error) {
|
||
console.error(`❌ Failed to add ${driftPos.symbol}:`, error)
|
||
syncResults.errors.push(`${driftPos.symbol}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const summary = {
|
||
success: true,
|
||
message: 'Position sync complete',
|
||
results: syncResults,
|
||
details: {
|
||
drift_positions: driftPositions.map(p => ({
|
||
symbol: p.symbol,
|
||
direction: p.side,
|
||
size: p.size,
|
||
entry: p.entryPrice,
|
||
pnl: p.unrealizedPnL,
|
||
})),
|
||
now_tracking: Array.from(positionManager.getActiveTrades().values()).map(t => ({
|
||
symbol: t.symbol,
|
||
direction: t.direction,
|
||
entry: t.entryPrice,
|
||
})),
|
||
},
|
||
}
|
||
|
||
console.log('✅ Position sync complete')
|
||
console.log(` Added: ${syncResults.added.length}`)
|
||
console.log(` Removed: ${syncResults.removed.length}`)
|
||
console.log(` Unchanged: ${syncResults.unchanged.length}`)
|
||
console.log(` Errors: ${syncResults.errors.length}`)
|
||
|
||
return NextResponse.json(summary)
|
||
|
||
} catch (error) {
|
||
console.error('❌ Position sync error:', error)
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
error: 'Internal server error',
|
||
message: error instanceof Error ? error.message : 'Unknown error',
|
||
},
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|