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"
This commit is contained in:
mindesbunister
2025-11-19 17:18:47 +01:00
parent e1bce56065
commit 60fc571aa6
7 changed files with 487 additions and 3 deletions

View File

@@ -0,0 +1,166 @@
/**
* Blocked Signal Tracking Status API
*
* GET: View tracking status and statistics
* POST: Manually trigger tracking update (requires auth)
*/
import { NextRequest, NextResponse } from 'next/server'
import { getPrismaClient } from '@/lib/database/trades'
import { getBlockedSignalTracker } from '@/lib/analysis/blocked-signal-tracker'
// GET: View tracking status
export async function GET(request: NextRequest) {
try {
const prisma = getPrismaClient()
// Get tracking statistics
const total = await prisma.blockedSignal.count({
where: { blockReason: 'DATA_COLLECTION_ONLY' }
})
const incomplete = await prisma.blockedSignal.count({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
analysisComplete: false
}
})
const complete = await prisma.blockedSignal.count({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
analysisComplete: true
}
})
// Get completion rates by timeframe
const byTimeframe = await prisma.blockedSignal.groupBy({
by: ['timeframe'],
where: { blockReason: 'DATA_COLLECTION_ONLY' },
_count: { id: true }
})
// Get signals with price data
const withPriceData = await prisma.blockedSignal.count({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
priceAfter1Min: { not: null }
}
})
// Get TP/SL hit rates
const tp1Hits = await prisma.blockedSignal.count({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
wouldHitTP1: true
}
})
const slHits = await prisma.blockedSignal.count({
where: {
blockReason: 'DATA_COLLECTION_ONLY',
wouldHitSL: true
}
})
// Get recent tracked signals
const recentSignals = await prisma.blockedSignal.findMany({
where: { blockReason: 'DATA_COLLECTION_ONLY' },
select: {
id: true,
timeframe: true,
symbol: true,
direction: true,
signalQualityScore: true,
priceAfter1Min: true,
priceAfter5Min: true,
priceAfter15Min: true,
priceAfter30Min: true,
wouldHitTP1: true,
wouldHitTP2: true,
wouldHitSL: true,
analysisComplete: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 10
})
return NextResponse.json({
success: true,
tracking: {
total,
complete,
incomplete,
completionRate: total > 0 ? ((complete / total) * 100).toFixed(1) : '0.0'
},
byTimeframe: byTimeframe.map(tf => ({
timeframe: tf.timeframe,
count: tf._count.id
})),
metrics: {
withPriceData,
tp1Hits,
slHits,
tp1HitRate: complete > 0 ? ((tp1Hits / complete) * 100).toFixed(1) : '0.0',
slHitRate: complete > 0 ? ((slHits / complete) * 100).toFixed(1) : '0.0'
},
recentSignals: recentSignals.map((signal: any) => ({
id: signal.id,
timeframe: `${signal.timeframe}min`,
symbol: signal.symbol,
direction: signal.direction,
quality: signal.signalQualityScore,
price1min: signal.priceAfter1Min,
price5min: signal.priceAfter5Min,
price15min: signal.priceAfter15Min,
price30min: signal.priceAfter30Min,
hitTP1: signal.wouldHitTP1,
hitTP2: signal.wouldHitTP2,
hitSL: signal.wouldHitSL,
complete: signal.analysisComplete,
time: signal.createdAt
}))
})
} catch (error) {
console.error('Error getting signal tracking status:', error)
return NextResponse.json(
{ success: false, error: 'Failed to get tracking status' },
{ status: 500 }
)
}
}
// POST: Manually trigger tracking update
export async function POST(request: NextRequest) {
try {
// Check auth
const authHeader = request.headers.get('Authorization')
const apiKey = process.env.API_SECRET_KEY
if (!authHeader || !apiKey || authHeader !== `Bearer ${apiKey}`) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
const tracker = getBlockedSignalTracker()
// Trigger manual update by restarting
console.log('🔄 Manual tracking update triggered')
tracker.stop()
tracker.start()
return NextResponse.json({
success: true,
message: 'Tracking update triggered'
})
} catch (error) {
console.error('Error triggering tracking update:', error)
return NextResponse.json(
{ success: false, error: 'Failed to trigger update' },
{ status: 500 }
)
}
}

View File

@@ -147,6 +147,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
console.log('🔍 Risk check for:', body)
// 🔬 MULTI-TIMEFRAME DATA COLLECTION
// Allow all non-5min signals to bypass risk checks (they'll be saved as data collection in execute endpoint)
const timeframe = body.timeframe || '5'
if (timeframe !== '5') {
console.log(`📊 DATA COLLECTION: ${timeframe}min signal bypassing risk checks (will save in execute endpoint)`)
return NextResponse.json({
allowed: true,
reason: 'Multi-timeframe data collection',
details: `${timeframe}min signal will be saved for analysis but not executed`,
})
}
const config = getMergedConfig()
// Check for existing positions on the same symbol

View File

@@ -14,6 +14,7 @@ import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/positi
import { createTrade, updateTradeExit } from '@/lib/database/trades'
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
export interface ExecuteTradeRequest {
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
@@ -111,6 +112,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
if (timeframe !== '5') {
console.log(`📊 DATA COLLECTION: ${timeframe}min signal from ${driftSymbol}, saving for analysis (not executing)`)
// Get current price for entry tracking
const priceMonitor = getPythPriceMonitor()
const latestPrice = priceMonitor.getCachedPrice(driftSymbol)
const currentPrice = latestPrice?.price || body.signalPrice || 0
// Save to BlockedSignal for cross-timeframe analysis
const { createBlockedSignal } = await import('@/lib/database/trades')
try {
@@ -125,13 +131,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeRatio: body.volumeRatio,
pricePosition: body.pricePosition,
timeframe: timeframe,
signalPrice: body.signalPrice || 0,
signalPrice: currentPrice,
signalQualityScore: 0, // Not scored since not executed
signalQualityVersion: 'data-collection',
minScoreRequired: 0,
scoreBreakdown: {},
})
console.log(`${timeframe}min signal saved to database for future analysis`)
console.log(`${timeframe}min signal saved at $${currentPrice.toFixed(2)} for future analysis`)
} catch (dbError) {
console.error(`❌ Failed to save ${timeframe}min signal:`, dbError)
}