Files
trading_bot_v4/app/api/trading/sync-positions/route.ts
mindesbunister 3aeb00f998 critical: Fix P&L calculation and TP1 false detection bugs
- Add originalPositionSize tracking to prevent stale size usage
- Add price validation to TP1 detection (prevents manual closes misidentified as TP1)
- Fix external closure P&L to use originalPositionSize not currentSize
- Add handleManualClosure method for proper exit reason detection
- Add isPriceAtTarget helper for TP/SL price validation (0.2% tolerance)
- Update all ActiveTrade creation points (execute, test, sync-positions, test-db)

Bug fixes:
- Manual close at 42.34 was detected as TP1 (target 40.71) - FIXED
- P&L showed -$1.71 instead of actual -$2.92 - FIXED
- Exit reason showed SL instead of manual - FIXED

Root cause: Position Manager detected size reduction without validating
price was actually at TP1 level. Used stale currentSize for P&L calculation.

Files modified:
- lib/trading/position-manager.ts (core fixes)
- app/api/trading/execute/route.ts
- app/api/trading/test/route.ts
- app/api/trading/sync-positions/route.ts
- app/api/trading/test-db/route.ts
2025-11-17 15:10:15 +01:00

197 lines
7.0 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 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
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,
originalPositionSize: positionSizeUSD, // Store original size for P&L
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
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 }
)
}
}