fix: Implement critical risk management fixes for bugs #76, #77, #78, #80

Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-09 22:23:43 +00:00
parent 2b0673636f
commit 63b94016fe
4 changed files with 326 additions and 131 deletions

View File

@@ -934,12 +934,54 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
if (!exitRes.success) {
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
// BUG #76 FIX: Log critical error for missing exit orders
logCriticalError('EXIT_ORDERS_PLACEMENT_FAILED', {
symbol: driftSymbol,
direction: body.direction,
entryPrice,
positionSize: positionSizeUSD,
transactionSignature: openResult.transactionSignature,
error: exitRes.error,
partialSignatures: exitRes.signatures || []
})
} else {
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
exitOrderSignatures = exitRes.signatures || []
// BUG #76 FIX: Validate expected signature count
const expectedCount = config.useDualStops ? 4 : 3 // TP1 + TP2 + (soft+hard OR single SL)
if (exitOrderSignatures.length < expectedCount) {
console.error(`❌ CRITICAL: Missing exit orders!`)
console.error(` Expected: ${expectedCount} signatures (TP1 + TP2 + ${config.useDualStops ? 'Soft SL + Hard SL' : 'SL'})`)
console.error(` Got: ${exitOrderSignatures.length} signatures`)
console.error(` Position is UNPROTECTED! Missing stop loss!`)
// Log to persistent file for post-mortem
logCriticalError('MISSING_EXIT_ORDERS', {
symbol: driftSymbol,
direction: body.direction,
entryPrice,
positionSize: positionSizeUSD,
transactionSignature: openResult.transactionSignature,
expectedCount,
actualCount: exitOrderSignatures.length,
signatures: exitOrderSignatures,
useDualStops: config.useDualStops
})
// Continue with trade creation but flag as needing verification
}
}
} catch (err) {
console.error('❌ Unexpected error placing exit orders:', err)
// Log unexpected error
logCriticalError('EXIT_ORDERS_UNEXPECTED_ERROR', {
symbol: driftSymbol,
direction: body.direction,
error: err instanceof Error ? err.message : String(err)
})
}
console.log('🔍 DEBUG: Exit orders section complete, about to calculate quality score...')

View File

