Files
trading_bot_v4/scripts/backtest-antichop-v2.mjs
mindesbunister 988fdb9ea4 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
2025-11-10 15:36:51 +01:00

352 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Backtest Anti-Chop Filter V2
*
* Compares OLD scoring (price position < 40% = OK)
* vs NEW scoring (price position < 40% + ADX < 25 = -25 points)
*/
const trades = [
// Format: [direction, atr, adx, rsi, volumeRatio, pricePosition, oldScore, pnl, holdTime, exitReason]
// Most recent first
['short', 0.45, 32, 42, 1.25, 45, 100, null, null, 'OPEN'], // Current position
['short', 0.37, 21.2, 44.1, 0.85, 16.8, 75, 0, 24, 'SOFT_SL'], // Flip-flop #3
['long', 0.37, 21.4, 47.5, 1.66, 30.6, 80, 0, 8, 'SOFT_SL'], // Flip-flop #2
['short', 0.36, 21.2, 41.1, 1.64, 23.8, 90, -3.4, 306, 'manual'], // Flip-flop #1
['long', 0.28, 19.7, 50.3, 0.83, 55.7, 75, 0, 1356, 'TP2'],
['short', 0.26, 16.9, 41.6, 1.37, 24.3, 85, -11.3, 1506, 'SL'],
['short', 0.33, 14.8, 37.1, 2.29, 12, 80, -31.2, 914, 'SL'],
['long', 0.52, 22.7, 53.1, 0.88, 37.9, 80, 15.6, 323, 'TP2'],
['short', 0.52, 24, 48.8, 1.04, 24.6, 80, -6.2, 301, 'manual'],
['long', 0.3, 26.1, 61.5, 1.45, 74.5, 95, 14.5, 9280, 'TP2'],
['short', 0.28, 19.8, 38.1, 1.26, 3.5, 65, 3.4, 1247, 'SL'],
['long', 0.26, 22.1, 57.3, 0.43, 81.4, 65, 14.6, 3560, 'TP2'],
['short', 0.29, 26.7, 57, 0.62, 78.8, 65, 7.6, 774, 'SL'],
['long', 0.32, 28, 62.4, 1.23, 88.9, 95, -3.1, 900, 'manual'],
['short', 0.27, 15.2, 44.5, 1.26, 43.9, 65, 16.3, 226, 'TP2'],
['long', 0.25, 16, 54.7, 1.65, 76.1, 70, -8.6, 597, 'manual'],
['short', 0.25, 18.2, 52.7, 1.25, 69.4, 75, 1.9, 32, 'TP1'],
['long', 0.25, 19.9, 58.2, 2.11, 87.7, 100, -0.9, 1204, 'manual'],
['short', 0.17, 12.9, 42.6, 1.7, 22.2, 70, 7.7, 585, 'SL'],
]
function scoreSignalQualityOLD(atr, adx, rsi, volumeRatio, pricePosition, direction) {
let score = 50
const reasons = []
// ATR
if (atr < 0.15) {
score -= 15
reasons.push(`ATR too low`)
} else if (atr > 2.5) {
score -= 20
reasons.push(`ATR too high`)
} else if (atr >= 0.15 && atr < 0.4) {
score += 5
reasons.push(`ATR moderate`)
} else {
score += 10
reasons.push(`ATR healthy`)
}
// ADX
if (adx > 25) {
score += 15
reasons.push(`Strong trend`)
} else if (adx < 18) {
score -= 15
reasons.push(`Weak trend`)
} else {
score += 5
reasons.push(`Moderate trend`)
}
// RSI
if (direction === 'long') {
if (rsi > 50 && rsi < 70) {
score += 10
reasons.push(`RSI supports long`)
} else if (rsi > 70) {
score -= 10
reasons.push(`RSI overbought`)
}
} else {
if (rsi < 50 && rsi > 30) {
score += 10
reasons.push(`RSI supports short`)
} else if (rsi < 30) {
score -= 10
reasons.push(`RSI oversold`)
}
}
// Volume
const isChoppy = adx < 16
const hasHighVolume = volumeRatio > 1.5
if (isChoppy && hasHighVolume) {
score -= 15
reasons.push(`Whipsaw trap`)
} else if (volumeRatio > 1.5) {
score += 15
reasons.push(`Very strong volume`)
} else if (volumeRatio > 1.2) {
score += 10
reasons.push(`Strong volume`)
} else if (volumeRatio < 0.8) {
score -= 10
reasons.push(`Weak volume`)
}
// Price position - OLD LOGIC
if (direction === 'long' && pricePosition > 95) {
if (volumeRatio > 1.4) {
score += 5
reasons.push(`Volume breakout at top`)
} else {
score -= 15
reasons.push(`Chasing highs`)
}
} else if (direction === 'short' && pricePosition < 5) {
if (volumeRatio > 1.4) {
score += 5
reasons.push(`Volume breakdown at bottom`)
} else {
score -= 15
reasons.push(`Chasing lows`)
}
} else {
score += 5
reasons.push(`Price position OK`)
}
return { score, reasons }
}
function scoreSignalQualityNEW(atr, adx, rsi, volumeRatio, pricePosition, direction) {
let score = 50
const reasons = []
// ATR (same as old)
if (atr < 0.15) {
score -= 15
reasons.push(`ATR too low`)
} else if (atr > 2.5) {
score -= 20
reasons.push(`ATR too high`)
} else if (atr >= 0.15 && atr < 0.4) {
score += 5
reasons.push(`ATR moderate`)
} else {
score += 10
reasons.push(`ATR healthy`)
}
// ADX (same as old)
if (adx > 25) {
score += 15
reasons.push(`Strong trend`)
} else if (adx < 18) {
score -= 15
reasons.push(`Weak trend`)
} else {
score += 5
reasons.push(`Moderate trend`)
}
// RSI (same as old)
if (direction === 'long') {
if (rsi > 50 && rsi < 70) {
score += 10
reasons.push(`RSI supports long`)
} else if (rsi > 70) {
score -= 10
reasons.push(`RSI overbought`)
}
} else {
if (rsi < 50 && rsi > 30) {
score += 10
reasons.push(`RSI supports short`)
} else if (rsi < 30) {
score -= 10
reasons.push(`RSI oversold`)
}
}
// Volume (same as old)
const isChoppy = adx < 16
const hasHighVolume = volumeRatio > 1.5
if (isChoppy && hasHighVolume) {
score -= 15
reasons.push(`Whipsaw trap`)
} else if (volumeRatio > 1.5) {
score += 15
reasons.push(`Very strong volume`)
} else if (volumeRatio > 1.2) {
score += 10
reasons.push(`Strong volume`)
} else if (volumeRatio < 0.8) {
score -= 10
reasons.push(`Weak volume`)
}
// Price position - NEW LOGIC WITH ANTI-CHOP
const isWeakTrend = adx < 25
const isLowInRange = pricePosition < 40
if (isLowInRange && isWeakTrend) {
score -= 25
reasons.push(`⚠️ RANGE-BOUND CHOP (pos ${pricePosition.toFixed(0)}%, ADX ${adx.toFixed(1)})`)
} else if (direction === 'long' && pricePosition > 95) {
if (volumeRatio > 1.4) {
score += 5
reasons.push(`Volume breakout at top`)
} else {
score -= 15
reasons.push(`Chasing highs`)
}
} else if (direction === 'short' && pricePosition < 5) {
if (volumeRatio > 1.4) {
score += 5
reasons.push(`Volume breakdown at bottom`)
} else {
score -= 15
reasons.push(`Chasing lows`)
}
} else {
score += 5
reasons.push(`Price position OK`)
}
return { score, reasons }
}
console.log('=' .repeat(100))
console.log('BACKTEST: Anti-Chop Filter V2 - Price Position < 40% + ADX < 25 = -25 points')
console.log('=' .repeat(100))
console.log()
let oldTotalPnL = 0
let oldWins = 0
let oldLosses = 0
let oldTradesExecuted = 0
let newTotalPnL = 0
let newWins = 0
let newLosses = 0
let newTradesExecuted = 0
let blockedBadTrades = []
let blockedGoodTrades = []
let stillExecutedBadTrades = []
const MIN_SCORE = 65
trades.forEach(([direction, atr, adx, rsi, volumeRatio, pricePosition, oldScore, pnl, holdTime, exitReason], idx) => {
const oldResult = scoreSignalQualityOLD(atr, adx, rsi, volumeRatio, pricePosition, direction)
const newResult = scoreSignalQualityNEW(atr, adx, rsi, volumeRatio, pricePosition, direction)
const oldPassed = oldResult.score >= MIN_SCORE
const newPassed = newResult.score >= MIN_SCORE
const isBadTrade = pnl !== null && (pnl <= 0 || holdTime < 60) // Loss or quick stop
const isGoodTrade = pnl !== null && pnl > 0 && holdTime > 300 // Profit + held > 5min
// OLD system stats
if (oldPassed && pnl !== null) {
oldTradesExecuted++
oldTotalPnL += pnl
if (pnl > 0) oldWins++
else oldLosses++
}
// NEW system stats
if (newPassed && pnl !== null) {
newTradesExecuted++
newTotalPnL += pnl
if (pnl > 0) newWins++
else newLosses++
}
// Track what changed
if (oldPassed && !newPassed) {
if (isBadTrade) {
blockedBadTrades.push({ direction, atr, adx, pricePosition, pnl, holdTime, exitReason, oldScore: oldResult.score, newScore: newResult.score })
} else if (isGoodTrade) {
blockedGoodTrades.push({ direction, atr, adx, pricePosition, pnl, holdTime, exitReason, oldScore: oldResult.score, newScore: newResult.score })
}
}
if (newPassed && isBadTrade) {
stillExecutedBadTrades.push({ direction, atr, adx, pricePosition, pnl, holdTime, exitReason, oldScore: oldResult.score, newScore: newResult.score })
}
// Print details for significant trades
if (oldPassed !== newPassed || Math.abs(oldResult.score - newResult.score) > 10) {
console.log(`Trade #${idx + 1}: ${direction.toUpperCase()} | ADX ${adx} | PricePos ${pricePosition.toFixed(0)}% | P&L $${pnl?.toFixed(1) || 'OPEN'}`)
console.log(` OLD: ${oldResult.score} ${oldPassed ? '✅ PASS' : '❌ BLOCK'} - ${oldResult.reasons.join(', ')}`)
console.log(` NEW: ${newResult.score} ${newPassed ? '✅ PASS' : '❌ BLOCK'} - ${newResult.reasons.join(', ')}`)
if (oldPassed && !newPassed && isBadTrade) {
console.log(` 🎯 BLOCKED BAD TRADE: ${exitReason} after ${holdTime}s`)
}
if (oldPassed && !newPassed && isGoodTrade) {
console.log(` ⚠️ BLOCKED GOOD TRADE: Would have made $${pnl.toFixed(2)}`)
}
console.log()
}
})
console.log('=' .repeat(100))
console.log('RESULTS SUMMARY')
console.log('=' .repeat(100))
console.log()
console.log('OLD SYSTEM (Price position < 40% = OK):')
console.log(` Trades executed: ${oldTradesExecuted}`)
console.log(` Wins: ${oldWins} | Losses: ${oldLosses} | Win rate: ${oldTradesExecuted > 0 ? ((oldWins / oldTradesExecuted) * 100).toFixed(1) : 0}%`)
console.log(` Total P&L: $${oldTotalPnL.toFixed(2)}`)
console.log(` Avg P&L per trade: $${oldTradesExecuted > 0 ? (oldTotalPnL / oldTradesExecuted).toFixed(2) : 0}`)
console.log()
console.log('NEW SYSTEM (Price position < 40% + ADX < 25 = -25 points):')
console.log(` Trades executed: ${newTradesExecuted}`)
console.log(` Wins: ${newWins} | Losses: ${newLosses} | Win rate: ${newTradesExecuted > 0 ? ((newWins / newTradesExecuted) * 100).toFixed(1) : 0}%`)
console.log(` Total P&L: $${newTotalPnL.toFixed(2)}`)
console.log(` Avg P&L per trade: $${newTradesExecuted > 0 ? (newTotalPnL / newTradesExecuted).toFixed(2) : 0}`)
console.log()
console.log('IMPACT:')
console.log(` 🎯 Bad trades BLOCKED: ${blockedBadTrades.length}`)
if (blockedBadTrades.length > 0) {
const savedLoss = blockedBadTrades.reduce((sum, t) => sum + Math.abs(t.pnl), 0)
console.log(` Saved loss: $${savedLoss.toFixed(2)}`)
blockedBadTrades.forEach(t => {
console.log(` - ${t.direction} ADX ${t.adx} Pos ${t.pricePosition.toFixed(0)}%: ${t.exitReason} in ${t.holdTime}s → $${t.pnl.toFixed(2)}`)
})
}
console.log()
console.log(` ⚠️ Good trades BLOCKED: ${blockedGoodTrades.length}`)
if (blockedGoodTrades.length > 0) {
const missedProfit = blockedGoodTrades.reduce((sum, t) => sum + t.pnl, 0)
console.log(` Missed profit: $${missedProfit.toFixed(2)}`)
blockedGoodTrades.forEach(t => {
console.log(` - ${t.direction} ADX ${t.adx} Pos ${t.pricePosition.toFixed(0)}%: Held ${t.holdTime}s → $${t.pnl.toFixed(2)}`)
})
}
console.log()
console.log(` ⚠️ Bad trades STILL EXECUTED: ${stillExecutedBadTrades.length}`)
if (stillExecutedBadTrades.length > 0) {
stillExecutedBadTrades.forEach(t => {
console.log(` - ${t.direction} ADX ${t.adx} Pos ${t.pricePosition.toFixed(0)}%: ${t.exitReason} in ${t.holdTime}s → $${t.pnl.toFixed(2)} (score ${t.newScore})`)
})
}
console.log()
const improvement = newTotalPnL - oldTotalPnL
console.log(`NET IMPROVEMENT: $${improvement.toFixed(2)} (${improvement > 0 ? '+' : ''}${oldTotalPnL !== 0 ? ((improvement / Math.abs(oldTotalPnL)) * 100).toFixed(1) : 0}%)`)
console.log()