feat: implement blocked signals tracking system
- Add BlockedSignal table with 25 fields for comprehensive signal analysis - Track all blocked signals with metrics (ATR, ADX, RSI, volume, price position) - Store quality scores, block reasons, and detailed breakdowns - Include future fields for automated price analysis (priceAfter1/5/15/30Min) - Restore signalQualityVersion field to Trade table Database changes: - New table: BlockedSignal with indexes on symbol, createdAt, score, blockReason - Fixed schema drift from manual changes API changes: - Modified check-risk endpoint to save blocked signals automatically - Fixed hasContextMetrics variable scope (moved to line 209) - Save blocks for: quality score too low, cooldown period, hourly limit - Use config.minSignalQualityScore instead of hardcoded 60 Database helpers: - Added createBlockedSignal() function with try/catch safety - Added getRecentBlockedSignals(limit) for queries - Added getBlockedSignalsForAnalysis(olderThanMinutes) for automation Documentation: - Created BLOCKED_SIGNALS_TRACKING.md with SQL queries and analysis workflow - Created SIGNAL_QUALITY_OPTIMIZATION_ROADMAP.md with 5-phase plan - Documented data-first approach: collect 10-20 signals before optimization Rationale: Only 2 historical trades scored 60-64 (insufficient sample size for threshold decision). Building data collection infrastructure before making premature optimizations. Phase 1 (current): Collect blocked signals for 1-2 weeks Phase 2 (next): Analyze patterns and make data-driven threshold decision Phase 3-5 (future): Automation and ML optimization
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getMergedConfig, TradingConfig } from '@/config/trading'
|
||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||
import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades'
|
||||
import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL, createBlockedSignal } from '@/lib/database/trades'
|
||||
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
|
||||
import { scoreSignalQuality, SignalQualityResult } from '@/lib/trading/signal-quality'
|
||||
|
||||
@@ -205,6 +205,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
// Continue to quality checks below instead of returning early
|
||||
}
|
||||
|
||||
// Check if we have context metrics (used throughout the function)
|
||||
const hasContextMetrics = body.atr !== undefined && body.atr > 0
|
||||
|
||||
// 1. Check daily drawdown limit
|
||||
const todayPnL = await getTodayPnL()
|
||||
if (todayPnL < config.maxDailyDrawdown) {
|
||||
@@ -228,6 +231,28 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
maxTradesPerHour: config.maxTradesPerHour,
|
||||
})
|
||||
|
||||
// Save blocked signal if we have metrics
|
||||
if (hasContextMetrics) {
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
const latestPrice = priceMonitor.getCachedPrice(body.symbol)
|
||||
|
||||
await createBlockedSignal({
|
||||
symbol: body.symbol,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe,
|
||||
signalPrice: latestPrice?.price || 0,
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio,
|
||||
pricePosition: body.pricePosition,
|
||||
signalQualityScore: 0, // Not calculated yet
|
||||
minScoreRequired: config.minSignalQualityScore,
|
||||
blockReason: 'HOURLY_TRADE_LIMIT',
|
||||
blockDetails: `${tradesInLastHour} trades in last hour (max: ${config.maxTradesPerHour})`,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Hourly trade limit',
|
||||
@@ -252,6 +277,28 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
remainingMinutes,
|
||||
})
|
||||
|
||||
// Save blocked signal if we have metrics
|
||||
if (hasContextMetrics) {
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
const latestPrice = priceMonitor.getCachedPrice(body.symbol)
|
||||
|
||||
await createBlockedSignal({
|
||||
symbol: body.symbol,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe,
|
||||
signalPrice: latestPrice?.price || 0,
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio,
|
||||
pricePosition: body.pricePosition,
|
||||
signalQualityScore: 0, // Not calculated yet
|
||||
minScoreRequired: config.minSignalQualityScore,
|
||||
blockReason: 'COOLDOWN_PERIOD',
|
||||
blockDetails: `Wait ${remainingMinutes} more min (cooldown: ${config.minTimeBetweenTrades} min)`,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Cooldown period',
|
||||
@@ -261,8 +308,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
}
|
||||
|
||||
// 4. Check signal quality (if context metrics provided)
|
||||
const hasContextMetrics = body.atr !== undefined && body.atr > 0
|
||||
|
||||
if (hasContextMetrics) {
|
||||
const qualityScore = scoreSignalQuality({
|
||||
atr: body.atr || 0,
|
||||
@@ -272,15 +317,39 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe, // Pass timeframe for context-aware scoring
|
||||
minScore: 60 // Hardcoded threshold
|
||||
minScore: config.minSignalQualityScore // Use config value
|
||||
})
|
||||
|
||||
if (!qualityScore.passed) {
|
||||
console.log('🚫 Risk check BLOCKED: Signal quality too low', {
|
||||
score: qualityScore.score,
|
||||
threshold: config.minSignalQualityScore,
|
||||
reasons: qualityScore.reasons
|
||||
})
|
||||
|
||||
// Get current price for the blocked signal record
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
const latestPrice = priceMonitor.getCachedPrice(body.symbol)
|
||||
|
||||
// Save blocked signal to database for future analysis
|
||||
await createBlockedSignal({
|
||||
symbol: body.symbol,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe,
|
||||
signalPrice: latestPrice?.price || 0,
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio,
|
||||
pricePosition: body.pricePosition,
|
||||
signalQualityScore: qualityScore.score,
|
||||
signalQualityVersion: 'v4', // Update this when scoring logic changes
|
||||
scoreBreakdown: { reasons: qualityScore.reasons },
|
||||
minScoreRequired: config.minSignalQualityScore,
|
||||
blockReason: 'QUALITY_SCORE_TOO_LOW',
|
||||
blockDetails: `Score: ${qualityScore.score}/${config.minSignalQualityScore} - ${qualityScore.reasons.join(', ')}`,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Signal quality too low',
|
||||
|
||||
Reference in New Issue
Block a user