/** * Smart Entry Timer Service * * Implements Phase 2: Smart Entry Timing * Waits up to 2 minutes for favorable pullback after signal arrival * * Strategy: * - LONG: Wait for 0.15-0.5% dip below signal price * - SHORT: Wait for 0.15-0.5% bounce above signal price * - Validate ADX hasn't dropped >2 points (trend still strong) * - Timeout at 2 minutes β†’ execute at whatever price * * Expected improvement: 0.2-0.5% per trade = $1,600-4,000 over 100 trades */ import { getMarketDataCache } from './market-data-cache' import { logger } from '../utils/logger' import { getPythPriceMonitor } from '../pyth/price-monitor' export interface QueuedSignal { id: string symbol: string direction: 'long' | 'short' signalPrice: number signalTime: number receivedAt: number expiresAt: number // Pullback targets targetPullbackMin: number // 0.15% targetPullbackMax: number // 0.50% // ADX validation signalADX: number adxTolerance: number // 2 points // Original signal data (for execution) originalSignalData: { atr?: number adx?: number rsi?: number volumeRatio?: number pricePosition?: number indicatorVersion?: string } // Tracking status: 'pending' | 'executed' | 'cancelled' | 'expired' checksPerformed: number bestPriceObserved: number executedAt?: number executedPrice?: number executionReason?: 'pullback_confirmed' | 'timeout' | 'manual_override' // Quality tracking qualityScore: number // Position sizing (passed from execute endpoint) positionSize?: number // USD amount for position leverage?: number // Leverage for position positionSizeUSD?: number // Alternative field name used by execute endpoint } interface SmartEntryConfig { enabled: boolean maxWaitMs: number // 2 minutes pullbackMin: number // 0.15% pullbackMax: number // 0.50% adxTolerance: number // 2 points monitorIntervalMs: number // 15 seconds } export class SmartEntryTimer { private queuedSignals: Map = new Map() private monitoringInterval: NodeJS.Timeout | null = null private config: SmartEntryConfig constructor() { // Load configuration from ENV this.config = { enabled: process.env.SMART_ENTRY_ENABLED === 'true', maxWaitMs: parseInt(process.env.SMART_ENTRY_MAX_WAIT_MS || '120000'), pullbackMin: parseFloat(process.env.SMART_ENTRY_PULLBACK_MIN || '0.15'), pullbackMax: parseFloat(process.env.SMART_ENTRY_PULLBACK_MAX || '0.50'), adxTolerance: parseFloat(process.env.SMART_ENTRY_ADX_TOLERANCE || '2'), monitorIntervalMs: 15000 // 15 seconds } logger.log('πŸ’‘ Smart Entry Timer initialized:', { enabled: this.config.enabled, maxWait: `${this.config.maxWaitMs / 1000}s`, pullback: `${this.config.pullbackMin}-${this.config.pullbackMax}%`, adxTolerance: `${this.config.adxTolerance} points` }) } /** * Queue a signal for smart entry timing */ queueSignal(signalData: { symbol: string direction: 'long' | 'short' signalPrice: number atr?: number adx?: number rsi?: number volumeRatio?: number pricePosition?: number indicatorVersion?: string qualityScore: number positionSizeUSD?: number // CRITICAL FIX (Dec 13, 2025): Store calculated USD size leverage?: number // CRITICAL FIX (Dec 13, 2025): Store calculated leverage }): QueuedSignal { const now = Date.now() const signal: QueuedSignal = { id: `${signalData.symbol}-${now}`, symbol: signalData.symbol, direction: signalData.direction, signalPrice: signalData.signalPrice, signalTime: now, receivedAt: now, expiresAt: now + this.config.maxWaitMs, targetPullbackMin: this.config.pullbackMin, targetPullbackMax: this.config.pullbackMax, signalADX: signalData.adx || 25, // Default if not provided adxTolerance: this.config.adxTolerance, originalSignalData: { atr: signalData.atr, adx: signalData.adx, rsi: signalData.rsi, volumeRatio: signalData.volumeRatio, pricePosition: signalData.pricePosition, indicatorVersion: signalData.indicatorVersion }, status: 'pending', checksPerformed: 0, bestPriceObserved: signalData.signalPrice, qualityScore: signalData.qualityScore, positionSizeUSD: signalData.positionSizeUSD, // Store calculated size leverage: signalData.leverage, // Store calculated leverage } this.queuedSignals.set(signal.id, signal) logger.log(`πŸ“₯ Smart Entry: Queued signal ${signal.id}`) logger.log(` ${signal.direction.toUpperCase()} ${signal.symbol} @ $${signal.signalPrice.toFixed(2)}`) logger.log(` Target pullback: ${this.config.pullbackMin}-${this.config.pullbackMax}%`) logger.log(` Max wait: ${this.config.maxWaitMs / 1000}s`) // Start monitoring if not already running if (!this.monitoringInterval) { this.startMonitoring() } return signal } /** * Start monitoring loop */ private startMonitoring(): void { if (this.monitoringInterval) return logger.log(`πŸ‘οΈ Smart Entry: Starting monitoring loop (${this.config.monitorIntervalMs / 1000}s interval)`) this.monitoringInterval = setInterval(() => { this.checkAllSignals() }, this.config.monitorIntervalMs) } /** * Stop monitoring loop */ private stopMonitoring(): void { if (this.monitoringInterval) { clearInterval(this.monitoringInterval) this.monitoringInterval = null logger.log(`⏸️ Smart Entry: Monitoring stopped (no active signals)`) } } /** * Check all queued signals */ private async checkAllSignals(): Promise { const now = Date.now() for (const [id, signal] of this.queuedSignals) { if (signal.status !== 'pending') continue signal.checksPerformed++ // Check for timeout if (now >= signal.expiresAt) { logger.log(`⏰ Smart Entry: Timeout for ${signal.symbol} (waited ${this.config.maxWaitMs / 1000}s)`) const priceMonitor = getPythPriceMonitor() const latestPrice = priceMonitor.getCachedPrice(signal.symbol) const currentPrice = latestPrice?.price || signal.signalPrice await this.executeSignal(signal, currentPrice, 'timeout') continue } // Check for optimal entry await this.checkSignalForEntry(signal) } } /** * Check if signal should be executed now */ private async checkSignalForEntry(signal: QueuedSignal): Promise { // Get current price const priceMonitor = getPythPriceMonitor() const latestPrice = priceMonitor.getCachedPrice(signal.symbol) if (!latestPrice || !latestPrice.price) { logger.log(`⚠️ Smart Entry: No price available for ${signal.symbol}, skipping check`) return } const currentPrice = latestPrice.price // Update best price if (signal.direction === 'long' && currentPrice < signal.bestPriceObserved) { signal.bestPriceObserved = currentPrice } else if (signal.direction === 'short' && currentPrice > signal.bestPriceObserved) { signal.bestPriceObserved = currentPrice } // Calculate pullback magnitude let pullbackMagnitude: number if (signal.direction === 'long') { // LONG: Want price BELOW signal (pullback = dip) pullbackMagnitude = ((signal.signalPrice - currentPrice) / signal.signalPrice) * 100 } else { // SHORT: Want price ABOVE signal (pullback = bounce) pullbackMagnitude = ((currentPrice - signal.signalPrice) / signal.signalPrice) * 100 } // Log check logger.log(`πŸ” Smart Entry: Checking ${signal.symbol} (check #${signal.checksPerformed})`) logger.log(` Signal: $${signal.signalPrice.toFixed(2)} β†’ Current: $${currentPrice.toFixed(2)}`) logger.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`) // Check if pullback is in target range if (pullbackMagnitude < signal.targetPullbackMin) { logger.log(` ⏳ Waiting for pullback (${pullbackMagnitude.toFixed(2)}% < ${signal.targetPullbackMin}%)`) return } if (pullbackMagnitude > signal.targetPullbackMax) { logger.log(` ⚠️ Pullback too large (${pullbackMagnitude.toFixed(2)}% > ${signal.targetPullbackMax}%), might be reversal - waiting`) return } // ============================================================ // PHASE 7.2: REAL-TIME QUALITY VALIDATION (Nov 27, 2025) // ============================================================ // Re-validate signal quality before entry using fresh market data // Prevents execution if conditions degraded during wait period const marketCache = getMarketDataCache() const latestMetrics = marketCache.get(signal.symbol) if (latestMetrics) { const now = Date.now() const dataAge = (now - latestMetrics.timestamp) / 1000 logger.log(` πŸ“Š Real-time validation (data age: ${dataAge.toFixed(0)}s):`) // 1. ADX degradation check (original logic) if (latestMetrics.adx) { const adxDrop = signal.signalADX - latestMetrics.adx if (adxDrop > signal.adxTolerance) { logger.log(` ❌ ADX degraded: ${signal.signalADX.toFixed(1)} β†’ ${latestMetrics.adx.toFixed(1)} (dropped ${adxDrop.toFixed(1)} points, max ${signal.adxTolerance})`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: ADX degradation exceeded tolerance`) return } logger.log(` βœ… ADX: ${signal.signalADX.toFixed(1)} β†’ ${latestMetrics.adx.toFixed(1)} (within tolerance)`) } // 2. Volume degradation check (NEW) // If volume drops significantly, momentum may be fading if (signal.originalSignalData.volumeRatio && latestMetrics.volumeRatio) { const originalVolume = signal.originalSignalData.volumeRatio const currentVolume = latestMetrics.volumeRatio const volumeDrop = ((originalVolume - currentVolume) / originalVolume) * 100 // Cancel if volume dropped >40% if (volumeDrop > 40) { logger.log(` ❌ Volume collapsed: ${originalVolume.toFixed(2)}x β†’ ${currentVolume.toFixed(2)}x (${volumeDrop.toFixed(0)}% drop)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: Volume degradation - momentum fading`) return } logger.log(` βœ… Volume: ${originalVolume.toFixed(2)}x β†’ ${currentVolume.toFixed(2)}x`) } // 3. RSI reversal check (NEW) // If RSI crossed into opposite territory, trend may be reversing if (signal.originalSignalData.rsi && latestMetrics.rsi) { const originalRSI = signal.originalSignalData.rsi const currentRSI = latestMetrics.rsi if (signal.direction === 'long') { // LONG: Cancel if RSI dropped into oversold (<30) if (originalRSI >= 40 && currentRSI < 30) { logger.log(` ❌ RSI collapsed: ${originalRSI.toFixed(1)} β†’ ${currentRSI.toFixed(1)} (now oversold)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`) return } } else { // SHORT: Cancel if RSI rose into overbought (>70) if (originalRSI <= 60 && currentRSI > 70) { logger.log(` ❌ RSI spiked: ${originalRSI.toFixed(1)} β†’ ${currentRSI.toFixed(1)} (now overbought)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`) return } } logger.log(` βœ… RSI: ${originalRSI.toFixed(1)} β†’ ${currentRSI.toFixed(1)}`) } // 4. MAGAP divergence check (NEW) // If MA gap widened in opposite direction, structure changing if (latestMetrics.maGap !== undefined) { const currentMAGap = latestMetrics.maGap if (signal.direction === 'long' && currentMAGap < -1.0) { // LONG but MAs now bearish diverging logger.log(` ❌ MA structure bearish: MAGAP ${currentMAGap.toFixed(2)}% (death cross accelerating)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: MA structure turned bearish`) return } if (signal.direction === 'short' && currentMAGap > 1.0) { // SHORT but MAs now bullish diverging logger.log(` ❌ MA structure bullish: MAGAP ${currentMAGap.toFixed(2)}% (golden cross accelerating)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) logger.log(` 🚫 Signal cancelled: MA structure turned bullish`) return } logger.log(` βœ… MAGAP: ${currentMAGap.toFixed(2)}%`) } logger.log(` βœ… All real-time validations passed - signal quality maintained`) } else { logger.log(` ⚠️ No fresh market data available - proceeding with original signal`) } // All conditions met - execute! logger.log(`βœ… Smart Entry: OPTIMAL ENTRY CONFIRMED`) logger.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`) logger.log(` Price improvement: $${signal.signalPrice.toFixed(2)} β†’ $${currentPrice.toFixed(2)}`) await this.executeSignal(signal, currentPrice, 'pullback_confirmed') } /** * Execute the trade with actual entry price */ private async executeSignal( signal: QueuedSignal, entryPrice: number, reason: 'pullback_confirmed' | 'timeout' | 'manual_override' ): Promise { signal.status = 'executed' signal.executedAt = Date.now() signal.executedPrice = entryPrice signal.executionReason = reason const improvement = ((signal.signalPrice - entryPrice) / signal.signalPrice) * 100 const improvementDirection = signal.direction === 'long' ? improvement : -improvement logger.log(`🎯 Smart Entry: EXECUTING ${signal.direction.toUpperCase()} ${signal.symbol}`) logger.log(` Signal Price: $${signal.signalPrice.toFixed(2)}`) logger.log(` Entry Price: $${entryPrice.toFixed(2)}`) logger.log(` Improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`) // Execute the actual trade through Drift try { const { openPosition, placeExitOrders } = await import('../drift/orders') const { initializeDriftService } = await import('../drift/client') const { createTrade } = await import('../database/trades') const { getInitializedPositionManager } = await import('./position-manager') const { getMergedConfig, getActualPositionSizeForSymbol, SUPPORTED_MARKETS } = await import('../../config/trading') // Get Drift service const driftService = await initializeDriftService() if (!driftService) { console.error('❌ Smart Entry: Drift service not available') return } // Get market config const marketConfig = SUPPORTED_MARKETS[signal.symbol] if (!marketConfig) { console.error(`❌ Smart Entry: No market config for ${signal.symbol}`) return } // CRITICAL FIX (Dec 13, 2025): Use stored positionSizeUSD from queue time // Bug: Recalculating fresh causes $10.40 instead of $435 (97.6% size loss) // Fix: Use stored values when available, only recalculate as fallback const config = getMergedConfig() let positionSizeUSD: number let leverage: number if (signal.positionSizeUSD && signal.leverage) { // Use stored values from queue time (FIX for timeout sizing bug) positionSizeUSD = signal.positionSizeUSD leverage = signal.leverage logger.log(` Using stored position size: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage (queued values)`) } else { // Fallback: Recalculate (backwards compatibility for old queued signals) const sizing = await getActualPositionSizeForSymbol( signal.symbol, config, signal.qualityScore ) positionSizeUSD = sizing.size leverage = sizing.leverage logger.log(` Recalculated position size: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage (fallback)`) } logger.log(` Opening position: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage`) // Open position const openResult = await openPosition({ symbol: signal.symbol, direction: signal.direction, sizeUSD: positionSizeUSD, slippageTolerance: 1.0 // 1% slippage tolerance }) if (!openResult.success || !openResult.transactionSignature) { console.error(`❌ Smart Entry: Position open failed - ${openResult.error}`) return } const fillPrice = openResult.fillPrice! logger.log(`βœ… Smart Entry: Position opened at $${fillPrice.toFixed(2)}`) // Calculate TP/SL prices let tp1Percent = config.takeProfit1Percent let tp2Percent = config.takeProfit2Percent let slPercent = config.stopLossPercent if (config.useAtrBasedTargets && signal.originalSignalData.atr && signal.originalSignalData.atr > 0) { // ATR-based targets tp1Percent = this.calculatePercentFromAtr( signal.originalSignalData.atr, fillPrice, config.atrMultiplierTp1, config.minTp1Percent, config.maxTp1Percent ) tp2Percent = this.calculatePercentFromAtr( signal.originalSignalData.atr, fillPrice, config.atrMultiplierTp2, config.minTp2Percent, config.maxTp2Percent ) slPercent = -Math.abs(this.calculatePercentFromAtr( signal.originalSignalData.atr, fillPrice, config.atrMultiplierSl, config.minSlPercent, config.maxSlPercent )) } const stopLossPrice = this.calculatePrice(fillPrice, slPercent, signal.direction) const tp1Price = this.calculatePrice(fillPrice, tp1Percent, signal.direction) const tp2Price = this.calculatePrice(fillPrice, tp2Percent, signal.direction) const effectiveTp2SizePercent = config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0 ? 0 : (config.takeProfit2SizePercent ?? 0) // Dual stops if enabled let softStopPrice: number | undefined let hardStopPrice: number | undefined if (config.useDualStops) { softStopPrice = this.calculatePrice(fillPrice, config.softStopPercent, signal.direction) hardStopPrice = this.calculatePrice(fillPrice, config.hardStopPercent, signal.direction) } // Place exit orders let exitOrderSignatures: string[] = [] try { const exitRes = await placeExitOrders({ symbol: signal.symbol, positionSizeUSD, entryPrice: fillPrice, tp1Price, tp2Price, stopLossPrice, tp1SizePercent: config.takeProfit1SizePercent ?? 75, tp2SizePercent: effectiveTp2SizePercent, direction: signal.direction, useDualStops: config.useDualStops, softStopPrice, softStopBuffer: config.softStopBuffer, hardStopPrice }) if (exitRes.success) { exitOrderSignatures = exitRes.signatures || [] logger.log(`βœ… Smart Entry: Exit orders placed - ${exitOrderSignatures.length} orders`) } } catch (err) { console.error(`❌ Smart Entry: Error placing exit orders:`, err) } // Save to database let savedTrade try { savedTrade = await createTrade({ positionId: openResult.transactionSignature, symbol: signal.symbol, direction: signal.direction, entryPrice: fillPrice, positionSizeUSD, leverage, stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent, tp2SizePercent: effectiveTp2SizePercent, entryOrderTx: openResult.transactionSignature, atrAtEntry: signal.originalSignalData.atr, adxAtEntry: signal.originalSignalData.adx, rsiAtEntry: signal.originalSignalData.rsi, signalQualityScore: signal.qualityScore, indicatorVersion: signal.originalSignalData.indicatorVersion, signalSource: 'tradingview', tp1OrderTx: exitOrderSignatures[0], tp2OrderTx: exitOrderSignatures[1], slOrderTx: exitOrderSignatures[2], configSnapshot: { leverage, stopLossPercent: slPercent, takeProfit1Percent: tp1Percent, takeProfit2Percent: tp2Percent, useDualStops: config.useDualStops, smartEntry: { used: true, improvement: improvementDirection, waitTime: Math.round((signal.executedAt! - signal.signalTime) / 1000), reason: reason, checksPerformed: signal.checksPerformed } } }) logger.log(`πŸ’Ύ Smart Entry: Trade saved to database (ID: ${savedTrade.id})`) } catch (dbError) { console.error(`❌ Smart Entry: Failed to save trade:`, dbError) const { logCriticalError } = await import('../utils/persistent-logger') logCriticalError('SMART_ENTRY_DATABASE_FAILURE', { error: dbError, symbol: signal.symbol, direction: signal.direction, entryPrice: fillPrice, transactionSignature: openResult.transactionSignature }) // If database save fails, generate synthetic ID as fallback savedTrade = { id: `trade-${Date.now()}` } as any } // Add to Position Manager try { const positionManager = await getInitializedPositionManager() const emergencyStopPrice = this.calculatePrice( fillPrice, -Math.abs(config.emergencyStopPercent), signal.direction ) const activeTrade: import('./position-manager').ActiveTrade = { id: savedTrade.id, // πŸ”§ BUG #88 FIX: Use real Prisma ID from database positionId: openResult.transactionSignature, symbol: signal.symbol, direction: signal.direction, entryPrice: fillPrice, entryTime: Date.now(), positionSize: positionSizeUSD, leverage, stopLossPrice, tp1Price, tp2Price, emergencyStopPrice, currentSize: positionSizeUSD, originalPositionSize: positionSizeUSD, takeProfitPrice1: tp1Price, takeProfitPrice2: tp2Price, tp1Hit: false, tp2Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, trailingStopActive: false, realizedPnL: 0, unrealizedPnL: 0, peakPnL: 0, peakPrice: fillPrice, maxFavorableExcursion: 0, maxAdverseExcursion: 0, maxFavorablePrice: fillPrice, maxAdversePrice: fillPrice, originalAdx: signal.originalSignalData.adx, timesScaled: 0, totalScaleAdded: 0, priceCheckCount: 0, lastPrice: fillPrice, lastUpdateTime: Date.now(), atrAtEntry: signal.originalSignalData.atr, adxAtEntry: signal.originalSignalData.adx, signalQualityScore: signal.qualityScore } await positionManager.addTrade(activeTrade) logger.log(`πŸ“Š Smart Entry: Added to Position Manager`) // CRITICAL FIX (Dec 13, 2025): Send Telegram notification for timeout executions // Bug: Telegram receives "null" when smart entry times out // Fix: Send notification directly from executeSignal since it runs outside API context try { const { sendTelegramMessage } = await import('../notifications/telegram') const timeWaited = Math.round((Date.now() - signal.receivedAt) / 1000) const message = ` 🎯 POSITION OPENED (Smart Entry ${reason}) πŸ“ˆ ${signal.symbol} ${signal.direction.toUpperCase()} πŸ’° Size: $${positionSizeUSD.toFixed(2)} ⚑ Leverage: ${leverage}x 🎯 Quality: ${signal.qualityScore} πŸ“ Entry: $${fillPrice.toFixed(4)} 🎯 TP1: $${tp1Price.toFixed(4)} (+${tp1Percent.toFixed(2)}%) 🎯 TP2: $${tp2Price.toFixed(4)} (+${tp2Percent.toFixed(2)}%) πŸ›‘οΈ SL: $${stopLossPrice.toFixed(4)} (${slPercent.toFixed(2)}%) ⏱️ Wait time: ${timeWaited}s πŸ“Š Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}% πŸ’΅ Value saved: $${(Math.abs(improvementDirection) / 100 * positionSizeUSD).toFixed(2)} ${reason === 'timeout' ? '⏰ Executed at timeout (max wait reached)' : 'βœ… Optimal entry confirmed'} `.trim() await sendTelegramMessage(message) logger.log(`πŸ“± Smart Entry: Telegram notification sent`) } catch (telegramError) { console.error(`❌ Smart Entry: Telegram notification failed:`, telegramError) // Don't fail the trade execution just because notification failed } } catch (pmError) { console.error(`❌ Smart Entry: Failed to add to Position Manager:`, pmError) } logger.log(`βœ… Smart Entry: Execution complete for ${signal.symbol}`) logger.log(` Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`) logger.log(` Estimated value: $${(Math.abs(improvementDirection) / 100 * positionSizeUSD).toFixed(2)}`) } catch (error) { console.error(`❌ Smart Entry: Execution error:`, error) } // Remove from queue after brief delay (for logging) setTimeout(() => { this.queuedSignals.delete(signal.id) logger.log(`πŸ—‘οΈ Smart Entry: Cleaned up signal ${signal.id}`) if (this.queuedSignals.size === 0) { this.stopMonitoring() } }, 5000) } /** * Helper: Calculate price from percentage */ private calculatePrice( entryPrice: number, percent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return entryPrice * (1 + percent / 100) } else { return entryPrice * (1 - percent / 100) } } /** * Helper: Calculate percentage from ATR with safety bounds */ private calculatePercentFromAtr( atrValue: number, entryPrice: number, atrMultiplier: number, minPercent: number, maxPercent: number ): number { const atrPercent = (atrValue / entryPrice) * 100 const targetPercent = atrPercent * atrMultiplier return Math.max(minPercent, Math.min(maxPercent, targetPercent)) } /** * Get status of all queued signals (for debugging) */ getQueueStatus(): QueuedSignal[] { return Array.from(this.queuedSignals.values()) } /** * Cancel a specific queued signal */ cancelSignal(signalId: string): boolean { const signal = this.queuedSignals.get(signalId) if (!signal) { return false } signal.status = 'cancelled' this.queuedSignals.delete(signalId) logger.log(`🚫 Smart Entry: Cancelled signal ${signalId}`) return true } /** * Check if smart entry is enabled */ isEnabled(): boolean { return this.config.enabled } } // Singleton instance let smartEntryTimerInstance: SmartEntryTimer | null = null export function getSmartEntryTimer(): SmartEntryTimer { if (!smartEntryTimerInstance) { smartEntryTimerInstance = new SmartEntryTimer() } return smartEntryTimerInstance } export function startSmartEntryTracking(): void { getSmartEntryTimer() logger.log('βœ… Smart Entry Timer service initialized') }