Files
trading_bot_v4/app/api/trading/sync-positions/route.ts
mindesbunister c3a053df63 CRITICAL FIX: Use ?? instead of || for tp2SizePercent to allow 0 value
BUG FOUND:
Line 558: tp2SizePercent: config.takeProfit2SizePercent || 100

When config.takeProfit2SizePercent = 0 (TP2-as-runner system), JavaScript's ||
operator treats 0 as falsy and falls back to 100, causing TP2 to close 100%
of remaining position instead of activating trailing stop.

IMPACT:
- On-chain orders placed correctly (line 481 uses ?? correctly)
- Position Manager reads from DB and expects TP2 to close position
- Result: User sees TWO take-profit orders instead of runner system

FIX:
Changed both tp1SizePercent and tp2SizePercent to use ?? operator:
- tp1SizePercent: config.takeProfit1SizePercent ?? 75
- tp2SizePercent: config.takeProfit2SizePercent ?? 0

This allows 0 value to be saved correctly for TP2-as-runner system.

VERIFICATION NEEDED:
Current open SHORT position in database has tp2SizePercent=100 from before
this fix. Next trade will use correct runner system.
2025-11-10 19:46:03 +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 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,
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 }
)
}
}