/** * 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 { 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 } console.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 }): 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 } this.queuedSignals.set(signal.id, signal) console.log(`πŸ“₯ Smart Entry: Queued signal ${signal.id}`) console.log(` ${signal.direction.toUpperCase()} ${signal.symbol} @ $${signal.signalPrice.toFixed(2)}`) console.log(` Target pullback: ${this.config.pullbackMin}-${this.config.pullbackMax}%`) console.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 console.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 console.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) { console.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) { console.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 console.log(`πŸ” Smart Entry: Checking ${signal.symbol} (check #${signal.checksPerformed})`) console.log(` Signal: $${signal.signalPrice.toFixed(2)} β†’ Current: $${currentPrice.toFixed(2)}`) console.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`) // Check if pullback is in target range if (pullbackMagnitude < signal.targetPullbackMin) { console.log(` ⏳ Waiting for pullback (${pullbackMagnitude.toFixed(2)}% < ${signal.targetPullbackMin}%)`) return } if (pullbackMagnitude > signal.targetPullbackMax) { console.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 console.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) { console.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) console.log(` 🚫 Signal cancelled: ADX degradation exceeded tolerance`) return } console.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) { console.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) console.log(` 🚫 Signal cancelled: Volume degradation - momentum fading`) return } console.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) { console.log(` ❌ RSI collapsed: ${originalRSI.toFixed(1)} β†’ ${currentRSI.toFixed(1)} (now oversold)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) console.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`) return } } else { // SHORT: Cancel if RSI rose into overbought (>70) if (originalRSI <= 60 && currentRSI > 70) { console.log(` ❌ RSI spiked: ${originalRSI.toFixed(1)} β†’ ${currentRSI.toFixed(1)} (now overbought)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) console.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`) return } } console.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 console.log(` ❌ MA structure bearish: MAGAP ${currentMAGap.toFixed(2)}% (death cross accelerating)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) console.log(` 🚫 Signal cancelled: MA structure turned bearish`) return } if (signal.direction === 'short' && currentMAGap > 1.0) { // SHORT but MAs now bullish diverging console.log(` ❌ MA structure bullish: MAGAP ${currentMAGap.toFixed(2)}% (golden cross accelerating)`) signal.status = 'cancelled' signal.executionReason = 'manual_override' this.queuedSignals.delete(signal.id) console.log(` 🚫 Signal cancelled: MA structure turned bullish`) return } console.log(` βœ… MAGAP: ${currentMAGap.toFixed(2)}%`) } console.log(` βœ… All real-time validations passed - signal quality maintained`) } else { console.log(` ⚠️ No fresh market data available - proceeding with original signal`) } // All conditions met - execute! console.log(`βœ… Smart Entry: OPTIMAL ENTRY CONFIRMED`) console.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`) console.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 console.log(`🎯 Smart Entry: EXECUTING ${signal.direction.toUpperCase()} ${signal.symbol}`) console.log(` Signal Price: $${signal.signalPrice.toFixed(2)}`) console.log(` Entry Price: $${entryPrice.toFixed(2)}`) console.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 } // Get position size from config const config = getMergedConfig() const { size: positionSizeUSD, leverage } = await getActualPositionSizeForSymbol( signal.symbol, config, signal.qualityScore ) console.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! console.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) // 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: config.takeProfit2SizePercent ?? 0, direction: signal.direction, useDualStops: config.useDualStops, softStopPrice, softStopBuffer: config.softStopBuffer, hardStopPrice }) if (exitRes.success) { exitOrderSignatures = exitRes.signatures || [] console.log(`βœ… Smart Entry: Exit orders placed - ${exitOrderSignatures.length} orders`) } } catch (err) { console.error(`❌ Smart Entry: Error placing exit orders:`, err) } // Save to database try { await createTrade({ positionId: openResult.transactionSignature, symbol: signal.symbol, direction: signal.direction, entryPrice: fillPrice, positionSizeUSD, leverage, stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent, tp2SizePercent: config.takeProfit2SizePercent, 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 } } }) console.log(`πŸ’Ύ Smart Entry: Trade saved to database`) } 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 }) } // 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: `trade-${Date.now()}`, 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) console.log(`πŸ“Š Smart Entry: Added to Position Manager`) } catch (pmError) { console.error(`❌ Smart Entry: Failed to add to Position Manager:`, pmError) } console.log(`βœ… Smart Entry: Execution complete for ${signal.symbol}`) console.log(` Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`) console.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) console.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) console.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() console.log('βœ… Smart Entry Timer service initialized') }