Files
trading_bot_v4/app/api/analytics/reentry-check/route.ts
mindesbunister 111e3ed12a feat: implement signal frequency penalties for flip-flop detection
PHASE 1 IMPLEMENTATION:
Signal quality scoring now checks database for recent trading patterns
and applies penalties to prevent overtrading and flip-flop losses.

NEW PENALTIES:
1. Overtrading: 3+ signals in 30min → -20 points
   - Detects consolidation zones where system generates excessive signals
   - Counts both executed trades AND blocked signals

2. Flip-flop: Opposite direction in last 15min → -25 points
   - Prevents rapid long→short→long whipsaws
   - Example: SHORT at 10:00, LONG at 10:12 = blocked

3. Alternating pattern: Last 3 trades flip directions → -30 points
   - Detects choppy market conditions
   - Pattern like long→short→long = system getting chopped

DATABASE INTEGRATION:
- New function: getRecentSignals() in lib/database/trades.ts
- Queries last 30min of trades + blocked signals
- Checks last 3 executed trades for alternating pattern
- Zero performance impact (fast indexed queries)

ARCHITECTURE:
- scoreSignalQuality() now async (requires database access)
- All callers updated: check-risk, execute, reentry-check
- skipFrequencyCheck flag available for special cases
- Frequency penalties included in qualityResult breakdown

EXPECTED IMPACT:
- Eliminate overnight flip-flop losses (like SOL $141-145 chop)
- Reduce overtrading during sideways consolidation
- Better capital preservation in non-trending markets
- Should improve win rate by 5-10% by avoiding worst setups

TESTING:
- Deploy and monitor next 5 signals in choppy markets
- Check logs for frequency penalty messages
- Analyze if blocked signals would have been losers

Files changed:
- lib/database/trades.ts: Added getRecentSignals()
- lib/trading/signal-quality.ts: Made async, added frequency checks
- app/api/trading/check-risk/route.ts: await + symbol parameter
- app/api/trading/execute/route.ts: await + symbol parameter
- app/api/analytics/reentry-check/route.ts: await + skipFrequencyCheck
2025-11-14 06:41:03 +01:00

240 lines
7.5 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 = await scoreSignalQuality({
atr: metrics.atr,
adx: metrics.adx,
rsi: metrics.rsi,
volumeRatio: metrics.volumeRatio,
pricePosition: metrics.pricePosition,
direction: direction as 'long' | 'short',
symbol: symbol,
skipFrequencyCheck: true, // Re-entry check already considers recent trades
})
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 }
)
}
}