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:
@@ -819,8 +819,11 @@ export class PositionManager {
|
||||
const treatAsFullClose = percentToClose >= 100
|
||||
|
||||
// Calculate actual P&L based on entry vs exit price
|
||||
// CRITICAL: closedUSD is NOTIONAL value (with leverage), must calculate based on collateral
|
||||
const profitPercent = this.calculateProfitPercent(trade.entryPrice, closePriceForCalc, trade.direction)
|
||||
const actualRealizedPnL = (closedUSD * profitPercent) / 100
|
||||
const collateralUSD = closedUSD / trade.leverage // Convert notional to actual collateral used
|
||||
const accountPnLPercent = profitPercent * trade.leverage // Account P&L includes leverage effect
|
||||
const actualRealizedPnL = (collateralUSD * accountPnLPercent) / 100
|
||||
|
||||
// Update trade state
|
||||
if (treatAsFullClose) {
|
||||
@@ -862,7 +865,10 @@ export class PositionManager {
|
||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
} else {
|
||||
// Partial close (TP1) - calculate P&L for partial amount
|
||||
const partialRealizedPnL = (closedUSD * profitPercent) / 100
|
||||
// CRITICAL: Same fix as above - closedUSD is notional, must use collateral
|
||||
const partialCollateralUSD = closedUSD / trade.leverage
|
||||
const partialAccountPnL = profitPercent * trade.leverage
|
||||
const partialRealizedPnL = (partialCollateralUSD * partialAccountPnL) / 100
|
||||
trade.realizedPnL += partialRealizedPnL
|
||||
trade.currentSize = Math.max(0, trade.currentSize - closedUSD)
|
||||
|
||||
@@ -1004,14 +1010,33 @@ export class PositionManager {
|
||||
|
||||
console.log(`🔒 (${context}) SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`)
|
||||
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: trade.tp2Price,
|
||||
tp1SizePercent: 100,
|
||||
tp2Price: trade.tp2Price,
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
// CRITICAL FIX: For runner system (tp2SizePercent=0), don't place any TP orders
|
||||
// The remaining 25% should only have stop loss and be managed by software trailing stop
|
||||
const shouldPlaceTpOrders = this.config.takeProfit2SizePercent > 0
|
||||
|
||||
if (shouldPlaceTpOrders) {
|
||||
// Traditional system: place TP2 order for remaining position
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: trade.tp2Price,
|
||||
tp1SizePercent: 100,
|
||||
tp2Price: trade.tp2Price,
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
} else {
|
||||
// Runner system: Only place stop loss, no TP orders
|
||||
// The 25% runner will be managed by software trailing stop
|
||||
console.log(`🏃 Runner system active - placing ONLY stop loss at breakeven, no TP orders`)
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: 0, // No TP1 order
|
||||
tp1SizePercent: 0,
|
||||
tp2Price: 0, // No TP2 order
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
@@ -140,8 +140,16 @@ export function scoreSignalQuality(params: {
|
||||
}
|
||||
|
||||
// Price position check (avoid chasing vs breakout detection)
|
||||
// CRITICAL: Low price position (< 40%) + weak trend (ADX < 25) = range-bound chop
|
||||
if (params.pricePosition > 0) {
|
||||
if (params.direction === 'long' && params.pricePosition > 95) {
|
||||
const isWeakTrend = params.adx > 0 && params.adx < 25
|
||||
const isLowInRange = params.pricePosition < 40
|
||||
|
||||
// ANTI-CHOP: Heavily penalize range-bound entries
|
||||
if (isLowInRange && isWeakTrend) {
|
||||
score -= 25
|
||||
reasons.push(`⚠️ RANGE-BOUND CHOP: Low position (${params.pricePosition.toFixed(0)}%) + weak trend (ADX ${params.adx.toFixed(1)}) = high whipsaw risk`)
|
||||
} else if (params.direction === 'long' && params.pricePosition > 95) {
|
||||
// High volume breakout at range top can be good
|
||||
if (params.volumeRatio > 1.4) {
|
||||
score += 5
|
||||
|
||||
Reference in New Issue
Block a user