- Created logger utility with environment-based gating (lib/utils/logger.ts) - Replaced 517 console.log statements with logger.log (71% reduction) - Fixed import paths in 15 files (resolved comment-trapped imports) - Added DEBUG_LOGS=false to .env - Achieves 71% immediate log reduction (517/731 statements) - Expected 90% reduction in production when deployed Impact: Reduced I/O blocking, lower log volume in production Risk: LOW (easy rollback, non-invasive) Phase: Phase 1, Task 1.1 (Quick Wins - Console.log Production Gating) Files changed: - NEW: lib/utils/logger.ts (production-safe logging) - NEW: scripts/replace-console-logs.js (automation tool) - Modified: 15 lib/*.ts files (console.log → logger.log) - Modified: .env (DEBUG_LOGS=false) Next: Task 1.2 (Image Size Optimization)
631 lines
22 KiB
TypeScript
631 lines
22 KiB
TypeScript
/**
|
||
* 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 { logger } from '../utils/logger'
|
||
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) {
|
||
logger.log('⚠️ Blocked signal tracker already running')
|
||
return
|
||
}
|
||
|
||
logger.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
|
||
|
||
logger.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
|
||
logger.log('⏹️ Blocked signal tracker stopped')
|
||
}
|
||
|
||
/**
|
||
* Main tracking logic - processes all incomplete blocked signals
|
||
*/
|
||
private async trackPrices(): Promise<void> {
|
||
try {
|
||
// Initialize Drift service if needed
|
||
const driftService = await initializeDriftService()
|
||
if (!driftService) {
|
||
logger.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) {
|
||
logger.log('📊 No blocked signals to track')
|
||
return
|
||
}
|
||
|
||
logger.log(`📊 Tracking ${signals.length} blocked signals...`)
|
||
|
||
for (const signal of signals) {
|
||
await this.trackSignal(signal as any)
|
||
}
|
||
|
||
logger.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<void> {
|
||
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) {
|
||
logger.log(`⚠️ No market config for ${signal.symbol}, skipping`)
|
||
return
|
||
}
|
||
|
||
const currentPrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
|
||
const entryPrice = Number(signal.entryPrice)
|
||
|
||
if (entryPrice === 0) {
|
||
logger.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
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 1min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 5 && !signal.priceAfter5Min) {
|
||
updates.priceAfter5Min = currentPrice
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 5min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 15 && !signal.priceAfter15Min) {
|
||
updates.priceAfter15Min = currentPrice
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 15min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 30 && !signal.priceAfter30Min) {
|
||
updates.priceAfter30Min = currentPrice
|
||
logger.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
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 1hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 120 && !signal.priceAfter2Hr) {
|
||
updates.priceAfter2Hr = currentPrice
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 2hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 240 && !signal.priceAfter4Hr) {
|
||
updates.priceAfter4Hr = currentPrice
|
||
logger.log(` 📍 ${signal.symbol} ${signal.direction} @ 4hr: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (elapsedMinutes >= 480 && !signal.priceAfter8Hr) {
|
||
updates.priceAfter8Hr = currentPrice
|
||
logger.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
|
||
logger.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'
|
||
logger.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
|
||
logger.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
|
||
logger.log(` 🎯 ${signal.symbol} ${signal.direction} hit TP2 (${profitPercent.toFixed(2)}%)`)
|
||
}
|
||
|
||
if (signal.wouldHitSL === null && profitPercent <= -slPercent) {
|
||
updates.wouldHitSL = true
|
||
logger.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 }
|
||
}
|
||
|
||
/**
|
||
* Query all 1-minute price data for a signal's tracking window
|
||
* Purpose: Get minute-by-minute granular data instead of 8 polling checkpoints
|
||
* Returns: Array of MarketData objects with price, timestamp, ATR, ADX, etc.
|
||
*/
|
||
private async getHistoricalPrices(
|
||
symbol: string,
|
||
startTime: Date,
|
||
endTime: Date
|
||
): Promise<any[]> {
|
||
try {
|
||
const marketData = await this.prisma.marketData.findMany({
|
||
where: {
|
||
symbol,
|
||
timeframe: '1', // 1-minute data
|
||
timestamp: {
|
||
gte: startTime,
|
||
lte: endTime
|
||
}
|
||
},
|
||
orderBy: {
|
||
timestamp: 'asc' // Chronological order
|
||
}
|
||
})
|
||
|
||
logger.log(`📊 Retrieved ${marketData.length} 1-minute data points for ${symbol}`)
|
||
return marketData
|
||
} catch (error) {
|
||
console.error('❌ Error querying historical prices:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Analyze minute-by-minute data to find EXACT timing of TP/SL hits
|
||
* Purpose: Replace 8 polling checkpoints with 480 data point analysis
|
||
* Algorithm:
|
||
* 1. Calculate TP1/TP2/SL target prices
|
||
* 2. Loop through all 1-minute data points:
|
||
* - Calculate profit % for each minute
|
||
* - Check if TP1/TP2/SL hit (first time only)
|
||
* - Record exact timestamp when hit
|
||
* - Track max favorable/adverse prices
|
||
* 3. Return updates object with all findings
|
||
*/
|
||
private async analyzeHistoricalData(
|
||
signal: BlockedSignalWithTracking,
|
||
historicalPrices: any[],
|
||
config: any
|
||
): Promise<any> {
|
||
const updates: any = {}
|
||
const entryPrice = Number(signal.entryPrice)
|
||
const direction = signal.direction
|
||
|
||
// Calculate TP/SL targets using ATR
|
||
const targets = this.calculateTargets(signal.atr || 0, entryPrice, config)
|
||
|
||
// Calculate actual target prices based on direction
|
||
let tp1Price: number, tp2Price: number, slPrice: number
|
||
if (direction === 'long') {
|
||
tp1Price = entryPrice * (1 + targets.tp1Percent / 100)
|
||
tp2Price = entryPrice * (1 + targets.tp2Percent / 100)
|
||
slPrice = entryPrice * (1 - targets.slPercent / 100)
|
||
} else {
|
||
tp1Price = entryPrice * (1 - targets.tp1Percent / 100)
|
||
tp2Price = entryPrice * (1 - targets.tp2Percent / 100)
|
||
slPrice = entryPrice * (1 + targets.slPercent / 100)
|
||
}
|
||
|
||
logger.log(`🎯 Analyzing ${signal.symbol} ${direction}: Entry $${entryPrice.toFixed(2)}, TP1 $${tp1Price.toFixed(2)}, TP2 $${tp2Price.toFixed(2)}, SL $${slPrice.toFixed(2)}`)
|
||
|
||
// Track hits (only record first occurrence)
|
||
let tp1HitTime: Date | null = null
|
||
let tp2HitTime: Date | null = null
|
||
let slHitTime: Date | null = null
|
||
|
||
// Track max favorable/adverse
|
||
let maxFavorablePrice = entryPrice
|
||
let maxAdversePrice = entryPrice
|
||
let maxFavorableExcursion = 0
|
||
let maxAdverseExcursion = 0
|
||
|
||
// Checkpoint tracking (for comparison with old system)
|
||
const checkpoints = {
|
||
'1min': null as number | null,
|
||
'5min': null as number | null,
|
||
'15min': null as number | null,
|
||
'30min': null as number | null,
|
||
'1hr': null as number | null,
|
||
'2hr': null as number | null,
|
||
'4hr': null as number | null,
|
||
'8hr': null as number | null
|
||
}
|
||
|
||
// Process each 1-minute data point
|
||
for (const dataPoint of historicalPrices) {
|
||
const currentPrice = Number(dataPoint.price)
|
||
const timestamp = new Date(dataPoint.timestamp)
|
||
const minutesElapsed = Math.floor((timestamp.getTime() - signal.createdAt.getTime()) / 60000)
|
||
|
||
// Calculate profit percentage
|
||
const profitPercent = this.calculateProfitPercent(entryPrice, currentPrice, direction)
|
||
|
||
// Track max favorable/adverse
|
||
if (profitPercent > maxFavorableExcursion) {
|
||
maxFavorableExcursion = profitPercent
|
||
maxFavorablePrice = currentPrice
|
||
}
|
||
if (profitPercent < maxAdverseExcursion) {
|
||
maxAdverseExcursion = profitPercent
|
||
maxAdversePrice = currentPrice
|
||
}
|
||
|
||
// Check for TP1 hit (first time only)
|
||
if (!tp1HitTime) {
|
||
const tp1Hit = direction === 'long'
|
||
? currentPrice >= tp1Price
|
||
: currentPrice <= tp1Price
|
||
if (tp1Hit) {
|
||
tp1HitTime = timestamp
|
||
logger.log(`✅ TP1 hit at ${timestamp.toISOString()} (${minutesElapsed}min) - Price: $${currentPrice.toFixed(2)}`)
|
||
}
|
||
}
|
||
|
||
// Check for TP2 hit (first time only)
|
||
if (!tp2HitTime) {
|
||
const tp2Hit = direction === 'long'
|
||
? currentPrice >= tp2Price
|
||
: currentPrice <= tp2Price
|
||
if (tp2Hit) {
|
||
tp2HitTime = timestamp
|
||
logger.log(`✅ TP2 hit at ${timestamp.toISOString()} (${minutesElapsed}min) - Price: $${currentPrice.toFixed(2)}`)
|
||
}
|
||
}
|
||
|
||
// Check for SL hit (first time only)
|
||
if (!slHitTime) {
|
||
const slHit = direction === 'long'
|
||
? currentPrice <= slPrice
|
||
: currentPrice >= slPrice
|
||
if (slHit) {
|
||
slHitTime = timestamp
|
||
logger.log(`❌ SL hit at ${timestamp.toISOString()} (${minutesElapsed}min) - Price: $${currentPrice.toFixed(2)}`)
|
||
}
|
||
}
|
||
|
||
// Record checkpoint prices (for comparison)
|
||
if (minutesElapsed >= 1 && !checkpoints['1min']) checkpoints['1min'] = currentPrice
|
||
if (minutesElapsed >= 5 && !checkpoints['5min']) checkpoints['5min'] = currentPrice
|
||
if (minutesElapsed >= 15 && !checkpoints['15min']) checkpoints['15min'] = currentPrice
|
||
if (minutesElapsed >= 30 && !checkpoints['30min']) checkpoints['30min'] = currentPrice
|
||
if (minutesElapsed >= 60 && !checkpoints['1hr']) checkpoints['1hr'] = currentPrice
|
||
if (minutesElapsed >= 120 && !checkpoints['2hr']) checkpoints['2hr'] = currentPrice
|
||
if (minutesElapsed >= 240 && !checkpoints['4hr']) checkpoints['4hr'] = currentPrice
|
||
if (minutesElapsed >= 480 && !checkpoints['8hr']) checkpoints['8hr'] = currentPrice
|
||
}
|
||
|
||
// Build updates object with findings
|
||
updates.wouldHitTP1 = tp1HitTime !== null
|
||
updates.wouldHitTP2 = tp2HitTime !== null
|
||
updates.wouldHitSL = slHitTime !== null
|
||
|
||
// CRITICAL: Store exact timestamps (minute precision)
|
||
if (tp1HitTime) updates.tp1HitTime = tp1HitTime
|
||
if (tp2HitTime) updates.tp2HitTime = tp2HitTime
|
||
if (slHitTime) updates.slHitTime = slHitTime
|
||
|
||
// Store max favorable/adverse
|
||
updates.maxFavorablePrice = maxFavorablePrice
|
||
updates.maxAdversePrice = maxAdversePrice
|
||
updates.maxFavorableExcursion = maxFavorableExcursion
|
||
updates.maxAdverseExcursion = maxAdverseExcursion
|
||
|
||
// Store checkpoint prices (for comparison with old system)
|
||
if (checkpoints['1min']) updates.priceAfter1Min = checkpoints['1min']
|
||
if (checkpoints['5min']) updates.priceAfter5Min = checkpoints['5min']
|
||
if (checkpoints['15min']) updates.priceAfter15Min = checkpoints['15min']
|
||
if (checkpoints['30min']) updates.priceAfter30Min = checkpoints['30min']
|
||
if (checkpoints['1hr']) updates.priceAfter1Hr = checkpoints['1hr']
|
||
if (checkpoints['2hr']) updates.priceAfter2Hr = checkpoints['2hr']
|
||
if (checkpoints['4hr']) updates.priceAfter4Hr = checkpoints['4hr']
|
||
if (checkpoints['8hr']) updates.priceAfter8Hr = checkpoints['8hr']
|
||
|
||
logger.log(`📊 Analysis complete: TP1=${updates.wouldHitTP1}, TP2=${updates.wouldHitTP2}, SL=${updates.wouldHitSL}, MFE=${maxFavorableExcursion.toFixed(2)}%, MAE=${maxAdverseExcursion.toFixed(2)}%`)
|
||
|
||
return updates
|
||
}
|
||
|
||
/**
|
||
* ONE-TIME BATCH PROCESSING: Process all signals with historical data
|
||
* Purpose: Analyze completed tracking windows using collected MarketData
|
||
* Algorithm:
|
||
* 1. Find signals NOT yet analyzed (analysisComplete = false)
|
||
* 2. Check if enough historical data exists (8 hours or TP/SL hit)
|
||
* 3. Query MarketData for signal's time window
|
||
* 4. Run minute-precision analysis
|
||
* 5. Update database with exact TP/SL timing
|
||
*
|
||
* This replaces the polling approach with batch historical analysis
|
||
*/
|
||
async processCompletedSignals(): Promise<void> {
|
||
try {
|
||
const config = getMergedConfig()
|
||
|
||
// Find signals ready for batch processing
|
||
// CRITICAL FIX (Dec 2, 2025): Changed from 30min to 1min
|
||
// Rationale: We collect 1-minute data, so use it! No reason to wait longer.
|
||
const oneMinuteAgo = new Date(Date.now() - 1 * 60 * 1000)
|
||
|
||
const signalsToProcess = await this.prisma.blockedSignal.findMany({
|
||
where: {
|
||
analysisComplete: false,
|
||
createdAt: {
|
||
lte: oneMinuteAgo // At least 1 minute old (we have 1-min data!)
|
||
},
|
||
blockReason: {
|
||
in: ['DATA_COLLECTION_ONLY', 'QUALITY_SCORE_TOO_LOW']
|
||
}
|
||
},
|
||
orderBy: {
|
||
createdAt: 'asc'
|
||
}
|
||
})
|
||
|
||
if (signalsToProcess.length === 0) {
|
||
logger.log('📊 No signals ready for batch processing')
|
||
return
|
||
}
|
||
|
||
logger.log(`🔄 Processing ${signalsToProcess.length} signals with historical data...`)
|
||
|
||
let processed = 0
|
||
let skipped = 0
|
||
|
||
for (const signal of signalsToProcess) {
|
||
try {
|
||
// Define 8-hour tracking window
|
||
const startTime = signal.createdAt
|
||
const endTime = new Date(startTime.getTime() + 8 * 60 * 60 * 1000)
|
||
|
||
// Query historical 1-minute data
|
||
const historicalPrices = await this.getHistoricalPrices(
|
||
signal.symbol,
|
||
startTime,
|
||
endTime
|
||
)
|
||
|
||
if (historicalPrices.length === 0) {
|
||
logger.log(`⏭️ Skipping ${signal.symbol} ${signal.direction} - no historical data`)
|
||
skipped++
|
||
continue
|
||
}
|
||
|
||
logger.log(`📊 Processing ${signal.symbol} ${signal.direction} with ${historicalPrices.length} data points...`)
|
||
|
||
// Analyze minute-by-minute
|
||
const updates = await this.analyzeHistoricalData(
|
||
signal as any, // Database model has more fields than interface
|
||
historicalPrices,
|
||
config
|
||
)
|
||
|
||
// Mark as complete
|
||
updates.analysisComplete = true
|
||
|
||
// Update database with findings
|
||
await this.prisma.blockedSignal.update({
|
||
where: { id: signal.id },
|
||
data: updates
|
||
})
|
||
|
||
processed++
|
||
logger.log(`✅ ${signal.symbol} ${signal.direction} analyzed successfully`)
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Error processing signal ${signal.id}:`, error)
|
||
skipped++
|
||
}
|
||
}
|
||
|
||
logger.log(`🎉 Batch processing complete: ${processed} analyzed, ${skipped} skipped`)
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error in batch processing:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
}
|