Files
trading_bot_v4/app/api/analytics/reentry-check/route.ts
mindesbunister 9b767342dc feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics
- Create /api/trading/market-data webhook endpoint for continuous data updates
- Add /api/analytics/reentry-check endpoint for validating manual trades
- Update execute endpoint to auto-cache metrics from incoming signals
- Enhance Telegram bot with pre-execution analytics validation
- Support --force flag to override analytics blocks
- Use fresh ADX/ATR/RSI data when available, fallback to historical
- Apply performance modifiers: -20 for losing streaks, +10 for winning
- Minimum re-entry score 55 (vs 60 for new signals)
- Fail-open design: proceeds if analytics unavailable
- Show data freshness and source in Telegram responses
- Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md

Phase 1 implementation for smart manual trade validation.
2025-11-07 20:40:07 +01:00

238 lines
7.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
import { getPrismaClient } from '@/lib/database/trades'
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
/**
* Re-Entry Analytics Endpoint
*
* Validates manual trades using:
* 1. Fresh TradingView market data (if available)
* 2. Recent trade performance (last 3 trades for symbol + direction)
* 3. Signal quality scoring with performance modifiers
*
* Called by Telegram bot before executing manual "long sol" / "short eth" commands
*/
interface ReentryAnalytics {
should_enter: boolean
score: number
reason: string
data_source: 'tradingview_real' | 'fallback_historical' | 'no_data'
data_age_seconds?: number
metrics: {
atr: number
adx: number
rsi: number
volumeRatio: number
pricePosition: number
timeframe: string
recentTradeStats: {
last3Trades: number
winRate: number
avgPnL: number
}
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { symbol, direction } = body
if (!symbol || !direction) {
return NextResponse.json(
{ error: 'Missing symbol or direction' },
{ status: 400 }
)
}
if (!['long', 'short'].includes(direction)) {
return NextResponse.json(
{ error: 'Direction must be "long" or "short"' },
{ status: 400 }
)
}
console.log(`🔍 Analyzing re-entry for ${direction.toUpperCase()} ${symbol}`)
// 1. Try to get REAL market data from TradingView cache
const marketCache = getMarketDataCache()
const cachedData = marketCache.get(symbol)
let metrics: any
let dataSource: 'tradingview_real' | 'fallback_historical' | 'no_data'
let dataAgeSeconds: number | undefined
if (cachedData) {
// Use REAL TradingView data (less than 5min old)
dataAgeSeconds = Math.round((Date.now() - cachedData.timestamp) / 1000)
dataSource = 'tradingview_real'
console.log(`✅ Using real TradingView data (${dataAgeSeconds}s old)`)
metrics = {
atr: cachedData.atr,
adx: cachedData.adx,
rsi: cachedData.rsi,
volumeRatio: cachedData.volumeRatio,
pricePosition: cachedData.pricePosition,
timeframe: cachedData.timeframe
}
} else {
// Fallback to most recent trade metrics
console.log(`⚠️ No fresh TradingView data, using historical metrics from last trade`)
const prisma = getPrismaClient()
const lastTrade = await prisma.trade.findFirst({
where: { symbol },
orderBy: { createdAt: 'desc' }
}) as any // Trade type has optional metric fields
if (lastTrade && lastTrade.atr && lastTrade.adx && lastTrade.rsi) {
dataSource = 'fallback_historical'
const tradeAge = Math.round((Date.now() - lastTrade.createdAt.getTime()) / 1000)
console.log(`📊 Using metrics from last trade (${tradeAge}s ago)`)
metrics = {
atr: lastTrade.atr,
adx: lastTrade.adx,
rsi: lastTrade.rsi,
volumeRatio: lastTrade.volumeRatio || 1.2,
pricePosition: lastTrade.pricePosition || 50,
timeframe: '5'
}
} else {
// No data available at all
console.log(`❌ No market data available for ${symbol}`)
dataSource = 'no_data'
metrics = {
atr: 1.0,
adx: 20,
rsi: direction === 'long' ? 45 : 55,
volumeRatio: 1.2,
pricePosition: 50,
timeframe: '5'
}
}
}
// 2. Get recent trade performance for this symbol + direction
const prisma = getPrismaClient()
const recentTrades = await prisma.trade.findMany({
where: {
symbol,
direction,
exitTime: { not: null },
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24h
}
},
orderBy: { createdAt: 'desc' },
take: 3
})
const last3Count = recentTrades.length
const winningTrades = recentTrades.filter((t: any) => (t.realizedPnL || 0) > 0)
const winRate = last3Count > 0 ? (winningTrades.length / last3Count) * 100 : 0
const avgPnL = last3Count > 0
? recentTrades.reduce((sum: number, t: any) => sum + (t.realizedPnL || 0), 0) / last3Count
: 0
console.log(`📊 Recent performance: ${last3Count} trades, ${winRate.toFixed(0)}% WR, ${avgPnL.toFixed(2)}% avg P&L`)
// 3. Score the re-entry with real/fallback metrics
const qualityResult = scoreSignalQuality({
atr: metrics.atr,
adx: metrics.adx,
rsi: metrics.rsi,
volumeRatio: metrics.volumeRatio,
pricePosition: metrics.pricePosition,
direction: direction as 'long' | 'short'
})
let finalScore = qualityResult.score
// 4. Apply recent performance modifiers
if (last3Count >= 2 && avgPnL < -5) {
finalScore -= 20
console.log(`⚠️ Recent trades losing (${avgPnL.toFixed(2)}% avg) - applying -20 penalty`)
}
if (last3Count >= 2 && avgPnL > 5 && winRate >= 66) {
finalScore += 10
console.log(`✨ Recent trades winning (${winRate.toFixed(0)}% WR) - applying +10 bonus`)
}
// 5. Penalize if using stale/no data
if (dataSource === 'fallback_historical') {
finalScore -= 5
console.log(`⚠️ Using historical data - applying -5 penalty`)
} else if (dataSource === 'no_data') {
finalScore -= 10
console.log(`⚠️ No market data available - applying -10 penalty`)
}
// 6. Determine if should enter
const MIN_REENTRY_SCORE = 55
const should_enter = finalScore >= MIN_REENTRY_SCORE
let reason = ''
if (!should_enter) {
if (dataSource === 'no_data') {
reason = `No market data available (score: ${finalScore})`
} else if (dataSource === 'fallback_historical') {
reason = `Using stale data (score: ${finalScore})`
} else if (finalScore < MIN_REENTRY_SCORE) {
reason = `Quality score too low (${finalScore} < ${MIN_REENTRY_SCORE})`
}
if (last3Count >= 2 && avgPnL < -5) {
reason += `. Recent ${direction} trades losing (${avgPnL.toFixed(2)}% avg)`
}
} else {
reason = `Quality score acceptable (${finalScore}/${MIN_REENTRY_SCORE})`
if (dataSource === 'tradingview_real') {
reason += ` [✅ FRESH TradingView data: ${dataAgeSeconds}s old]`
} else if (dataSource === 'fallback_historical') {
reason += ` [⚠️ Historical data - consider waiting for fresh signal]`
} else {
reason += ` [❌ No data - risky entry]`
}
if (winRate >= 66 && last3Count >= 2) {
reason += `. Recent win rate: ${winRate.toFixed(0)}%`
}
}
const response: ReentryAnalytics = {
should_enter,
score: finalScore,
reason,
data_source: dataSource,
data_age_seconds: dataAgeSeconds,
metrics: {
...metrics,
recentTradeStats: {
last3Trades: last3Count,
winRate,
avgPnL
}
}
}
console.log(`📊 Re-entry analysis complete:`, {
should_enter,
score: finalScore,
data_source: dataSource
})
return NextResponse.json(response)
} catch (error) {
console.error('❌ Re-entry analysis error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}