Files
trading_bot_v4/lib/analysis/blocked-signal-tracker.ts
mindesbunister 60fc571aa6 feat: Automated multi-timeframe price tracking system
Implemented comprehensive price tracking for multi-timeframe signal analysis.

**Components Added:**
- lib/analysis/blocked-signal-tracker.ts - Background job tracking prices
- app/api/analytics/signal-tracking/route.ts - Status/metrics endpoint

**Features:**
- Automatic price tracking at 1min, 5min, 15min, 30min intervals
- TP1/TP2/SL hit detection using ATR-based targets
- Max favorable/adverse excursion tracking (MFE/MAE)
- Analysis completion after 30 minutes
- Background job runs every 5 minutes
- Entry price captured from signal time

**Database Changes:**
- Added entryPrice field to BlockedSignal (for price tracking baseline)
- Added maxFavorablePrice, maxAdversePrice fields
- Added maxFavorableExcursion, maxAdverseExcursion fields

**Integration:**
- Auto-starts on container startup
- Tracks all DATA_COLLECTION_ONLY signals
- Uses same TP/SL calculation as live trades (ATR-based)
- Calculates profit % based on direction (long vs short)

**API Endpoints:**
- GET /api/analytics/signal-tracking - View tracking status and metrics
- POST /api/analytics/signal-tracking - Manually trigger update (auth required)

**Purpose:**
Enables data-driven multi-timeframe comparison. After 50+ signals per
timeframe, can analyze which timeframe (5min vs 15min vs 1H vs 4H vs Daily)
has best win rate, profit potential, and signal quality.

**What It Tracks:**
- Price at 1min, 5min, 15min, 30min after signal
- Would TP1/TP2/SL have been hit?
- Maximum profit/loss during 30min window
- Complete analysis of signal profitability

**How It Works:**
1. Signal comes in (15min, 1H, 4H, Daily) → saved to BlockedSignal
2. Background job runs every 5min
3. Queries current price from Pyth
4. Calculates profit % from entry
5. Checks if TP/SL thresholds crossed
6. Updates MFE/MAE if new highs/lows
7. After 30min, marks analysisComplete=true

**Future Analysis:**
After 50+ signals per timeframe:
- Compare TP1 hit rates across timeframes
- Identify which timeframe has highest win rate
- Determine optimal signal frequency vs quality trade-off
- Switch production to best-performing timeframe

User requested: "i want all the bells and whistles. lets make the
powerhouse more powerfull. i cant see any reason why we shouldnt"
2025-11-19 17:18:47 +01:00

285 lines
8.7 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 { getPythPriceMonitor } from '../pyth/price-monitor'
import { getMergedConfig } 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 {
// 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
const priceMonitor = getPythPriceMonitor()
const latestPrice = priceMonitor.getCachedPrice(signal.symbol)
if (!latestPrice || !latestPrice.price) {
console.log(`⚠️ No price available for ${signal.symbol}, skipping`)
return
}
const currentPrice = latestPrice.price
const entryPrice = Number(signal.entryPrice)
// 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()
}
}