diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index 666fb52..879d38a 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -10,6 +10,7 @@ import { getMergedConfig, TradingConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades' import { getPythPriceMonitor } from '@/lib/pyth/price-monitor' +import { scoreSignalQuality, SignalQualityResult } from '@/lib/trading/signal-quality' export interface RiskCheckRequest { symbol: string @@ -320,138 +321,10 @@ export async function POST(request: NextRequest): Promise 0) { - if (params.atr < 0.15) { - score -= 15 - reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`) - } else if (params.atr > 2.5) { - score -= 20 - reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`) - } else if (params.atr >= 0.15 && params.atr < 0.4) { - score += 5 - reasons.push(`ATR moderate (${params.atr.toFixed(2)}%)`) - } else { - score += 10 - reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`) - } - } - - // ADX check (trend strength: want >18) - if (params.adx > 0) { - if (params.adx > 25) { - score += 15 - reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`) - } else if (params.adx < 18) { - score -= 15 - reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`) - } else { - score += 5 - reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`) - } - } - - // RSI check (momentum confirmation) - if (params.rsi > 0) { - if (params.direction === 'long') { - if (params.rsi > 50 && params.rsi < 70) { - score += 10 - reasons.push(`RSI supports long (${params.rsi.toFixed(1)})`) - } else if (params.rsi > 70) { - score -= 10 - reasons.push(`RSI overbought (${params.rsi.toFixed(1)})`) - } - } else { // short - if (params.rsi < 50 && params.rsi > 30) { - score += 10 - reasons.push(`RSI supports short (${params.rsi.toFixed(1)})`) - } else if (params.rsi < 30) { - score -= 10 - reasons.push(`RSI oversold (${params.rsi.toFixed(1)})`) - } - } - } - - // Volume check (want > 1.0 = above average) - if (params.volumeRatio > 0) { - if (params.volumeRatio > 1.5) { - score += 15 - reasons.push(`Very strong volume (${params.volumeRatio.toFixed(2)}x avg)`) - } else if (params.volumeRatio > 1.2) { - score += 10 - reasons.push(`Strong volume (${params.volumeRatio.toFixed(2)}x avg)`) - } else if (params.volumeRatio < 0.8) { - score -= 10 - reasons.push(`Weak volume (${params.volumeRatio.toFixed(2)}x avg)`) - } - } - - // Price position check (avoid chasing vs breakout detection) - if (params.pricePosition > 0) { - if (params.direction === 'long' && params.pricePosition > 95) { - // High volume breakout at range top can be good - if (params.volumeRatio > 1.4) { - score += 5 - reasons.push(`Volume breakout at range top (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x)`) - } else { - score -= 15 - reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`) - } - } else if (params.direction === 'short' && params.pricePosition < 5) { - // High volume breakdown at range bottom can be good - if (params.volumeRatio > 1.4) { - score += 5 - reasons.push(`Volume breakdown at range bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x)`) - } else { - score -= 15 - reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - risky short`) - } - } else { - score += 5 - reasons.push(`Price position OK (${params.pricePosition.toFixed(0)}%)`) - } - } - - // Volume breakout bonus (high volume can override other weaknesses) - if (params.volumeRatio > 1.8 && params.atr < 0.6) { - score += 10 - reasons.push(`Volume breakout compensates for low ATR`) - } - - const minScore = params.minScore ?? 60 // Use config value or default to 60 - return { - passed: score >= minScore, - score, - reasons - } -} diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index 79c8786..ecb6f30 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -12,84 +12,7 @@ import { normalizeTradingViewSymbol } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade, updateTradeExit } from '@/lib/database/trades' - -/** - * Calculate signal quality score (same logic as check-risk endpoint) - */ -function calculateQualityScore(params: { - atr?: number - adx?: number - rsi?: number - volumeRatio?: number - pricePosition?: number - direction: 'long' | 'short' -}): number | undefined { - // If no metrics provided, return undefined - if (!params.atr || params.atr === 0) { - return undefined - } - - let score = 50 // Base score - - // ATR check - if (params.atr < 0.6) { - score -= 15 - } else if (params.atr > 2.5) { - score -= 20 - } else { - score += 10 - } - - // ADX check - if (params.adx && params.adx > 0) { - if (params.adx > 25) { - score += 15 - } else if (params.adx < 18) { - score -= 15 - } else { - score += 5 - } - } - - // RSI check - if (params.rsi && params.rsi > 0) { - if (params.direction === 'long') { - if (params.rsi > 50 && params.rsi < 70) { - score += 10 - } else if (params.rsi > 70) { - score -= 10 - } - } else { - if (params.rsi < 50 && params.rsi > 30) { - score += 10 - } else if (params.rsi < 30) { - score -= 10 - } - } - } - - // Volume check - if (params.volumeRatio && params.volumeRatio > 0) { - if (params.volumeRatio > 1.2) { - score += 10 - } else if (params.volumeRatio < 0.8) { - score -= 10 - } - } - - // Price position check - if (params.pricePosition && params.pricePosition > 0) { - if (params.direction === 'long' && params.pricePosition > 90) { - score -= 15 - } else if (params.direction === 'short' && params.pricePosition < 10) { - score -= 15 - } else { - score += 5 - } - } - - return score -} +import { scoreSignalQuality } from '@/lib/trading/signal-quality' export interface ExecuteTradeRequest { symbol: string // TradingView symbol (e.g., 'SOLUSDT') @@ -381,12 +304,12 @@ export async function POST(request: NextRequest): Promise 0) { + if (params.atr < 0.15) { + score -= 15 + reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`) + } else if (params.atr > 2.5) { + score -= 20 + reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`) + } else if (params.atr >= 0.15 && params.atr < 0.4) { + score += 5 + reasons.push(`ATR moderate (${params.atr.toFixed(2)}%)`) + } else { + score += 10 + reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`) + } + } + + // ADX check (trend strength: want >18) + if (params.adx > 0) { + if (params.adx > 25) { + score += 15 + reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`) + } else if (params.adx < 18) { + score -= 15 + reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`) + } else { + score += 5 + reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`) + } + } + + // RSI check (momentum confirmation) + if (params.rsi > 0) { + if (params.direction === 'long') { + if (params.rsi > 50 && params.rsi < 70) { + score += 10 + reasons.push(`RSI supports long (${params.rsi.toFixed(1)})`) + } else if (params.rsi > 70) { + score -= 10 + reasons.push(`RSI overbought (${params.rsi.toFixed(1)})`) + } + } else { // short + if (params.rsi < 50 && params.rsi > 30) { + score += 10 + reasons.push(`RSI supports short (${params.rsi.toFixed(1)})`) + } else if (params.rsi < 30) { + score -= 10 + reasons.push(`RSI oversold (${params.rsi.toFixed(1)})`) + } + } + } + + // Volume check (want > 1.0 = above average) + if (params.volumeRatio > 0) { + if (params.volumeRatio > 1.5) { + score += 15 + reasons.push(`Very strong volume (${params.volumeRatio.toFixed(2)}x avg)`) + } else if (params.volumeRatio > 1.2) { + score += 10 + reasons.push(`Strong volume (${params.volumeRatio.toFixed(2)}x avg)`) + } else if (params.volumeRatio < 0.8) { + score -= 10 + reasons.push(`Weak volume (${params.volumeRatio.toFixed(2)}x avg)`) + } + } + + // Price position check (avoid chasing vs breakout detection) + if (params.pricePosition > 0) { + if (params.direction === 'long' && params.pricePosition > 95) { + // High volume breakout at range top can be good + if (params.volumeRatio > 1.4) { + score += 5 + reasons.push(`Volume breakout at range top (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x)`) + } else { + score -= 15 + reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`) + } + } else if (params.direction === 'short' && params.pricePosition < 5) { + // High volume breakdown at range bottom can be good + if (params.volumeRatio > 1.4) { + score += 5 + reasons.push(`Volume breakdown at range bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x)`) + } else { + score -= 15 + reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - risky short`) + } + } else { + score += 5 + reasons.push(`Price position OK (${params.pricePosition.toFixed(0)}%)`) + } + } + + // Volume breakout bonus (high volume can override other weaknesses) + if (params.volumeRatio > 1.8 && params.atr < 0.6) { + score += 10 + reasons.push(`Volume breakout compensates for low ATR`) + } + + const minScore = params.minScore || 60 + const passed = score >= minScore + + return { + score, + passed, + reasons, + } +}