critical: Bug #93 - Three-layer entry price validation with oracle fallback
Root Cause: quoteAssetAmount/baseAssetAmount division producing garbage entry prices - Found 6 autosync records with impossible prices (.18, 6.30, 7.11, 116.24 for SOL) - Drift SDK values can be corrupted during state transitions Fix Layer 1 (lib/drift/client.ts): - Added per-asset price range validation (SOL: 0-000, BTC: 0k-00k, ETH: 00-0k) - Returns null for invalid prices Fix Layer 2 (lib/trading/sync-helper.ts): - Added validatedEntryPrice calculation with oracle fallback - Falls back to Pyth oracle when calculated price is garbage Fix Layer 3 (lib/trading/sync-helper.ts): - Trade creation uses validatedEntryPrice in all 4 price fields - entryPrice, peakPrice, maxFavorablePrice, maxAdversePrice Documentation: - Full Bug #93 added to COMMON_PITFALLS.md with code examples - Quick Reference table updated Cleaned: 6 garbage autosync records deleted from database
This commit is contained in:
@@ -107,7 +107,31 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
|
||||
const direction = driftPos.side
|
||||
const entryPrice = driftPos.entryPrice
|
||||
|
||||
// Calculate TP/SL prices
|
||||
// BUG #89 FIX: Validate entry price is realistic before creating sync record
|
||||
// Prevents garbage autosync records with impossible entry prices
|
||||
const priceValidation: { [symbol: string]: { min: number; max: number } } = {
|
||||
'SOL-PERP': { min: 50, max: 1000 }, // SOL: $50-$1000 range
|
||||
'BTC-PERP': { min: 10000, max: 500000 }, // BTC: $10k-$500k range
|
||||
'ETH-PERP': { min: 500, max: 20000 }, // ETH: $500-$20k range
|
||||
}
|
||||
|
||||
const validation = priceValidation[driftPos.symbol]
|
||||
if (validation && (entryPrice < validation.min || entryPrice > validation.max)) {
|
||||
console.error(`❌ AUTOSYNC REJECTED: Invalid entry price $${entryPrice.toFixed(2)} for ${driftPos.symbol}`)
|
||||
console.error(` Expected range: $${validation.min}-$${validation.max}`)
|
||||
console.error(` Current oracle price: $${currentPrice.toFixed(2)}`)
|
||||
console.error(` Position size: ${driftPos.size} tokens`)
|
||||
console.error(` Using oracle price as fallback entry price`)
|
||||
// Use oracle price as fallback instead of garbage entry price
|
||||
// This is acceptable for syncing since we're just trying to track the position
|
||||
}
|
||||
|
||||
// Use validated entry price (fallback to oracle if garbage)
|
||||
const validatedEntryPrice = (validation && (entryPrice < validation.min || entryPrice > validation.max))
|
||||
? currentPrice
|
||||
: entryPrice
|
||||
|
||||
// Calculate TP/SL prices using validated entry
|
||||
const calculatePrice = (entry: number, percent: number, dir: 'long' | 'short') => {
|
||||
if (dir === 'long') {
|
||||
return entry * (1 + percent / 100)
|
||||
@@ -116,10 +140,10 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
const stopLossPrice = calculatePrice(validatedEntryPrice, config.stopLossPercent, direction)
|
||||
const tp1Price = calculatePrice(validatedEntryPrice, config.takeProfit1Percent, direction)
|
||||
const tp2Price = calculatePrice(validatedEntryPrice, config.takeProfit2Percent, direction)
|
||||
const emergencyStopPrice = calculatePrice(validatedEntryPrice, config.emergencyStopPercent, direction)
|
||||
|
||||
// Calculate position size in USD (Drift size is tokens)
|
||||
const positionSizeUSD = Math.abs(driftPos.size) * currentPrice
|
||||
@@ -286,7 +310,7 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
|
||||
positionId: syntheticPositionId,
|
||||
symbol: driftPos.symbol,
|
||||
direction,
|
||||
entryPrice,
|
||||
entryPrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
|
||||
entryTime: now,
|
||||
positionSizeUSD,
|
||||
collateralUSD: positionSizeUSD / config.leverage,
|
||||
@@ -323,12 +347,12 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
|
||||
realizedPnL: 0,
|
||||
unrealizedPnL: driftPos.unrealizedPnL ?? 0,
|
||||
peakPnL: driftPos.unrealizedPnL ?? 0,
|
||||
peakPrice: entryPrice,
|
||||
peakPrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
|
||||
lastPrice: currentPrice,
|
||||
maxFavorableExcursion: 0,
|
||||
maxAdverseExcursion: 0,
|
||||
maxFavorablePrice: entryPrice,
|
||||
maxAdversePrice: entryPrice,
|
||||
maxFavorablePrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
|
||||
maxAdversePrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
|
||||
lastUpdate: now.toISOString(),
|
||||
// TP2 runner configuration (CRITICAL FIX - Dec 12, 2025)
|
||||
tp1SizePercent: config.takeProfit1SizePercent,
|
||||
|
||||
Reference in New Issue
Block a user