feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics - Create /api/trading/market-data webhook endpoint for continuous data updates - Add /api/analytics/reentry-check endpoint for validating manual trades - Update execute endpoint to auto-cache metrics from incoming signals - Enhance Telegram bot with pre-execution analytics validation - Support --force flag to override analytics blocks - Use fresh ADX/ATR/RSI data when available, fallback to historical - Apply performance modifiers: -20 for losing streaks, +10 for winning - Minimum re-entry score 55 (vs 60 for new signals) - Fail-open design: proceeds if analytics unavailable - Show data freshness and source in Telegram responses - Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md Phase 1 implementation for smart manual trade validation.
This commit is contained in:
@@ -14,18 +14,14 @@ export interface SignalQualityResult {
|
||||
/**
|
||||
* Calculate signal quality score based on technical indicators
|
||||
*
|
||||
* TIMEFRAME-AWARE SCORING:
|
||||
* 5min charts naturally have lower ADX/ATR than higher timeframes
|
||||
*
|
||||
* Scoring breakdown:
|
||||
* - Base: 50 points
|
||||
* - ATR (volatility): -20 to +10 points (5min: 0.25-0.7% is healthy)
|
||||
* - ADX (trend strength): -15 to +15 points (5min: 15+ is trending)
|
||||
* - ATR (volatility): -20 to +10 points
|
||||
* - ADX (trend strength): -15 to +15 points
|
||||
* - RSI (momentum): -10 to +10 points
|
||||
* - Volume: -10 to +15 points
|
||||
* - Price position: -15 to +5 points
|
||||
* - Volume breakout bonus: +10 points
|
||||
* - Anti-chop filter: -20 points (5min only, extreme chop)
|
||||
*
|
||||
* Total range: ~15-115 points (realistically 30-100)
|
||||
* Threshold: 60 points minimum for execution
|
||||
@@ -38,92 +34,38 @@ export function scoreSignalQuality(params: {
|
||||
pricePosition: number
|
||||
direction: 'long' | 'short'
|
||||
minScore?: number // Configurable minimum score threshold
|
||||
timeframe?: string // e.g., '5', '15', '60', '1D'
|
||||
}): SignalQualityResult {
|
||||
let score = 50 // Base score
|
||||
const reasons: string[] = []
|
||||
|
||||
// Detect 5-minute timeframe
|
||||
const is5min = params.timeframe === '5' || params.timeframe === 'manual'
|
||||
|
||||
// ATR check - TIMEFRAME AWARE
|
||||
// ATR check (volatility gate: 0.15% - 2.5%)
|
||||
if (params.atr > 0) {
|
||||
if (is5min) {
|
||||
// 5min: lower thresholds, more lenient
|
||||
if (params.atr < 0.2) {
|
||||
score -= 15
|
||||
reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`)
|
||||
} else if (params.atr > 1.5) {
|
||||
score -= 20
|
||||
reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`)
|
||||
} else if (params.atr >= 0.2 && params.atr < 0.35) {
|
||||
score += 5
|
||||
reasons.push(`ATR acceptable (${params.atr.toFixed(2)}%)`)
|
||||
} else {
|
||||
score += 10
|
||||
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`)
|
||||
}
|
||||
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 {
|
||||
// Higher timeframes: stricter requirements
|
||||
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)}%)`)
|
||||
}
|
||||
score += 10
|
||||
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`)
|
||||
}
|
||||
}
|
||||
|
||||
// ADX check - TIMEFRAME AWARE
|
||||
// ADX check (trend strength: want >18)
|
||||
if (params.adx > 0) {
|
||||
if (is5min) {
|
||||
// 5min: ADX 15+ is actually trending, 20+ is strong
|
||||
// High volume can compensate for lower ADX in breakouts/breakdowns
|
||||
const hasStrongVolume = params.volumeRatio > 1.2
|
||||
|
||||
if (params.adx > 22) {
|
||||
score += 15
|
||||
reasons.push(`Strong 5min trend (ADX ${params.adx.toFixed(1)})`)
|
||||
} else if (params.adx < 12) {
|
||||
// Reduce penalty if strong volume present (breakdown/breakout in progress)
|
||||
if (hasStrongVolume) {
|
||||
score -= 5
|
||||
reasons.push(`Lower 5min ADX (${params.adx.toFixed(1)}) but strong volume compensates`)
|
||||
} else {
|
||||
score -= 15
|
||||
reasons.push(`Weak 5min trend (ADX ${params.adx.toFixed(1)})`)
|
||||
}
|
||||
} else {
|
||||
score += 5
|
||||
reasons.push(`Moderate 5min trend (ADX ${params.adx.toFixed(1)})`)
|
||||
}
|
||||
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 {
|
||||
// Higher timeframes: stricter ADX requirements
|
||||
const hasStrongVolume = params.volumeRatio > 1.2
|
||||
|
||||
if (params.adx > 25) {
|
||||
score += 15
|
||||
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`)
|
||||
} else if (params.adx < 18) {
|
||||
// Reduce penalty if strong volume present
|
||||
if (hasStrongVolume) {
|
||||
score -= 5
|
||||
reasons.push(`Lower ADX (${params.adx.toFixed(1)}) but strong volume compensates`)
|
||||
} else {
|
||||
score -= 15
|
||||
reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`)
|
||||
}
|
||||
} else {
|
||||
score += 5
|
||||
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
|
||||
}
|
||||
score += 5
|
||||
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +104,7 @@ export function scoreSignalQuality(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Price position check (avoid chasing vs breakout/breakdown detection)
|
||||
// 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
|
||||
@@ -173,35 +115,14 @@ export function scoreSignalQuality(params: {
|
||||
score -= 15
|
||||
reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`)
|
||||
}
|
||||
} else if (params.direction === 'short' && params.pricePosition < 15) {
|
||||
// Shorts near range bottom (< 15%) require strong confirmation
|
||||
// Require STRONG trend (ADX > 18) to avoid false breakdowns in choppy ranges
|
||||
// OR very bearish RSI (< 35) indicating strong momentum continuation
|
||||
const hasStrongTrend = params.adx > 18
|
||||
const isVeryBearish = params.rsi > 0 && params.rsi < 35
|
||||
const hasGoodVolume = params.volumeRatio > 1.2
|
||||
|
||||
if ((hasGoodVolume && hasStrongTrend) || isVeryBearish) {
|
||||
} 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(`Valid breakdown at range bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, ADX ${params.adx.toFixed(1)}, RSI ${params.rsi.toFixed(1)})`)
|
||||
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 (${params.pricePosition.toFixed(0)}%) - need ADX > 18 or RSI < 35 for breakdown`)
|
||||
}
|
||||
} else if (params.direction === 'long' && params.pricePosition < 15) {
|
||||
// Longs near range bottom (< 15%) require strong reversal confirmation
|
||||
// Require STRONG trend (ADX > 18) to avoid catching falling knives
|
||||
// OR very bullish RSI (> 60) after bounce showing momentum shift
|
||||
const hasStrongTrend = params.adx > 18
|
||||
const isVeryBullish = params.rsi > 0 && params.rsi > 60
|
||||
const hasGoodVolume = params.volumeRatio > 1.2
|
||||
|
||||
if ((hasGoodVolume && hasStrongTrend) || isVeryBullish) {
|
||||
score += 5
|
||||
reasons.push(`Potential reversal at bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, ADX ${params.adx.toFixed(1)}, RSI ${params.rsi.toFixed(1)})`)
|
||||
} else {
|
||||
score -= 15
|
||||
reasons.push(`Price near bottom (${params.pricePosition.toFixed(0)}%) - need ADX > 18 or RSI > 60 for reversal`)
|
||||
reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - risky short`)
|
||||
}
|
||||
} else {
|
||||
score += 5
|
||||
@@ -214,12 +135,6 @@ export function scoreSignalQuality(params: {
|
||||
score += 10
|
||||
reasons.push(`Volume breakout compensates for low ATR`)
|
||||
}
|
||||
|
||||
// ANTI-CHOP FILTER for 5min (extreme penalty for sideways chop)
|
||||
if (is5min && params.adx < 10 && params.atr < 0.25 && params.volumeRatio < 0.9) {
|
||||
score -= 20
|
||||
reasons.push(`⛔ Extreme chop detected (ADX ${params.adx.toFixed(1)}, ATR ${params.atr.toFixed(2)}%, Vol ${params.volumeRatio.toFixed(2)}x)`)
|
||||
}
|
||||
|
||||
const minScore = params.minScore || 60
|
||||
const passed = score >= minScore
|
||||
|
||||
Reference in New Issue
Block a user