critical: Fix exit order token sizing - TP/SL now use exact position size

BUG #92: Exit orders (TP1, TP2, SL) had different token sizes than position
- Position: 142.91 SOL but TP1=140.87 SOL, SL=147.03 SOL (WRONG)
- Root cause: usdToBase() calculated tokens as USD/price per order
- Each exit order price produced different token amounts

FIX: Pass actual token count via positionSizeTokens parameter
- Added positionSizeTokens to PlaceExitOrdersOptions interface
- Added tokensToBase() helper (tokens * 1e9 directly)
- All exit sections now use token-based calculation when available

Files updated to pass positionSizeTokens:
- app/api/trading/execute/route.ts: openResult.fillSize
- lib/trading/smart-entry-timer.ts: openResult.fillSize
- lib/trading/sync-helper.ts: Math.abs(driftPos.size)
- lib/trading/position-manager.ts: Math.abs(position.size) + fetch patterns
- lib/startup/init-position-manager.ts: Math.abs(position.size)
- lib/health/position-manager-health.ts: Drift position fetch + token size

Result: When position = X tokens, ALL exit orders close portions of X tokens
- TP1: X * tp1SizePercent / 100 tokens
- TP2: remaining * tp2SizePercent / 100 tokens
- SL: X tokens (full position)

Backward compatible: Falls back to USD calculation if positionSizeTokens not provided
This commit is contained in:
mindesbunister
2026-01-07 09:59:36 +01:00
parent efbe4d0c04
commit 361f3ba183
7 changed files with 151 additions and 32 deletions

View File

@@ -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)