Files
trading_bot_v4/lib/analysis/blocked-signal-tracker.ts
mindesbunister 5773d7d36d feat: Extend 1-minute data retention from 4 weeks to 1 year
- Updated lib/maintenance/data-cleanup.ts retention period: 28 days → 365 days
- Storage requirements validated: 251 MB/year (negligible)
- Rationale: 13× more historical data for better pattern analysis
- Benefits: 260-390 blocked signals/year vs 20-30/month
- Cleanup cutoff: Now Dec 2, 2024 (vs Nov 4, 2025 previously)
- Deployment verified: Container restarted, cleanup scheduled for 3 AM daily
2025-12-02 11:55:36 +01:00

343 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<void> {
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<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) {
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()
}
}