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:
351
scripts/backtest-antichop-v2.mjs
Normal file
351
scripts/backtest-antichop-v2.mjs
Normal file
@@ -0,0 +1,351 @@
|
||||
#!/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()
|
||||
73
scripts/fix_pnl_calculations.sql
Normal file
73
scripts/fix_pnl_calculations.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- Fix P&L calculations for all trades
|
||||
-- Problem: P&L was calculated on notional position size instead of collateral
|
||||
-- Formula was: realizedPnL = positionSizeUSD * profitPercent / 100
|
||||
-- Should be: realizedPnL = (positionSizeUSD / leverage) * (profitPercent * leverage) / 100
|
||||
-- Which simplifies to: realizedPnL = positionSizeUSD * profitPercent / 100 (but correctly calculated)
|
||||
|
||||
-- Step 1: Calculate and populate collateralUSD for all trades
|
||||
UPDATE "Trade"
|
||||
SET "collateralUSD" = "positionSizeUSD" / "leverage"
|
||||
WHERE "collateralUSD" IS NULL;
|
||||
|
||||
-- Step 2: Recalculate realizedPnL for all closed trades
|
||||
-- The bug was that closedUSD (notional) was used directly without dividing by leverage
|
||||
-- Correct calculation: collateral * (price_change% * leverage) / 100
|
||||
|
||||
UPDATE "Trade"
|
||||
SET "realizedPnL" = (
|
||||
-- Collateral used
|
||||
("positionSizeUSD" / "leverage") *
|
||||
-- Price change percentage
|
||||
(CASE
|
||||
WHEN direction = 'long' THEN
|
||||
(("exitPrice" - "entryPrice") / "entryPrice") * 100
|
||||
WHEN direction = 'short' THEN
|
||||
(("entryPrice" - "exitPrice") / "entryPrice") * 100
|
||||
END) *
|
||||
-- Leverage multiplier
|
||||
"leverage"
|
||||
) / 100
|
||||
WHERE "exitReason" IS NOT NULL
|
||||
AND "exitPrice" IS NOT NULL
|
||||
AND "realizedPnL" IS NOT NULL;
|
||||
|
||||
-- Step 3: Also update realizedPnLPercent to reflect account P&L
|
||||
UPDATE "Trade"
|
||||
SET "realizedPnLPercent" = (
|
||||
(CASE
|
||||
WHEN direction = 'long' THEN
|
||||
(("exitPrice" - "entryPrice") / "entryPrice") * 100
|
||||
WHEN direction = 'short' THEN
|
||||
(("entryPrice" - "exitPrice") / "entryPrice") * 100
|
||||
END) * "leverage"
|
||||
)
|
||||
WHERE "exitReason" IS NOT NULL
|
||||
AND "exitPrice" IS NOT NULL;
|
||||
|
||||
-- Step 4: Verify the fix by comparing old vs new P&L for one sample trade
|
||||
SELECT
|
||||
id,
|
||||
direction,
|
||||
"entryPrice",
|
||||
"exitPrice",
|
||||
"positionSizeUSD",
|
||||
"collateralUSD",
|
||||
leverage,
|
||||
"realizedPnL" as corrected_pnl,
|
||||
"realizedPnLPercent" as account_pnl_percent,
|
||||
-- Show what it was before (incorrectly calculated)
|
||||
"positionSizeUSD" * (("exitPrice" - "entryPrice") / "entryPrice") as old_wrong_calculation
|
||||
FROM "Trade"
|
||||
WHERE id = 'cmhr8papg0009p907jczfgdxn';
|
||||
|
||||
-- Step 5: Show summary of corrected P&L
|
||||
SELECT
|
||||
COUNT(*) as total_trades,
|
||||
COUNT(CASE WHEN "exitReason" IS NOT NULL THEN 1 END) as closed_trades,
|
||||
ROUND(SUM("realizedPnL")::numeric, 2) as total_corrected_pnl,
|
||||
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl_per_trade,
|
||||
COUNT(CASE WHEN "realizedPnL" > 0 THEN 1 END) as winning_trades,
|
||||
COUNT(CASE WHEN "realizedPnL" < 0 THEN 1 END) as losing_trades,
|
||||
ROUND((COUNT(CASE WHEN "realizedPnL" > 0 THEN 1 END)::float /
|
||||
NULLIF(COUNT(CASE WHEN "exitReason" IS NOT NULL THEN 1 END), 0) * 100)::numeric, 1) as win_rate_percent
|
||||
FROM "Trade";
|
||||
400
scripts/optimize-signal-quality.mjs
Normal file
400
scripts/optimize-signal-quality.mjs
Normal 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)
|
||||
90
scripts/query-drift-pnl.mjs
Normal file
90
scripts/query-drift-pnl.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Query Drift Protocol trade history and compare with database
|
||||
*/
|
||||
|
||||
import { Connection, PublicKey } from '@solana/web3.js'
|
||||
import { DriftClient, initialize } from '@drift-labs/sdk'
|
||||
import bs58 from 'bs58'
|
||||
|
||||
const DRIFT_WALLET_KEY = process.env.DRIFT_WALLET_PRIVATE_KEY
|
||||
const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
|
||||
|
||||
async function queryDriftHistory() {
|
||||
console.log('🔍 Querying Drift Protocol trade history...\n')
|
||||
|
||||
// Setup connection
|
||||
const connection = new Connection(SOLANA_RPC_URL, 'confirmed')
|
||||
|
||||
// Parse wallet
|
||||
let secretKey
|
||||
if (DRIFT_WALLET_KEY.startsWith('[')) {
|
||||
secretKey = new Uint8Array(JSON.parse(DRIFT_WALLET_KEY))
|
||||
} else {
|
||||
secretKey = bs58.decode(DRIFT_WALLET_KEY)
|
||||
}
|
||||
|
||||
const walletKeypair = { publicKey: PublicKey.default, secretKey }
|
||||
|
||||
// Initialize Drift
|
||||
const sdkConfig = initialize({ env: 'mainnet-beta' })
|
||||
const driftClient = new DriftClient({
|
||||
connection,
|
||||
wallet: { publicKey: walletKeypair.publicKey },
|
||||
programID: new PublicKey(sdkConfig.DRIFT_PROGRAM_ID),
|
||||
opts: { commitment: 'confirmed' }
|
||||
})
|
||||
|
||||
await driftClient.subscribe()
|
||||
|
||||
// Get account
|
||||
const user = driftClient.getUser()
|
||||
const userAccount = user.getUserAccount()
|
||||
|
||||
console.log('📊 Drift Account Summary:')
|
||||
console.log('=' .repeat(60))
|
||||
|
||||
// Get total collateral
|
||||
const totalCollateral = Number(user.getTotalCollateral()) / 1e6
|
||||
const totalLiability = Number(user.getTotalLiabilityValue()) / 1e6
|
||||
const freeCollateral = Number(user.getFreeCollateral()) / 1e6
|
||||
const unrealizedPnL = Number(user.getUnrealizedPNL()) / 1e6
|
||||
|
||||
console.log(`Total Collateral: $${totalCollateral.toFixed(2)}`)
|
||||
console.log(`Total Liability: $${totalLiability.toFixed(2)}`)
|
||||
console.log(`Free Collateral: $${freeCollateral.toFixed(2)}`)
|
||||
console.log(`Unrealized P&L: $${unrealizedPnL.toFixed(2)}`)
|
||||
|
||||
// Get settled P&L
|
||||
const settledPnL = Number(userAccount.settledPerpPnl) / 1e6
|
||||
console.log(`\n💰 Settled Perp P&L: $${settledPnL.toFixed(2)}`)
|
||||
|
||||
// Get cumulative P&L
|
||||
const cumulativePnL = Number(userAccount.cumulativePerpFunding) / 1e6
|
||||
console.log(`Cumulative Funding: $${cumulativePnL.toFixed(2)}`)
|
||||
|
||||
// Calculate deposits/withdrawals impact
|
||||
const netDeposits = Number(userAccount.totalDeposits) / 1e6
|
||||
const netWithdrawals = Number(userAccount.totalWithdraws) / 1e6
|
||||
|
||||
console.log(`\nTotal Deposits: $${netDeposits.toFixed(2)}`)
|
||||
console.log(`Total Withdrawals: $${netWithdrawals.toFixed(2)}`)
|
||||
console.log(`Net Deposits: $${(netDeposits - netWithdrawals).toFixed(2)}`)
|
||||
|
||||
// Calculate actual trading P&L
|
||||
const actualTradingPnL = totalCollateral - (netDeposits - netWithdrawals)
|
||||
console.log(`\n🎯 Actual Trading P&L: $${actualTradingPnL.toFixed(2)}`)
|
||||
console.log(` (Total Collateral - Net Deposits)`)
|
||||
|
||||
await driftClient.unsubscribe()
|
||||
|
||||
return {
|
||||
settledPnL,
|
||||
unrealizedPnL,
|
||||
totalCollateral,
|
||||
netDeposits: netDeposits - netWithdrawals,
|
||||
actualTradingPnL
|
||||
}
|
||||
}
|
||||
|
||||
queryDriftHistory().catch(console.error)
|
||||
Reference in New Issue
Block a user