Files
trading_bot_v4/app/api/trading/sync-positions/route.ts
mindesbunister 089308a07e 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
2025-11-10 17:05:32 +01:00

194 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 }
)
}
}