- New /api/trading/sync-positions endpoint (no auth) - Fetches actual Drift positions and compares with Position Manager - Removes stale tracking, adds missing positions with calculated TP/SL - Settings UI: Orange 'Sync Positions' button added - CLI script: scripts/sync-positions.sh for terminal access - Full documentation in docs/guides/POSITION_SYNC_GUIDE.md - Quick reference: POSITION_SYNC_QUICK_REF.md - Updated AI instructions with pitfall #23 Problem solved: Manual Telegram trades with partial fills can cause Position Manager to lose tracking, leaving positions without software- based stop loss protection. This feature restores dual-layer protection. Note: Docker build not picking up route yet (cache issue), needs investigation
194 lines
6.9 KiB
TypeScript
194 lines
6.9 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 getDriftService()
|
||
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
|
||
const 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
|
||
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
|
||
const positionSizeUSD = driftPos.size * currentPrice
|
||
|
||
// Create ActiveTrade object
|
||
const activeTrade = {
|
||
id: `sync-${Date.now()}-${driftPos.symbol}`,
|
||
positionId: `manual-${Date.now()}`, // Synthetic ID since we don't have the original
|
||
symbol: driftPos.symbol,
|
||
direction: direction,
|
||
entryPrice: entryPrice,
|
||
entryTime: Date.now() - (60 * 60 * 1000), // Assume 1 hour ago (we don't know actual time)
|
||
positionSize: positionSizeUSD,
|
||
leverage: config.leverage,
|
||
stopLossPrice: stopLossPrice,
|
||
tp1Price: tp1Price,
|
||
tp2Price: tp2Price,
|
||
emergencyStopPrice: emergencyStopPrice,
|
||
currentSize: positionSizeUSD,
|
||
tp1Hit: false,
|
||
tp2Hit: false,
|
||
slMovedToBreakeven: false,
|
||
slMovedToProfit: false,
|
||
trailingStopActive: false,
|
||
realizedPnL: 0,
|
||
unrealizedPnL: driftPos.unrealizedPnL,
|
||
peakPnL: driftPos.unrealizedPnL,
|
||
peakPrice: currentPrice,
|
||
maxFavorableExcursion: 0,
|
||
maxAdverseExcursion: 0,
|
||
maxFavorablePrice: currentPrice,
|
||
maxAdversePrice: currentPrice,
|
||
originalAdx: undefined,
|
||
timesScaled: 0,
|
||
totalScaleAdded: 0,
|
||
atrAtEntry: undefined,
|
||
runnerTrailingPercent: undefined,
|
||
priceCheckCount: 0,
|
||
lastPrice: currentPrice,
|
||
lastUpdateTime: Date.now(),
|
||
}
|
||
|
||
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 }
|
||
)
|
||
}
|
||
}
|