#!/usr/bin/env node /** * Signal Quality Optimization Script * * Brute-force tests different threshold combinations to find optimal parameters * for signal quality scoring that maximize win rate and P&L */ import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() // Current thresholds from signal-quality.ts const CURRENT_THRESHOLDS = { atr: { veryLow: 0.25, low: 0.4, healthy: 0.7, high: 2.0 }, adx: { weak: 10, moderate: 18, strong: 30, veryStrong: 40 }, rsi: { oversold: 30, neutral: 50, overbought: 70 }, volume: { low: 0.8, normal: 1.0, high: 1.5 }, pricePosition: { extreme: 5, moderate: 15, safe: 30 } } // Test ranges for each parameter const TEST_RANGES = { atr: { veryLow: [0.15, 0.20, 0.25, 0.30], low: [0.3, 0.4, 0.5], healthy: [0.6, 0.7, 0.8], high: [1.5, 2.0, 2.5] }, adx: { weak: [8, 10, 12], moderate: [15, 18, 20], strong: [25, 30, 35], veryStrong: [38, 40, 45] }, rsi: { oversold: [25, 30, 35], neutral: [45, 50, 55], overbought: [65, 70, 75] }, volume: { low: [0.7, 0.8, 0.9], normal: [1.0, 1.1], high: [1.3, 1.5, 1.7] }, pricePosition: { extreme: [5, 10, 15], moderate: [15, 20, 25], safe: [25, 30, 35] } } // Score a single trade with given thresholds function scoreTradeWithThresholds(trade, thresholds, timeframe = null) { let score = 50 // Base score const reasons = [] const atr = trade.atrAtEntry const adx = trade.adxAtEntry const rsi = trade.rsiAtEntry const volumeRatio = trade.volumeAtEntry || 1.0 const pricePosition = trade.pricePositionAtEntry || 50 const direction = trade.direction // Determine if short timeframe (5min, 15min) const is5minChart = timeframe === '5' const is15minChart = timeframe === '15' const isShortTimeframe = is5minChart || is15minChart // ATR scoring if (atr) { if (isShortTimeframe) { if (atr < thresholds.atr.veryLow) { score -= 20 } else if (atr >= thresholds.atr.veryLow && atr < thresholds.atr.healthy) { score += 5 } else if (atr >= thresholds.atr.healthy && atr <= thresholds.atr.high) { score -= 10 } } else { if (atr < thresholds.atr.low) { score -= 20 } else if (atr >= thresholds.atr.low && atr < thresholds.atr.healthy) { score += 5 } else if (atr >= thresholds.atr.healthy && atr <= thresholds.atr.high) { score += 10 } else { score -= 15 } } } // ADX scoring if (adx) { if (isShortTimeframe) { if (adx < thresholds.adx.weak) { score -= 15 } else if (adx >= thresholds.adx.weak && adx < thresholds.adx.moderate) { score += 5 } else if (adx >= thresholds.adx.moderate && adx <= thresholds.adx.strong) { score += 15 } else { score -= 5 } } else { if (adx < thresholds.adx.moderate) { score -= 15 } else if (adx >= thresholds.adx.moderate && adx <= thresholds.adx.strong) { score += 15 } else if (adx > thresholds.adx.veryStrong) { score -= 10 } } } // RSI scoring if (rsi) { if (direction === 'long') { if (rsi < thresholds.rsi.oversold) { score += 10 } else if (rsi >= thresholds.rsi.oversold && rsi < thresholds.rsi.neutral) { score += 5 } else if (rsi > thresholds.rsi.overbought) { score -= 10 } } else { if (rsi > thresholds.rsi.overbought) { score += 10 } else if (rsi > thresholds.rsi.neutral && rsi <= thresholds.rsi.overbought) { score += 5 } else if (rsi < thresholds.rsi.oversold) { score -= 10 } } } // Volume scoring if (volumeRatio) { if (volumeRatio < thresholds.volume.low) { score -= 10 } else if (volumeRatio >= thresholds.volume.normal && volumeRatio < thresholds.volume.high) { score += 5 } else if (volumeRatio >= thresholds.volume.high) { score += 15 } } // Price position scoring if (pricePosition !== null) { if (direction === 'long') { if (pricePosition > 90) { score -= 30 } else if (pricePosition > 80) { score -= 15 } } else { if (pricePosition < 10) { score -= 30 } else if (pricePosition < 20) { score -= 15 } } } // Anti-chop filter if (adx && atr && volumeRatio) { if (adx < thresholds.adx.weak && atr < thresholds.atr.veryLow && volumeRatio < thresholds.volume.low) { score -= 20 } } return Math.max(0, Math.min(100, score)) } // Evaluate performance for a set of thresholds function evaluateThresholds(trades, thresholds, minScore = 65) { const results = { totalTrades: 0, acceptedTrades: 0, rejectedTrades: 0, wins: 0, losses: 0, totalPnL: 0, winRate: 0, avgWin: 0, avgLoss: 0, profitFactor: 0, avgScore: 0, acceptanceRate: 0 } const acceptedTrades = [] const rejectedTrades = [] for (const trade of trades) { const score = scoreTradeWithThresholds(trade, thresholds, trade.timeframe) results.totalTrades++ if (score >= minScore) { // Trade would be accepted results.acceptedTrades++ acceptedTrades.push({ ...trade, score }) if (trade.realizedPnL > 0) { results.wins++ results.totalPnL += trade.realizedPnL } else { results.losses++ results.totalPnL += trade.realizedPnL } } else { // Trade would be rejected results.rejectedTrades++ rejectedTrades.push({ ...trade, score }) } } // Calculate metrics if (results.acceptedTrades > 0) { results.winRate = (results.wins / results.acceptedTrades) * 100 results.acceptanceRate = (results.acceptedTrades / results.totalTrades) * 100 const winningTrades = acceptedTrades.filter(t => t.realizedPnL > 0) const losingTrades = acceptedTrades.filter(t => t.realizedPnL <= 0) if (winningTrades.length > 0) { results.avgWin = winningTrades.reduce((sum, t) => sum + t.realizedPnL, 0) / winningTrades.length } if (losingTrades.length > 0) { results.avgLoss = losingTrades.reduce((sum, t) => sum + t.realizedPnL, 0) / losingTrades.length } if (results.avgLoss !== 0) { results.profitFactor = Math.abs(results.avgWin / results.avgLoss) } results.avgScore = acceptedTrades.reduce((sum, t) => sum + t.score, 0) / acceptedTrades.length } // Calculate what we would have saved/lost by rejecting trades results.rejectedPnL = rejectedTrades.reduce((sum, t) => sum + (t.realizedPnL || 0), 0) return results } // Main optimization function async function optimizeSignalQuality() { console.log('šŸ”¬ Signal Quality Optimization Starting...\n') // Fetch all closed trades with metrics const trades = await prisma.trade.findMany({ where: { exitReason: { not: null }, realizedPnL: { not: null }, atrAtEntry: { not: null }, adxAtEntry: { not: null }, rsiAtEntry: { not: null } }, select: { id: true, direction: true, realizedPnL: true, atrAtEntry: true, adxAtEntry: true, rsiAtEntry: true, volumeAtEntry: true, pricePositionAtEntry: true, timeframe: true, signalQualityScore: true } }) console.log(`šŸ“Š Analyzing ${trades.length} trades with complete metrics\n`) if (trades.length < 20) { console.log('āš ļø Warning: Less than 20 trades available. Results may not be statistically significant.\n') } // Baseline: Current thresholds console.log('šŸ“ˆ BASELINE (Current Thresholds):') console.log('='.repeat(60)) const baseline = evaluateThresholds(trades, CURRENT_THRESHOLDS) console.log(`Total Trades: ${baseline.totalTrades}`) console.log(`Accepted: ${baseline.acceptedTrades} (${baseline.acceptanceRate.toFixed(1)}%)`) console.log(`Win Rate: ${baseline.winRate.toFixed(1)}%`) console.log(`Total P&L: $${baseline.totalPnL.toFixed(2)}`) console.log(`Avg Win: $${baseline.avgWin.toFixed(2)} | Avg Loss: $${baseline.avgLoss.toFixed(2)}`) console.log(`Profit Factor: ${baseline.profitFactor.toFixed(2)}`) console.log(`Avg Score: ${baseline.avgScore.toFixed(1)}`) console.log(`Rejected P&L: $${baseline.rejectedPnL.toFixed(2)} (would have saved/lost)\n`) // Test different minimum score thresholds console.log('šŸŽÆ Testing Different Minimum Score Thresholds:') console.log('='.repeat(60)) const scoreThresholds = [50, 55, 60, 65, 70, 75, 80] let bestScoreThreshold = { minScore: 65, result: baseline } for (const minScore of scoreThresholds) { const result = evaluateThresholds(trades, CURRENT_THRESHOLDS, minScore) console.log(`\nMin Score: ${minScore}`) console.log(` Accepted: ${result.acceptedTrades}/${result.totalTrades} (${result.acceptanceRate.toFixed(1)}%)`) console.log(` Win Rate: ${result.winRate.toFixed(1)}%`) console.log(` Total P&L: $${result.totalPnL.toFixed(2)}`) console.log(` Profit Factor: ${result.profitFactor.toFixed(2)}`) // Best = highest P&L with decent acceptance rate (>30%) if (result.acceptanceRate > 30 && result.totalPnL > bestScoreThreshold.result.totalPnL) { bestScoreThreshold = { minScore, result } } } console.log('\n\nšŸ† BEST MINIMUM SCORE THRESHOLD:') console.log('='.repeat(60)) console.log(`Min Score: ${bestScoreThreshold.minScore}`) console.log(`Win Rate: ${bestScoreThreshold.result.winRate.toFixed(1)}%`) console.log(`Total P&L: $${bestScoreThreshold.result.totalPnL.toFixed(2)}`) console.log(`Acceptance Rate: ${bestScoreThreshold.result.acceptanceRate.toFixed(1)}%`) console.log(`Profit Factor: ${bestScoreThreshold.result.profitFactor.toFixed(2)}\n`) // Now test key threshold variations console.log('\nšŸ”§ Testing Key Threshold Variations:') console.log('='.repeat(60)) const variations = [] // Test ADX thresholds (most impactful) for (const moderate of TEST_RANGES.adx.moderate) { const testThresholds = { ...CURRENT_THRESHOLDS, adx: { ...CURRENT_THRESHOLDS.adx, moderate } } const result = evaluateThresholds(trades, testThresholds, bestScoreThreshold.minScore) variations.push({ name: `ADX Moderate: ${moderate}`, thresholds: testThresholds, result }) } // Test ATR thresholds for (const low of TEST_RANGES.atr.low) { const testThresholds = { ...CURRENT_THRESHOLDS, atr: { ...CURRENT_THRESHOLDS.atr, low } } const result = evaluateThresholds(trades, testThresholds, bestScoreThreshold.minScore) variations.push({ name: `ATR Low: ${low}`, thresholds: testThresholds, result }) } // Test price position thresholds for (const extreme of TEST_RANGES.pricePosition.extreme) { const testThresholds = { ...CURRENT_THRESHOLDS, pricePosition: { ...CURRENT_THRESHOLDS.pricePosition, extreme } } const result = evaluateThresholds(trades, testThresholds, bestScoreThreshold.minScore) variations.push({ name: `Price Extreme: ${extreme}`, thresholds: testThresholds, result }) } // Sort by P&L variations.sort((a, b) => b.result.totalPnL - a.result.totalPnL) console.log('\nTop 10 Variations by P&L:') console.log('-'.repeat(60)) variations.slice(0, 10).forEach((v, i) => { console.log(`${i + 1}. ${v.name}`) console.log(` Win Rate: ${v.result.winRate.toFixed(1)}% | P&L: $${v.result.totalPnL.toFixed(2)} | Accepted: ${v.result.acceptedTrades}/${v.result.totalTrades}`) }) console.log('\n\nšŸ“‹ FINAL RECOMMENDATIONS:') console.log('='.repeat(60)) const best = variations[0] console.log(`\nBest Configuration Found:`) console.log(`- ${best.name}`) console.log(`- Min Score Threshold: ${bestScoreThreshold.minScore}`) console.log(`\nPerformance Improvement:`) console.log(`- Current P&L: $${baseline.totalPnL.toFixed(2)}`) console.log(`- Optimized P&L: $${best.result.totalPnL.toFixed(2)}`) console.log(`- Improvement: $${(best.result.totalPnL - baseline.totalPnL).toFixed(2)} (${(((best.result.totalPnL - baseline.totalPnL) / Math.abs(baseline.totalPnL)) * 100).toFixed(1)}%)`) console.log(`- Current Win Rate: ${baseline.winRate.toFixed(1)}%`) console.log(`- Optimized Win Rate: ${best.result.winRate.toFixed(1)}%`) console.log(`- Acceptance Rate: ${best.result.acceptanceRate.toFixed(1)}% (${best.result.acceptedTrades}/${best.result.totalTrades} trades)`) await prisma.$disconnect() } // Run optimization optimizeSignalQuality().catch(console.error)