@@ -352,66 +352,83 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
const slUSD = options.positionSizeUSD
const slBaseAmount = usdToBase(slUSD)
// Calculate expected number of orders for validation (Bug #76 fix)
const useDualStops = options.useDualStops ?? false
const expectedOrderCount = 2 + (useDualStops ? 2 : 1) // TP1 + TP2 + (soft+hard SL OR single SL)
logger.log(`📊 Expected ${expectedOrderCount} exit orders total (TP1 + TP2 + ${useDualStops ? 'dual stops' : 'single stop'})`)
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const useDualStops = options.useDualStops ?? false
if (useDualStops && options.softStopPrice && options.hardStopPrice) {
// ============== DUAL STOP SYSTEM ==============
logger.log('🛡️🛡️ Placing DUAL STOP SYSTEM...')
// 1. Soft Stop (TRIGGER_LIMIT) - Avoids wicks
const softStopBuffer = options.softStopBuffer ?? 0.4
const softStopMultiplier = options.direction === 'long'
? (1 - softStopBuffer / 100)
: (1 + softStopBuffer / 100)
const softStopParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)),
price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
try {
// 1. Soft Stop (TRIGGER_LIMIT) - Avoids wicks
const softStopBuffer = options.softStopBuffer ?? 0.4
const softStopMultiplier = options.direction === 'long'
? (1 - softStopBuffer / 100)
: (1 + softStopBuffer / 100)
const softStopParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)),
price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
logger.log(` 1⃣ Soft Stop (TRIGGER_LIMIT):`)
logger.log(` Trigger: $${options.softStopPrice.toFixed(4)}`)
logger.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`)
logger.log(` Purpose: Avoid false breakouts/wicks`)
logger.log(` 🔄 Executing soft stop placement...`)
const softStopSig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(softStopParams)
)
logger.log(` ✅ Soft stop placed: ${softStopSig}`)
signatures.push(softStopSig)
} catch (softStopError) {
console.error(`❌ CRITICAL: Failed to place soft stop:`, softStopError)
throw new Error(`Soft stop placement failed: ${softStopError instanceof Error ? softStopError.message : 'Unknown error'}`)
}
logger.log(` 1⃣ Soft Stop (TRIGGER_LIMIT):`)
logger.log(` Trigger: $${options.softStopPrice.toFixed(4)}`)
logger.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`)
logger.log(` Purpose: Avoid false breakouts/wicks`)
const softStopSig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(softStopParams)
)
logger.log(` ✅ Soft stop placed: ${softStopSig}`)
signatures.push(softStopSig)
// 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit
const hardStopParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
try {
// 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit
const hardStopParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
logger.log(` 2⃣ Hard Stop (TRIGGER_MARKET):`)
logger.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`)
logger.log(` Purpose: Guaranteed exit if soft stop doesn't fill`)
logger.log(` 🔄 Executing hard stop placement...`)
const hardStopSig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(hardStopParams)
)
logger.log(` ✅ Hard stop placed: ${hardStopSig}`)
signatures.push(hardStopSig)
} catch (hardStopError) {
console.error(`❌ CRITICAL: Failed to place hard stop:`, hardStopError)
throw new Error(`Hard stop placement failed: ${hardStopError instanceof Error ? hardStopError.message : 'Unknown error'}`)
}
logger.log(` 2⃣ Hard Stop (TRIGGER_MARKET):`)
logger.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`)
logger.log(` Purpose: Guaranteed exit if soft stop doesn't fill`)
const hardStopSig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(hardStopParams)
)
logger.log(` ✅ Hard stop placed: ${hardStopSig}`)
signatures.push(hardStopSig)
logger.log(`🎯 Dual stop system active: Soft @ $${options.softStopPrice.toFixed(2)} | Hard @ $${options.hardStopPrice.toFixed(2)}`)
} else {
@@ -419,64 +436,86 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
const useStopLimit = options.useStopLimit ?? false
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5
if (useStopLimit) {
// TRIGGER_LIMIT: For liquid markets
const limitPriceMultiplier = options.direction === 'long'
? (1 - stopLimitBuffer / 100)
: (1 + stopLimitBuffer / 100)
const orderParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
try {
if (useStopLimit) {
// TRIGGER_LIMIT: For liquid markets
const limitPriceMultiplier = options.direction === 'long'
? (1 - stopLimitBuffer / 100)
: (1 + stopLimitBuffer / 100)
const orderParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
logger.log(`🛡️ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`)
logger.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
logger.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`)
logger.log(` ⚠️ May not fill during fast moves - use for liquid markets only!`)
logger.log(`🔄 Executing SL trigger-limit placement...`)
const sig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(orderParams)
)
logger.log('✅ SL trigger-limit order placed:', sig)
signatures.push(sig)
} else {
// TRIGGER_MARKET: Default, guaranteed execution
const orderParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
logger.log(`🛡️ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`)
logger.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
logger.log(` ✅ Will execute at market price when triggered (may slip but WILL fill)`)
logger.log(`🔄 Executing SL trigger-market placement...`)
const sig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(orderParams)
)
logger.log('✅ SL trigger-market order placed:', sig)
signatures.push(sig)
}
logger.log(`🛡️ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`)
logger.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
logger.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`)
logger.log(` ⚠️ May not fill during fast moves - use for liquid markets only!`)
const sig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(orderParams)
)
logger.log('✅ SL trigger-limit order placed:', sig)
signatures.push(sig)
} else {
// TRIGGER_MARKET: Default, guaranteed execution
const orderParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
logger.log(`🛡️ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`)
logger.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
logger.log(` ✅ Will execute at market price when triggered (may slip but WILL fill)`)
const sig = await retryWithBackoff(async () =>
await (driftClient as any).placePerpOrder(orderParams)
)
logger.log('✅ SL trigger-market order placed:', sig)
signatures.push(sig)
} catch (slError) {
console.error(`❌ CRITICAL: Failed to place stop loss:`, slError)
throw new Error(`Stop loss placement failed: ${slError instanceof Error ? slError.message : 'Unknown error'}`)
}
}
} else {
logger.log('⚠️ SL size below market min, skipping on-chain SL')
}
// CRITICAL VALIDATION (Bug #76 fix): Verify all expected orders were placed
if (signatures.length < expectedOrderCount) {
const errorMsg = `MISSING EXIT ORDERS: Expected ${expectedOrderCount}, got ${signatures.length}. Position is UNPROTECTED!`
console.error(`${errorMsg}`)
console.error(` Expected: TP1 + TP2 + ${useDualStops ? 'Soft SL + Hard SL' : 'SL'}`)
console.error(` Got ${signatures.length} signatures:`, signatures)
return {
success: false,
error: errorMsg,
signatures // Return partial signatures for debugging
}
}
logger.log(`✅ All ${expectedOrderCount} exit orders placed successfully`)
return { success: true, signatures }
} catch (error) {
console.error('❌ Failed to place exit orders:', error)

View File

@@ -31,6 +31,9 @@ class DriftStateVerifier {
private isRunning: boolean = false
private checkIntervalMs: number = 10 * 60 * 1000 // 10 minutes
private intervalId: NodeJS.Timeout | null = null
// BUG #80 FIX: Track close attempts per symbol to enforce cooldown
private recentCloseAttempts: Map<string, number> = new Map()
private readonly COOLDOWN_MS = 5 * 60 * 1000 // 5 minutes
/**
* Start the periodic verification service
@@ -215,14 +218,30 @@ class DriftStateVerifier {
/**
* Retry closing a position that should be closed but isn't
* CRITICAL FIX (Dec 9, 2025): Stop retry loop if close transaction confirms
* BUG #80 FIX: Enhanced cooldown enforcement to prevent retry loops
*/
private async retryClose(mismatch: DriftStateMismatch): Promise<void> {
console.log(`🔄 Retrying close for ${mismatch.symbol}...`)
try {
// CRITICAL: Check if this trade already has a close attempt in progress
// If we recently tried to close (within 5 minutes), SKIP to avoid retry loop
// BUG #80 FIX: Check in-memory cooldown map first (faster than DB query)
const lastAttemptTime = this.recentCloseAttempts.get(mismatch.symbol)
if (lastAttemptTime) {
const timeSinceAttempt = Date.now() - lastAttemptTime
if (timeSinceAttempt < this.COOLDOWN_MS) {
const remainingCooldown = Math.ceil((this.COOLDOWN_MS - timeSinceAttempt) / 1000)
console.log(` ⏸️ COOLDOWN ACTIVE: Last attempt ${(timeSinceAttempt / 1000).toFixed(0)}s ago`)
console.log(` ⏳ Must wait ${remainingCooldown}s more before retry (5min cooldown)`)
console.log(` 📊 Cooldown map state: ${Array.from(this.recentCloseAttempts.entries()).map(([s, t]) => `${s}:${Date.now()-t}ms`).join(', ')}`)
return
} else {
console.log(` ✅ Cooldown expired (${(timeSinceAttempt / 1000).toFixed(0)}s since last attempt)`)
}
}
// ALSO check database for persistent cooldown tracking (survives restarts)
const prisma = getPrismaClient()
const trade = await prisma.trade.findUnique({
where: { id: mismatch.tradeId },
@@ -241,13 +260,23 @@ class DriftStateVerifier {
const timeSinceRetry = Date.now() - lastRetryTime.getTime()
// If we retried within last 5 minutes, SKIP (Drift propagation delay)
if (timeSinceRetry < 5 * 60 * 1000) {
console.log(` ⏳ Skipping retry - last attempt ${(timeSinceRetry / 1000).toFixed(0)}s ago (Drift propagation delay)`)
if (timeSinceRetry < this.COOLDOWN_MS) {
console.log(` ⏸️ DATABASE COOLDOWN: Last DB retry ${(timeSinceRetry / 1000).toFixed(0)}s ago`)
console.log(` ⏳ Drift propagation delay - skipping retry`)
// Update in-memory map to match DB state
this.recentCloseAttempts.set(mismatch.symbol, lastRetryTime.getTime())
return
}
}
}
console.log(` 🚀 Proceeding with close attempt...`)
// Record attempt time BEFORE calling closePosition
const attemptTime = Date.now()
this.recentCloseAttempts.set(mismatch.symbol, attemptTime)
const result = await closePosition({
symbol: mismatch.symbol,
percentToClose: 100,
@@ -268,15 +297,20 @@ class DriftStateVerifier {
configSnapshot: {
...trade?.configSnapshot as any,
retryCloseAttempted: true,
retryCloseTime: new Date().toISOString(),
retryCloseTime: new Date(attemptTime).toISOString(),
}
}
})
console.log(` 📝 Cooldown recorded: ${mismatch.symbol}${new Date(attemptTime).toISOString()}`)
} else {
console.error(` ❌ Failed to close ${mismatch.symbol}: ${result.error}`)
// Keep cooldown even on failure to prevent spam
}
} catch (error) {
console.error(` ❌ Error retrying close for ${mismatch.symbol}:`, error)
// On error, still record attempt time to prevent rapid retries
this.recentCloseAttempts.set(mismatch.symbol, Date.now())
}
}

View File

@@ -267,31 +267,89 @@ export class PositionManager {
// Start monitoring if not already running
if (!this.isMonitoring && this.activeTrades.size > 0) {
await this.startMonitoring()
// BUG #77 FIX: Verify monitoring actually started
if (this.activeTrades.size > 0 && !this.isMonitoring) {
const errorMsg = `CRITICAL: Failed to start monitoring! activeTrades=${this.activeTrades.size}, isMonitoring=${this.isMonitoring}`
console.error(`${errorMsg}`)
// Log to persistent file
const { logCriticalError } = await import('../utils/persistent-logger')
await logCriticalError('MONITORING_START_FAILED', {
activeTradesCount: this.activeTrades.size,
isMonitoring: this.isMonitoring,
symbols: Array.from(this.activeTrades.values()).map(t => t.symbol),
tradeIds: Array.from(this.activeTrades.keys())
})
throw new Error(errorMsg)
}
logger.log(`✅ Monitoring verification passed: isMonitoring=${this.isMonitoring}`)
}
}
/**
* Remove a trade from monitoring
* BUG #78 FIX: Safely handle order cancellation to avoid removing active position orders
*/
async removeTrade(tradeId: string): Promise<void> {
const trade = this.activeTrades.get(tradeId)
if (trade) {
logger.log(`🗑️ Removing trade: ${trade.symbol}`)
// Cancel all orders for this symbol (cleanup orphaned orders)
// BUG #78 FIX: Check Drift position size before canceling orders
// If Drift shows an open position, DON'T cancel orders (may belong to active position)
try {
const { cancelAllOrders } = await import('../drift/orders')
const cancelResult = await cancelAllOrders(trade.symbol)
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
logger.log(`✅ Cancelled ${cancelResult.cancelledCount} orphaned orders`)
const driftService = getDriftService()
const marketConfig = getMarketConfig(trade.symbol)
// Query Drift for current position
const driftPosition = await driftService.getPosition(marketConfig.driftMarketIndex)
if (driftPosition && Math.abs(driftPosition.size) >= 0.01) {
// Position still open on Drift - DO NOT cancel orders
console.warn(`⚠️ SAFETY CHECK: ${trade.symbol} position still open on Drift (size: ${driftPosition.size})`)
console.warn(` Skipping order cancellation to avoid removing active position protection`)
console.warn(` Removing from tracking only`)
// Just remove from map, don't cancel orders
this.activeTrades.delete(tradeId)
// Log for monitoring
const { logCriticalError } = await import('../utils/persistent-logger')
await logCriticalError('ORPHAN_REMOVAL_SKIPPED_ACTIVE_POSITION', {
tradeId,
symbol: trade.symbol,
driftSize: driftPosition.size,
reason: 'Drift position still open - preserved orders for safety'
})
} else {
// Position confirmed closed on Drift - safe to cancel orders
logger.log(`✅ Drift position confirmed closed (size: ${driftPosition?.size || 0})`)
logger.log(` Safe to cancel remaining orders`)
const { cancelAllOrders } = await import('../drift/orders')
const cancelResult = await cancelAllOrders(trade.symbol)
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
logger.log(`✅ Cancelled ${cancelResult.cancelledCount} orphaned orders`)
} else if (!cancelResult.success) {
console.error(`❌ Failed to cancel orders: ${cancelResult.error}`)
} else {
logger.log(` No orders to cancel`)
}
this.activeTrades.delete(tradeId)
}
} catch (error) {
console.error('❌ Failed to cancel orders during trade removal:', error)
// Continue with removal even if cancel fails
console.error('❌ Error checking Drift position during trade removal:', error)
console.warn('⚠️ Removing from tracking without canceling orders (safety first)')
// On error, err on side of caution - don't cancel orders
this.activeTrades.delete(tradeId)
}
this.activeTrades.delete(tradeId)
// Stop monitoring if no more trades
if (this.activeTrades.size === 0 && this.isMonitoring) {
this.stopMonitoring()
@@ -481,6 +539,7 @@ export class PositionManager {
*/
private async startMonitoring(): Promise<void> {
if (this.isMonitoring) {
logger.log('⚠️ Monitoring already active, skipping duplicate start')
return
}
@@ -490,28 +549,49 @@ export class PositionManager {
)]
if (symbols.length === 0) {
logger.log('⚠️ No symbols to monitor, skipping start')
return
}
logger.log('🚀 Starting price monitoring for:', symbols)
logger.log('🚀 Starting price monitoring...')
logger.log(` Active trades: ${this.activeTrades.size}`)
logger.log(` Symbols: ${symbols.join(', ')}`)
logger.log(` Current isMonitoring: ${this.isMonitoring}`)
const priceMonitor = getPythPriceMonitor()
await priceMonitor.start({
symbols,
onPriceUpdate: async (update: PriceUpdate) => {
await this.handlePriceUpdate(update)
},
onError: (error: Error) => {
console.error('❌ Price monitor error:', error)
},
})
try {
logger.log('📡 Calling priceMonitor.start()...')
await priceMonitor.start({
symbols,
onPriceUpdate: async (update: PriceUpdate) => {
await this.handlePriceUpdate(update)
},
onError: (error: Error) => {
console.error('❌ Price monitor error:', error)
},
})
this.isMonitoring = true
logger.log('✅ Position monitoring active')
// Schedule periodic validation to detect and cleanup ghost positions
this.scheduleValidation()
this.isMonitoring = true
logger.log('✅ Position monitoring active')
logger.log(` isMonitoring flag set to: ${this.isMonitoring}`)
// Schedule periodic validation to detect and cleanup ghost positions
this.scheduleValidation()
} catch (error) {
console.error('❌ CRITICAL: Failed to start price monitoring:', error)
// Log error to persistent file
const { logCriticalError } = await import('../utils/persistent-logger')
await logCriticalError('PRICE_MONITOR_START_FAILED', {
symbols,
activeTradesCount: this.activeTrades.size,
error: error instanceof Error ? error.message : String(error)
})
throw error // Re-throw so caller knows monitoring failed
}
}
/**