diff --git a/app/api/analytics/reentry-check/route.ts b/app/api/analytics/reentry-check/route.ts index fe2edfa..0d3ba90 100644 --- a/app/api/analytics/reentry-check/route.ts +++ b/app/api/analytics/reentry-check/route.ts @@ -147,6 +147,7 @@ export async function POST(request: NextRequest) { pricePosition: metrics.pricePosition, direction: direction as 'long' | 'short', symbol: symbol, + currentPrice: metrics.currentPrice, skipFrequencyCheck: true, // Re-entry check already considers recent trades }) diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index 5bce618..04faf5b 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -16,6 +16,7 @@ export interface RiskCheckRequest { symbol: string direction: 'long' | 'short' timeframe?: string // e.g., "5" for 5min, "60" for 1H, "D" for daily + currentPrice?: number // Current market price (for flip-flop context) // Optional context metrics from TradingView atr?: number adx?: number @@ -58,6 +59,7 @@ async function shouldAllowScaling( pricePosition: newSignal.pricePosition, direction: newSignal.direction, symbol: newSignal.symbol, + currentPrice: newSignal.currentPrice, minScore: config.minScaleQualityScore, }) @@ -318,6 +320,7 @@ export async function POST(request: NextRequest): Promise isAlternatingPattern: boolean }> { @@ -581,7 +582,7 @@ export async function getRecentSignals(params: { symbol: params.symbol, createdAt: { gte: timeAgo }, }, - select: { direction: true, createdAt: true }, + select: { direction: true, createdAt: true, entryPrice: true }, orderBy: { createdAt: 'desc' }, }), prisma.blockedSignal.findMany({ @@ -589,15 +590,23 @@ export async function getRecentSignals(params: { symbol: params.symbol, createdAt: { gte: timeAgo }, }, - select: { direction: true, createdAt: true }, + select: { direction: true, createdAt: true, signalPrice: true }, orderBy: { createdAt: 'desc' }, }), ]) - // Combine and sort all signals + // Combine and sort all signals with their prices 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 })), + ...trades.map(t => ({ + direction: t.direction as 'long' | 'short', + createdAt: t.createdAt, + price: t.entryPrice + })), + ...blockedSignals.map(b => ({ + direction: b.direction as 'long' | 'short', + createdAt: b.createdAt, + price: b.signalPrice + })), ].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) // Check for opposite direction in last 15 minutes @@ -629,6 +638,7 @@ export async function getRecentSignals(params: { oppositeDirectionMinutesAgo: oppositeInLast15 ? Math.floor((Date.now() - oppositeInLast15.createdAt.getTime()) / 60000) : undefined, + oppositeDirectionPrice: oppositeInLast15?.price, last3Trades: last3Trades.map(t => ({ direction: t.direction as 'long' | 'short', createdAt: t.createdAt diff --git a/lib/trading/signal-quality.ts b/lib/trading/signal-quality.ts index 84530be..bdd4c1e 100644 --- a/lib/trading/signal-quality.ts +++ b/lib/trading/signal-quality.ts @@ -51,6 +51,7 @@ export async function scoreSignalQuality(params: { pricePosition: number direction: 'long' | 'short' symbol: string // Required for frequency check + currentPrice?: number // Required for flip-flop price context check timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily minScore?: number // Configurable minimum score threshold skipFrequencyCheck?: boolean // For testing or when frequency check not needed @@ -209,7 +210,30 @@ export async function scoreSignalQuality(params: { } // Penalty 2: Flip-flop (opposite direction in last 15 minutes) - if (recentSignals.oppositeDirectionInWindow) { + // BUT: Only penalize if price hasn't moved significantly (< 2% from opposite signal) + // This distinguishes chop (bad) from legitimate reversals (good) + if (recentSignals.oppositeDirectionInWindow && recentSignals.oppositeDirectionPrice) { + const priceChangePercent = Math.abs( + ((params.currentPrice || 0) - recentSignals.oppositeDirectionPrice) / recentSignals.oppositeDirectionPrice * 100 + ) + + if (priceChangePercent < 2.0) { + // Small price move = consolidation/chop = BAD + frequencyPenalties.flipFlop = -25 + score -= 25 + reasons.push( + `⚠️ Flip-flop in tight range: ${recentSignals.oppositeDirectionMinutesAgo}min ago, ` + + `only ${priceChangePercent.toFixed(2)}% move (-25 pts)` + ) + } else { + // Large price move = potential reversal = ALLOW + reasons.push( + `✅ Direction change after ${priceChangePercent.toFixed(1)}% move ` + + `(${recentSignals.oppositeDirectionMinutesAgo}min ago) - reversal allowed` + ) + } + } else if (recentSignals.oppositeDirectionInWindow && !recentSignals.oppositeDirectionPrice) { + // Fallback: If we don't have price data, apply penalty (conservative) frequencyPenalties.flipFlop = -25 score -= 25 reasons.push(`⚠️ Flip-flop detected: opposite direction ${recentSignals.oppositeDirectionMinutesAgo}min ago (-25 pts)`)