diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index 7293e7c..e8b114f 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -964,6 +964,7 @@ export async function POST(request: NextRequest): Promise { - const base = usd / price // Use the specific order price (NOT entryPrice) - return Math.floor(base * 1e9) // 9 decimals expected by SDK - } - - // Calculate sizes in USD for each TP - // CRITICAL FIX: TP2 must be percentage of REMAINING size after TP1, not original size - const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100 - const remainingAfterTP1 = options.positionSizeUSD - tp1USD - const requestedTp2Percent = options.tp2SizePercent - // Allow explicit 0% to mean "no TP2 order" without forcing it back to 100% - const normalizedTp2Percent = requestedTp2Percent === undefined - ? 100 - : Math.max(0, requestedTp2Percent) - const tp2USD = (remainingAfterTP1 * normalizedTp2Percent) / 100 - - if (normalizedTp2Percent === 0) { - logger.log('â„šī¸ TP2 on-chain order skipped (trigger-only; software handles trailing)') + // CRITICAL FIX (Jan 6, 2026): Use actual token count for exit orders + // BUG: When position = 142.91 SOL, exit orders should close 142.91 SOL (not recalculated from USD) + // OLD (WRONG): TP1 = USD/TP1_price = different token amount per order price + // NEW (CORRECT): TP1 = positionSizeTokens * tp1Percent = SAME token amount as position + + // Helper to convert tokens to base asset lamports (9 decimals) + const tokensToBase = (tokens: number) => { + return Math.floor(tokens * 1e9) // 9 decimals expected by SDK } - logger.log(`📊 Exit order sizes:`) - logger.log(` TP1: ${options.tp1SizePercent}% of $${options.positionSizeUSD.toFixed(2)} = $${tp1USD.toFixed(2)}`) - logger.log(` Remaining after TP1: $${remainingAfterTP1.toFixed(2)}`) - logger.log(` TP2: ${normalizedTp2Percent}% of remaining = $${tp2USD.toFixed(2)}`) - logger.log(` Runner (if any): $${(remainingAfterTP1 - tp2USD).toFixed(2)}`) + // Legacy helper for backward compatibility (when positionSizeTokens not provided) + const usdToBase = (usd: number, price: number) => { + const base = usd / price + return Math.floor(base * 1e9) + } + + // Calculate sizes in TOKENS for each TP (correct approach) + // If positionSizeTokens provided, use it directly; otherwise fall back to USD calculation + const hasTokenSize = options.positionSizeTokens !== undefined && options.positionSizeTokens > 0 + + let tp1Tokens: number + let tp2Tokens: number + let slTokens: number + + if (hasTokenSize) { + // CORRECT: Use actual position token count + tp1Tokens = (options.positionSizeTokens! * options.tp1SizePercent) / 100 + const remainingAfterTP1 = options.positionSizeTokens! - tp1Tokens + const normalizedTp2Percent = options.tp2SizePercent === undefined ? 100 : Math.max(0, options.tp2SizePercent) + tp2Tokens = (remainingAfterTP1 * normalizedTp2Percent) / 100 + slTokens = options.positionSizeTokens! // SL closes full position + + logger.log(`📊 Exit order sizes (TOKEN-BASED - CORRECT):`) + logger.log(` Position: ${options.positionSizeTokens!.toFixed(4)} tokens`) + logger.log(` TP1: ${options.tp1SizePercent}% = ${tp1Tokens.toFixed(4)} tokens`) + logger.log(` Remaining after TP1: ${remainingAfterTP1.toFixed(4)} tokens`) + logger.log(` TP2: ${normalizedTp2Percent}% of remaining = ${tp2Tokens.toFixed(4)} tokens`) + logger.log(` SL: ${slTokens.toFixed(4)} tokens (full position)`) + } else { + // LEGACY FALLBACK: Calculate from USD (less accurate but backward compatible) + console.warn('âš ī¸ positionSizeTokens not provided, falling back to USD calculation (less accurate)') + const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100 + const remainingAfterTP1USD = options.positionSizeUSD - tp1USD + const normalizedTp2Percent = options.tp2SizePercent === undefined ? 100 : Math.max(0, options.tp2SizePercent) + const tp2USD = (remainingAfterTP1USD * normalizedTp2Percent) / 100 + + // Calculate tokens from USD using respective prices + tp1Tokens = tp1USD / options.tp1Price + tp2Tokens = tp2USD / options.tp2Price + slTokens = options.positionSizeUSD / options.stopLossPrice + + logger.log(`📊 Exit order sizes (USD-BASED - LEGACY):`) + logger.log(` TP1: ${options.tp1SizePercent}% of $${options.positionSizeUSD.toFixed(2)} = $${tp1USD.toFixed(2)} = ${tp1Tokens.toFixed(4)} tokens`) + logger.log(` Remaining after TP1: $${remainingAfterTP1USD.toFixed(2)}`) + logger.log(` TP2: ${normalizedTp2Percent}% of remaining = $${tp2USD.toFixed(2)} = ${tp2Tokens.toFixed(4)} tokens`) + logger.log(` SL: $${options.positionSizeUSD.toFixed(2)} = ${slTokens.toFixed(4)} tokens`) + } + + if (options.tp2SizePercent === 0) { + logger.log('â„šī¸ TP2 on-chain order skipped (trigger-only; software handles trailing)') + } // For orders that close a long, the order direction should be SHORT (sell) const orderDirection = options.direction === 'long' ? PositionDirection.SHORT : PositionDirection.LONG // Place TP1 LIMIT reduce-only - if (tp1USD > 0) { - const baseAmount = usdToBase(tp1USD, options.tp1Price) // Use TP1 price + if (tp1Tokens > 0) { + const baseAmount = tokensToBase(tp1Tokens) if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { expectedOrders += 1 const orderParams: any = { @@ -334,8 +367,10 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise< } // Place TP2 LIMIT reduce-only - if (tp2USD > 0) { - const baseAmount = usdToBase(tp2USD, options.tp2Price) // Use TP2 price + if (tp2Tokens > 0) { + const baseAmount = tokensToBase(tp2Tokens) + logger.log(`📊 TP2 size calculation: TOKEN-BASED`) + logger.log(` TP2 tokens: ${tp2Tokens.toFixed(4)}, baseAmount: ${baseAmount}`) if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { expectedOrders += 1 const orderParams: any = { @@ -365,7 +400,9 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise< // 3. Single TRIGGER_MARKET (default, guaranteed execution) const slUSD = options.positionSizeUSD - const slBaseAmount = usdToBase(slUSD, options.stopLossPrice) // Use SL price + const slBaseAmount = hasTokenSize ? tokensToBase(slTokens) : usdToBase(slUSD, options.stopLossPrice) + logger.log(`📊 SL size calculation: ${hasTokenSize ? 'TOKEN-BASED - CORRECT' : 'USD-BASED - LEGACY'}`) + logger.log(` SL tokens: ${slTokens.toFixed(4)}, slBaseAmount: ${slBaseAmount}`) const useDualStops = options.useDualStops ?? false logger.log(`📊 Expected exit orders: TP1/TP2 that meet min size + ${useDualStops ? 'soft+hard SL' : 'single SL'}`) diff --git a/lib/health/position-manager-health.ts b/lib/health/position-manager-health.ts index ef08381..2a481df 100644 --- a/lib/health/position-manager-health.ts +++ b/lib/health/position-manager-health.ts @@ -128,9 +128,28 @@ async function ensureExitOrdersForTrade( const { placeExitOrders } = await import('../drift/orders') + // Fetch current position size in tokens for accurate order sizing + let positionSizeTokens: number | undefined + try { + const { getDriftService } = await import('../drift/client') + const { getMarketConfig } = await import('../../config/trading') + const driftService = getDriftService() + if (driftService && (driftService as any).isInitialized) { + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + if (position && Math.abs(position.size) > 0.01) { + positionSizeTokens = Math.abs(position.size) + console.log(`📊 Position size for health monitor orders: ${positionSizeTokens.toFixed(4)} tokens`) + } + } + } catch (posError) { + console.warn('âš ī¸ Could not fetch position for health monitor token size, using USD fallback') + } + const placeResult = await placeExitOrders({ symbol: trade.symbol, positionSizeUSD, + positionSizeTokens, entryPrice: trade.entryPrice, tp1Price, tp2Price, diff --git a/lib/startup/init-position-manager.ts b/lib/startup/init-position-manager.ts index 9f8f05b..b686182 100644 --- a/lib/startup/init-position-manager.ts +++ b/lib/startup/init-position-manager.ts @@ -280,6 +280,12 @@ async function restoreOrdersIfMissing( // Import order placement function const { placeExitOrders } = await import('../drift/orders') + // Get position size in tokens for accurate order sizing + const positionSizeTokens = position && position.size ? Math.abs(position.size) : undefined + if (positionSizeTokens) { + logger.log(`📊 Position size for restored orders: ${positionSizeTokens.toFixed(4)} tokens`) + } + // Place exit orders using trade's TP/SL prices const result = await placeExitOrders({ symbol: trade.symbol, @@ -289,6 +295,7 @@ async function restoreOrdersIfMissing( tp2Price: trade.takeProfit2Price, stopLossPrice: trade.stopLossPrice, positionSizeUSD: trade.positionSizeUSD, + positionSizeTokens: positionSizeTokens, tp1SizePercent: 75, tp2SizePercent: 0, // TP2-as-runner }) diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 44ca323..0d944f7 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -848,6 +848,7 @@ export class PositionManager { const placeResult = await placeExitOrders({ symbol: trade.symbol, positionSizeUSD: trade.currentSize, + positionSizeTokens: Math.abs(position.size), // CRITICAL FIX (Jan 6, 2026): Use actual token count entryPrice: trade.entryPrice, tp1Price: trade.tp2Price || trade.entryPrice * (trade.direction === 'long' ? 1.02 : 0.98), tp2Price: trade.tp2Price || trade.entryPrice * (trade.direction === 'long' ? 1.04 : 0.96), @@ -911,6 +912,7 @@ export class PositionManager { direction: trade.direction, entryPrice: trade.entryPrice, positionSizeUSD: trade.currentSize, // Runner size + positionSizeTokens: Math.abs(position.size), // CRITICAL FIX (Jan 6, 2026): Use actual token count stopLossPrice: trade.stopLossPrice, // At breakeven now tp1Price: trade.tp2Price, // TP2 becomes new TP1 for runner tp2Price: 0, // No TP2 for runner @@ -1580,6 +1582,22 @@ export class PositionManager { if (cancelResult.success) { logger.log(`✅ Cancelled ${cancelResult.cancelledCount || 0} old orders`) + // CRITICAL FIX (Jan 6, 2026): Get current position tokens for accurate SL sizing + let currentPositionTokens: number | undefined + try { + const driftService = getDriftService() + if (driftService && (driftService as any).isInitialized) { + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + if (position && Math.abs(position.size) > 0.01) { + currentPositionTokens = Math.abs(position.size) + logger.log(`📊 Current position size for SL: ${currentPositionTokens.toFixed(4)} tokens`) + } + } + } catch (posError) { + console.warn('âš ī¸ Could not fetch position for token size, using USD fallback') + } + // Place ONLY new SL orders at breakeven/profit level for remaining position // DO NOT place TP2 order - trailing stop is software-only (Position Manager monitors) logger.log(`đŸ›Ąī¸ Placing only SL orders at $${newStopLossPrice.toFixed(4)} for remaining position...`) @@ -1587,6 +1605,7 @@ export class PositionManager { const exitOrdersResult = await placeExitOrders({ symbol: trade.symbol, positionSizeUSD: trade.currentSize, + positionSizeTokens: currentPositionTokens, // CRITICAL FIX (Jan 6, 2026): Use actual token count entryPrice: trade.entryPrice, tp1Price: trade.tp2Price, // Dummy value, won't be used (tp1SizePercent=0) tp2Price: trade.tp2Price, // Dummy value, won't be used (tp2SizePercent=0) @@ -1683,6 +1702,22 @@ export class PositionManager { if (cancelResult.success) { logger.log(`✅ Old SL orders cancelled`) + // CRITICAL FIX (Jan 6, 2026): Get current position tokens for accurate SL sizing + let currentPositionTokens: number | undefined + try { + const driftService = getDriftService() + if (driftService && (driftService as any).isInitialized) { + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + if (position && Math.abs(position.size) > 0.01) { + currentPositionTokens = Math.abs(position.size) + logger.log(`📊 Current position size for trailing SL: ${currentPositionTokens.toFixed(4)} tokens`) + } + } + } catch (posError) { + console.warn('âš ī¸ Could not fetch position for token size, using USD fallback') + } + // Calculate initial trailing SL price const trailingDistancePercent = this.config.trailingStopPercent || 0.5 const initialTrailingSL = this.calculatePrice( @@ -1697,6 +1732,7 @@ export class PositionManager { const exitOrdersResult = await placeExitOrders({ symbol: trade.symbol, positionSizeUSD: trade.currentSize, + positionSizeTokens: currentPositionTokens, // CRITICAL FIX (Jan 6, 2026): Use actual token count entryPrice: trade.entryPrice, tp1Price: trade.tp2Price, // No TP1 (already hit) tp2Price: trade.tp2Price, // No TP2 (trigger only) @@ -1894,10 +1930,27 @@ export class PositionManager { if (cancelResult.success) { logger.log(`✅ Old SL orders cancelled`) + // Fetch current position size in tokens for accurate SL sizing + let currentPositionTokens: number | undefined + try { + const driftService = getDriftService() + if (driftService && (driftService as any).isInitialized) { + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + if (position && Math.abs(position.size) > 0.01) { + currentPositionTokens = Math.abs(position.size) + logger.log(`📊 Current position size for trailing SL: ${currentPositionTokens.toFixed(4)} tokens`) + } + } + } catch (posError) { + console.warn('âš ī¸ Could not fetch position for trailing SL token size, using USD fallback') + } + // Place new SL orders at trailing stop price const exitOrdersResult = await placeExitOrders({ symbol: trade.symbol, positionSizeUSD: trade.currentSize, + positionSizeTokens: currentPositionTokens, entryPrice: trade.entryPrice, tp1Price: trade.tp2Price, // No TP1 (already hit) tp2Price: trade.tp2Price, // No TP2 (already hit) diff --git a/lib/trading/smart-entry-timer.ts b/lib/trading/smart-entry-timer.ts index 7a7042c..ca1b785 100644 --- a/lib/trading/smart-entry-timer.ts +++ b/lib/trading/smart-entry-timer.ts @@ -515,6 +515,7 @@ export class SmartEntryTimer { const exitRes = await placeExitOrders({ symbol: signal.symbol, positionSizeUSD, + positionSizeTokens: openResult.fillSize, // CRITICAL FIX (Jan 6, 2026): Use actual token count entryPrice: fillPrice, tp1Price, tp2Price, diff --git a/lib/trading/sync-helper.ts b/lib/trading/sync-helper.ts index 5acf8eb..11bfcd6 100644 --- a/lib/trading/sync-helper.ts +++ b/lib/trading/sync-helper.ts @@ -227,6 +227,7 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P const placeResult = await placeExitOrders({ symbol: driftPos.symbol, positionSizeUSD, + positionSizeTokens: Math.abs(driftPos.size), // CRITICAL FIX (Jan 6, 2026): Use actual token count entryPrice, tp1Price, tp2Price,