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:
166
app/api/analytics/signal-tracking/route.ts
Normal file
166
app/api/analytics/signal-tracking/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user