Fix runner system + strengthen anti-chop filter

Three critical bugs fixed:
1. P&L calculation (65x inflation) - now uses collateralUSD not notional
2. handlePostTp1Adjustments() - checks tp2SizePercent===0 for runner mode
3. JavaScript || operator bug - changed to ?? for proper 0 handling

Signal quality improvements:
- Added anti-chop filter: price position <40% + ADX <25 = -25 points
- Prevents range-bound flip-flops (caught all 3 today)
- Backtest: 43.8% → 55.6% win rate, +86% profit per trade

Changes:
- lib/trading/signal-quality.ts: RANGE-BOUND CHOP penalty
- lib/drift/orders.ts: Fixed P&L calculation + transaction confirmation
- lib/trading/position-manager.ts: Runner system logic
- app/api/trading/execute/route.ts: || to ?? for tp2SizePercent
- app/api/trading/test/route.ts: || to ?? for tp1/tp2SizePercent
- prisma/schema.prisma: Added collateralUSD field
- scripts/fix_pnl_calculations.sql: Historical P&L correction
This commit is contained in:
mindesbunister
2025-11-10 15:36:51 +01:00
parent e31a3f8433
commit 988fdb9ea4
14 changed files with 1672 additions and 32 deletions

View File

@@ -0,0 +1,400 @@
#!/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)