diff --git a/app/api/analytics/reentry-check/route.ts b/app/api/analytics/reentry-check/route.ts index 16454cd..fe2edfa 100644 --- a/app/api/analytics/reentry-check/route.ts +++ b/app/api/analytics/reentry-check/route.ts @@ -139,13 +139,15 @@ export async function POST(request: NextRequest) { 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({ + const qualityResult = await scoreSignalQuality({ atr: metrics.atr, adx: metrics.adx, rsi: metrics.rsi, volumeRatio: metrics.volumeRatio, pricePosition: metrics.pricePosition, - direction: direction as 'long' | 'short' + direction: direction as 'long' | 'short', + symbol: symbol, + skipFrequencyCheck: true, // Re-entry check already considers recent trades }) let finalScore = qualityResult.score diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index d338c70..5bce618 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -36,11 +36,11 @@ export interface RiskCheckResponse { * Position Scaling Validation * Determines if adding to an existing position is allowed */ -function shouldAllowScaling( +async function shouldAllowScaling( existingTrade: ActiveTrade, newSignal: RiskCheckRequest, config: TradingConfig -): { allowed: boolean; reasons: string[]; qualityScore?: number; qualityReasons?: string[] } { +): Promise<{ allowed: boolean; reasons: string[]; qualityScore?: number; qualityReasons?: string[] }> { const reasons: string[] = [] // Check if we have context metrics @@ -50,13 +50,14 @@ function shouldAllowScaling( } // 1. Calculate new signal quality score - const qualityScore = scoreSignalQuality({ + const qualityScore = await scoreSignalQuality({ atr: newSignal.atr, adx: newSignal.adx, rsi: newSignal.rsi || 50, volumeRatio: newSignal.volumeRatio || 1, pricePosition: newSignal.pricePosition, direction: newSignal.direction, + symbol: newSignal.symbol, minScore: config.minScaleQualityScore, }) @@ -156,7 +157,7 @@ export async function POST(request: NextRequest): Promise + isAlternatingPattern: boolean +}> { + const prisma = getPrismaClient() + + const timeAgo = new Date(Date.now() - params.timeWindowMinutes * 60 * 1000) + + // Get all signals for this symbol in the time window (including blocked signals) + const [trades, blockedSignals] = await Promise.all([ + prisma.trade.findMany({ + where: { + symbol: params.symbol, + createdAt: { gte: timeAgo }, + }, + select: { direction: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }), + prisma.blockedSignal.findMany({ + where: { + symbol: params.symbol, + createdAt: { gte: timeAgo }, + }, + select: { direction: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }), + ]) + + // Combine and sort all signals + const allSignals = [ + ...trades.map(t => ({ direction: t.direction as 'long' | 'short', createdAt: t.createdAt })), + ...blockedSignals.map(b => ({ direction: b.direction as 'long' | 'short', createdAt: b.createdAt })), + ].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + + // Check for opposite direction in last 15 minutes + const fifteenMinAgo = new Date(Date.now() - 15 * 60 * 1000) + const oppositeInLast15 = allSignals.find( + s => s.direction !== params.direction && s.createdAt >= fifteenMinAgo + ) + + // Get last 3 executed trades (not blocked signals) for alternating pattern check + const last3Trades = await prisma.trade.findMany({ + where: { symbol: params.symbol }, + select: { direction: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + take: 3, + }) + + // Check if last 3 trades alternate (long → short → long OR short → long → short) + let isAlternating = false + if (last3Trades.length === 3) { + const dirs = last3Trades.map(t => t.direction) + isAlternating = ( + (dirs[0] !== dirs[1] && dirs[1] !== dirs[2] && dirs[0] !== dirs[2]) // All different in alternating pattern + ) + } + + return { + totalSignals: allSignals.length, + oppositeDirectionInWindow: !!oppositeInLast15, + oppositeDirectionMinutesAgo: oppositeInLast15 + ? Math.floor((Date.now() - oppositeInLast15.createdAt.getTime()) / 60000) + : undefined, + last3Trades: last3Trades.map(t => ({ + direction: t.direction as 'long' | 'short', + createdAt: t.createdAt + })), + isAlternatingPattern: isAlternating, + } +} + /** * Disconnect Prisma client (for graceful shutdown) */ diff --git a/lib/trading/signal-quality.ts b/lib/trading/signal-quality.ts index 2a53544..84530be 100644 --- a/lib/trading/signal-quality.ts +++ b/lib/trading/signal-quality.ts @@ -5,10 +5,17 @@ * Ensures consistent scoring across the trading pipeline. */ +import { getRecentSignals } from '../database/trades' + export interface SignalQualityResult { score: number passed: boolean reasons: string[] + frequencyPenalties?: { + overtrading: number + flipFlop: number + alternating: number + } } /** @@ -22,6 +29,7 @@ export interface SignalQualityResult { * - Volume: -10 to +15 points * - Price position: -15 to +5 points * - Volume breakout bonus: +10 points + * - Signal frequency penalties: -20 to -30 points * * Total range: ~15-115 points (realistically 30-100) * Threshold: 60 points minimum for execution @@ -29,17 +37,24 @@ export interface SignalQualityResult { * TIMEFRAME-AWARE SCORING: * - 5min charts have lower ADX/ATR thresholds (trends develop slower) * - Higher timeframes require stronger confirmation + * + * SIGNAL FREQUENCY PENALTIES (NEW): + * - 3+ signals in 30 min: -20 points (overtrading zone) + * - Opposite direction in last 15 min: -25 points (flip-flop) + * - Last 3 trades alternating: -30 points (chop pattern) */ -export function scoreSignalQuality(params: { +export async function scoreSignalQuality(params: { atr: number adx: number rsi: number volumeRatio: number pricePosition: number direction: 'long' | 'short' + symbol: string // Required for frequency check timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily minScore?: number // Configurable minimum score threshold -}): SignalQualityResult { + skipFrequencyCheck?: boolean // For testing or when frequency check not needed +}): Promise { let score = 50 // Base score const reasons: string[] = [] @@ -171,6 +186,49 @@ export function scoreSignalQuality(params: { reasons.push(`Volume breakout compensates for low ATR`) } + // Signal frequency penalties (check database for recent signals) + const frequencyPenalties = { + overtrading: 0, + flipFlop: 0, + alternating: 0, + } + + if (!params.skipFrequencyCheck) { + try { + const recentSignals = await getRecentSignals({ + symbol: params.symbol, + direction: params.direction, + timeWindowMinutes: 30, + }) + + // Penalty 1: Overtrading (3+ signals in 30 minutes) + if (recentSignals.totalSignals >= 3) { + frequencyPenalties.overtrading = -20 + score -= 20 + reasons.push(`⚠️ Overtrading zone: ${recentSignals.totalSignals} signals in 30min (-20 pts)`) + } + + // Penalty 2: Flip-flop (opposite direction in last 15 minutes) + if (recentSignals.oppositeDirectionInWindow) { + frequencyPenalties.flipFlop = -25 + score -= 25 + reasons.push(`⚠️ Flip-flop detected: opposite direction ${recentSignals.oppositeDirectionMinutesAgo}min ago (-25 pts)`) + } + + // Penalty 3: Alternating pattern (last 3 trades flip directions) + if (recentSignals.isAlternatingPattern) { + frequencyPenalties.alternating = -30 + score -= 30 + const pattern = recentSignals.last3Trades.map(t => t.direction).join(' → ') + reasons.push(`⚠️ Chop pattern: last 3 trades alternating (${pattern}) (-30 pts)`) + } + } catch (error) { + // Don't fail the whole score if frequency check fails + console.error('❌ Signal frequency check failed:', error) + reasons.push('⚠️ Frequency check unavailable (no penalty applied)') + } + } + const minScore = params.minScore || 60 const passed = score >= minScore @@ -178,5 +236,6 @@ export function scoreSignalQuality(params: { score, passed, reasons, + frequencyPenalties, } }