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:
mindesbunister
2025-11-11 11:49:21 +01:00
parent ee89d15b8b
commit ba13c20c60
5 changed files with 785 additions and 4 deletions

View File

@@ -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',