/** * Blocked Signal Price Tracker * * Automatically tracks price movements for blocked signals to determine * if they would have been profitable trades. This enables data-driven * multi-timeframe analysis. * * Features: * - Price tracking at 1min, 5min, 15min, 30min intervals * - TP1/TP2/SL hit detection using ATR-based targets * - Max favorable/adverse excursion tracking * - Automatic analysis completion after 30 minutes * - Background job runs every 5 minutes */ import { getPrismaClient } from '../database/trades' import { initializeDriftService } from '../drift/client' import { getMergedConfig, SUPPORTED_MARKETS } from '../../config/trading' interface BlockedSignalWithTracking { id: string symbol: string direction: 'long' | 'short' entryPrice: number atr: number adx: number createdAt: Date priceAfter1Min: number | null priceAfter5Min: number | null priceAfter15Min: number | null priceAfter30Min: number | null wouldHitTP1: boolean | null wouldHitTP2: boolean | null wouldHitSL: boolean | null maxFavorablePrice: number | null maxAdversePrice: number | null maxFavorableExcursion: number | null maxAdverseExcursion: number | null analysisComplete: boolean } export class BlockedSignalTracker { private prisma = getPrismaClient() private intervalId: NodeJS.Timeout | null = null private isRunning = false /** * Start the background tracking job * Runs every 5 minutes to update price data for blocked signals */ public start(): void { if (this.isRunning) { console.log('âš ī¸ Blocked signal tracker already running') return } console.log('đŸ”Ŧ Starting blocked signal price tracker...') this.isRunning = true // Run immediately on start this.trackPrices().catch(error => { console.error('❌ Error in initial price tracking:', error) }) // Then run every 5 minutes this.intervalId = setInterval(() => { this.trackPrices().catch(error => { console.error('❌ Error in price tracking:', error) }) }, 5 * 60 * 1000) // 5 minutes console.log('✅ Blocked signal tracker started (runs every 5 minutes)') } /** * Stop the background tracking job */ public stop(): void { if (this.intervalId) { clearInterval(this.intervalId) this.intervalId = null } this.isRunning = false console.log('âšī¸ Blocked signal tracker stopped') } /** * Main tracking logic - processes all incomplete blocked signals */ private async trackPrices(): Promise { try { // Initialize Drift service if needed const driftService = await initializeDriftService() if (!driftService) { console.log('âš ī¸ Drift service not available, skipping price tracking') return } // Get all incomplete signals from last 24 hours // Track BOTH quality-blocked AND data collection signals const signals = await this.prisma.blockedSignal.findMany({ where: { blockReason: { in: ['DATA_COLLECTION_ONLY', 'QUALITY_SCORE_TOO_LOW'] }, analysisComplete: false, createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours } }, orderBy: { createdAt: 'asc' } }) if (signals.length === 0) { console.log('📊 No blocked signals to track') return } console.log(`📊 Tracking ${signals.length} blocked signals...`) for (const signal of signals) { await this.trackSignal(signal as any) } console.log(`✅ Price tracking complete for ${signals.length} signals`) } catch (error) { console.error('❌ Error in trackPrices:', error) } } /** * Track a single blocked signal */ private async trackSignal(signal: BlockedSignalWithTracking): Promise { try { const now = Date.now() const signalTime = signal.createdAt.getTime() const elapsedMinutes = (now - signalTime) / (60 * 1000) // Get current price from Drift oracle const driftService = await initializeDriftService() const marketConfig = SUPPORTED_MARKETS[signal.symbol] if (!marketConfig) { console.log(`âš ī¸ No market config for ${signal.symbol}, skipping`) return } const currentPrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex) const entryPrice = Number(signal.entryPrice) if (entryPrice === 0) { console.log(`âš ī¸ Entry price is 0 for ${signal.symbol}, skipping`) return } // Calculate profit percentage const profitPercent = this.calculateProfitPercent( entryPrice, currentPrice, signal.direction ) // Calculate TP/SL levels using ATR const config = getMergedConfig() const { tp1Percent, tp2Percent, slPercent } = this.calculateTargets( Number(signal.atr), entryPrice, config ) // Update prices at intervals const updates: any = {} if (elapsedMinutes >= 1 && !signal.priceAfter1Min) { updates.priceAfter1Min = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 1min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 5 && !signal.priceAfter5Min) { updates.priceAfter5Min = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 5min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 15 && !signal.priceAfter15Min) { updates.priceAfter15Min = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 15min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 30 && !signal.priceAfter30Min) { updates.priceAfter30Min = currentPrice updates.analysisComplete = true console.log(` ✅ ${signal.symbol} ${signal.direction} @ 30min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%) - COMPLETE`) } // Update max favorable/adverse excursion const currentMFE = signal.maxFavorableExcursion || 0 const currentMAE = signal.maxAdverseExcursion || 0 if (profitPercent > currentMFE) { updates.maxFavorableExcursion = profitPercent updates.maxFavorablePrice = currentPrice } if (profitPercent < currentMAE) { updates.maxAdverseExcursion = profitPercent updates.maxAdversePrice = currentPrice } // Check if TP1/TP2/SL would have been hit if (signal.wouldHitTP1 === null && Math.abs(profitPercent) >= tp1Percent) { updates.wouldHitTP1 = profitPercent > 0 console.log(` đŸŽ¯ ${signal.symbol} ${signal.direction} hit ${profitPercent > 0 ? 'TP1' : 'SL'} (${profitPercent.toFixed(2)}%)`) } if (signal.wouldHitTP2 === null && Math.abs(profitPercent) >= tp2Percent) { updates.wouldHitTP2 = profitPercent > 0 console.log(` đŸŽ¯ ${signal.symbol} ${signal.direction} hit TP2 (${profitPercent.toFixed(2)}%)`) } if (signal.wouldHitSL === null && profitPercent <= -slPercent) { updates.wouldHitSL = true console.log(` 🛑 ${signal.symbol} ${signal.direction} hit SL (${profitPercent.toFixed(2)}%)`) } // Update database if we have changes if (Object.keys(updates).length > 0) { await this.prisma.blockedSignal.update({ where: { id: signal.id }, data: updates }) } } catch (error) { console.error(`❌ Error tracking signal ${signal.id}:`, error) } } /** * Calculate profit percentage based on direction */ private calculateProfitPercent( entryPrice: number, currentPrice: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return ((currentPrice - entryPrice) / entryPrice) * 100 } else { return ((entryPrice - currentPrice) / entryPrice) * 100 } } /** * Calculate TP/SL targets using ATR */ private calculateTargets( atr: number, entryPrice: number, config: any ): { tp1Percent: number; tp2Percent: number; slPercent: number } { // ATR as percentage of price const atrPercent = (atr / entryPrice) * 100 // TP1: ATR × 2.0 multiplier let tp1Percent = atrPercent * config.atrMultiplierTp1 tp1Percent = Math.max(config.minTp1Percent, Math.min(config.maxTp1Percent, tp1Percent)) // TP2: ATR × 4.0 multiplier let tp2Percent = atrPercent * config.atrMultiplierTp2 tp2Percent = Math.max(config.minTp2Percent, Math.min(config.maxTp2Percent, tp2Percent)) // SL: ATR × 3.0 multiplier let slPercent = atrPercent * config.atrMultiplierSl slPercent = Math.max(config.minSlPercent, Math.min(config.maxSlPercent, slPercent)) return { tp1Percent, tp2Percent, slPercent } } } // Singleton instance let trackerInstance: BlockedSignalTracker | null = null export function getBlockedSignalTracker(): BlockedSignalTracker { if (!trackerInstance) { trackerInstance = new BlockedSignalTracker() } return trackerInstance } export function startBlockedSignalTracking(): void { const tracker = getBlockedSignalTracker() tracker.start() } export function stopBlockedSignalTracking(): void { if (trackerInstance) { trackerInstance.stop() } }