Add Position Sync feature for recovering tracking after partial fills

- 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
This commit is contained in:
mindesbunister
2025-11-10 17:05:32 +01:00
parent 2e47731e8e
commit 089308a07e
6 changed files with 440 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
/**
* 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 }
)
}
}