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
352 lines
11 KiB
JavaScript
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()
|