/** * 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, 1hr, 2hr, 4hr, 8hr intervals * - TP1/TP2/SL hit detection using ATR-based targets * - Max favorable/adverse excursion tracking * - Automatic analysis completion after 8 hours or TP/SL hit * - Background job runs every 5 minutes * * EXTENDED TRACKING (Dec 2, 2025): * - Previously tracked for 30 minutes only (missed slow developers) * - Now tracks for 8 hours to capture low ADX signals that take 4+ hours * - User directive: "30 minutes...simply not long enough to know whats going to happen" * - Purpose: Accurate win rate data for quality 80-89 signals */ 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 priceAfter1Hr: number | null priceAfter2Hr: number | null priceAfter4Hr: number | null priceAfter8Hr: 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 48 hours (extended for 8hr tracking) // 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() - 48 * 60 * 60 * 1000) // Last 48 hours (8hr tracking + buffer) } }, 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 console.log(` 📍 ${signal.symbol} ${signal.direction} @ 30min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } // EXTENDED TRACKING (Dec 2, 2025): Track up to 8 hours for slow developers if (elapsedMinutes >= 60 && !signal.priceAfter1Hr) { updates.priceAfter1Hr = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 1hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 120 && !signal.priceAfter2Hr) { updates.priceAfter2Hr = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 2hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 240 && !signal.priceAfter4Hr) { updates.priceAfter4Hr = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 4hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } if (elapsedMinutes >= 480 && !signal.priceAfter8Hr) { updates.priceAfter8Hr = currentPrice console.log(` 📍 ${signal.symbol} ${signal.direction} @ 8hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`) } // Mark complete after 8 hours OR if TP/SL already hit if (elapsedMinutes >= 480 && !signal.analysisComplete) { updates.analysisComplete = true console.log(` ✅ ${signal.symbol} ${signal.direction} @ 8hr: TRACKING COMPLETE`) } // Early completion if TP1/TP2/SL hit (no need to wait full 8 hours) if (!signal.analysisComplete && (signal.wouldHitTP1 || signal.wouldHitTP2 || signal.wouldHitSL)) { updates.analysisComplete = true const hitReason = signal.wouldHitTP1 ? 'TP1' : signal.wouldHitTP2 ? 'TP2' : 'SL' console.log(` ✅ ${signal.symbol} ${signal.direction}: ${hitReason} hit at ${profitPercent.toFixed(2)}% - TRACKING 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() } }