Files
trading_bot_v4/lib/analysis/blocked-signal-tracker.ts
mindesbunister 6b00303970 fix: BlockedSignalTracker now uses Drift oracle prices instead of Pyth cache
- Changed from getPythPriceMonitor() to initializeDriftService()
- Uses getOraclePrice() with Drift market index
- Skips signals with entryPrice = 0
- Initialize Drift service in trackPrices() before processing
- Price tracking now working: priceAfter1Min/5Min/15Min/30Min fields populate
- analysisComplete transitions to true after 30 minutes
- wouldHitTP1/TP2/SL detection working (based on ATR targets)

Bug: Pyth price cache didn't have SOL-PERP prices, tracker skipped all signals
Fix: Drift oracle prices always available, tracker now functional
Impact: Multi-timeframe data collection now operational for Phase 1 analysis
2025-11-20 10:35:04 +01:00

297 lines
9.1 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 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<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 24 hours
const signals = await this.prisma.blockedSignal.findMany({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
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<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
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()
}
}