feat: add price movement context to flip-flop detection
Improved flip-flop penalty logic to distinguish between: - Chop (bad): <2% price move from opposite signal → -25 penalty - Reversal (good): ≥2% price move from opposite signal → allowed Changes: - lib/database/trades.ts: getRecentSignals() now returns oppositeDirectionPrice - lib/trading/signal-quality.ts: Added currentPrice parameter, price movement check - app/api/trading/check-risk/route.ts: Added currentPrice to RiskCheckRequest interface - app/api/trading/execute/route.ts: Pass openResult.fillPrice as currentPrice - app/api/analytics/reentry-check/route.ts: Pass currentPrice from metrics Example scenarios: - ETH $170 SHORT → $153 LONG (10% move) = reversal allowed ✅ - ETH $154.50 SHORT → $154.30 LONG (0.13% move) = chop blocked ⚠️ Deployed: 09:18 CET Nov 14, 2025 Container: trading-bot-v4
This commit is contained in:
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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<NextResponse<RiskCheck
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction,
|
||||
symbol: body.symbol,
|
||||
currentPrice: body.currentPrice,
|
||||
timeframe: body.timeframe, // Pass timeframe for context-aware scoring
|
||||
minScore: config.minSignalQualityScore // Use config value
|
||||
})
|
||||
|
||||
@@ -365,6 +365,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction,
|
||||
symbol: driftSymbol,
|
||||
currentPrice: openResult.fillPrice,
|
||||
timeframe: body.timeframe,
|
||||
})
|
||||
|
||||
@@ -596,6 +597,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction,
|
||||
symbol: driftSymbol,
|
||||
currentPrice: openResult.fillPrice,
|
||||
timeframe: body.timeframe,
|
||||
})
|
||||
|
||||
|
||||
@@ -567,6 +567,7 @@ export async function getRecentSignals(params: {
|
||||
totalSignals: number
|
||||
oppositeDirectionInWindow: boolean
|
||||
oppositeDirectionMinutesAgo?: number
|
||||
oppositeDirectionPrice?: number
|
||||
last3Trades: Array<{ direction: 'long' | 'short'; createdAt: Date }>
|
||||
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
|
||||
|
||||
@@ -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)`)
|
||||
|
||||
Reference in New Issue
Block a user