From 6d5991172a26989479909882412854a619abaed9 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 7 Nov 2025 17:01:22 +0100 Subject: [PATCH] feat: Implement ATR-based dynamic TP2 system and fix P&L calculation - Add ATR-based dynamic TP2 scaling from 0.7% to 3.0% based on volatility - New config options: useAtrBasedTargets, atrMultiplierForTp2, minTp2Percent, maxTp2Percent - Enhanced settings UI with ATR controls and updated risk calculator - Fix external closure P&L calculation using unrealized P&L instead of volatile current price - Update execute and test endpoints to use calculateDynamicTp2() function - Maintain 25% runner system for capturing extended moves (4-5% targets) - Add environment variables for ATR-based configuration - Better P&L accuracy for manual position closures --- .env | 17 +++++++- app/api/settings/route.ts | 12 ++++++ app/api/trading/execute/route.ts | 14 +++++-- app/api/trading/test/route.ts | 15 +++++-- app/settings/page.tsx | 70 +++++++++++++++++++++++++++++++- config/trading.ts | 57 ++++++++++++++++++++++++++ lib/trading/position-manager.ts | 42 +++++++++++++++---- 7 files changed, 208 insertions(+), 19 deletions(-) diff --git a/.env b/.env index 724a253..8391b67 100644 --- a/.env +++ b/.env @@ -105,7 +105,22 @@ TAKE_PROFIT_2_PERCENT=0.7 # Take Profit 2 Size: What % of remaining position to close at TP2 # Example: 100 = close all remaining position -TAKE_PROFIT_2_SIZE_PERCENT=75 +TAKE_PROFIT_2_SIZE_PERCENT=0 + +# ATR-based dynamic targets (capture big moves like 4-5% drops) +# Enable dynamic TP2 based on market volatility +USE_ATR_BASED_TARGETS=true + +# ATR multiplier for TP2 calculation (TP2 = ATR × this value) +# Example: ATR=0.8% × 2.0 = 1.6% TP2 target +ATR_MULTIPLIER_FOR_TP2=2.0 + +# Minimum TP2 level regardless of ATR (safety floor) +MIN_TP2_PERCENT=0.7 + +# Maximum TP2 level cap (prevents excessive targets) +# Example: 3.0% = 30% account gain at 10x leverage +MAX_TP2_PERCENT=3.0 # Emergency Stop: Hard stop if this level is breached # Example: -2.0% on 10x = -20% account loss (rare but protects from flash crashes) diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 28f8260..2ba335b 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -97,6 +97,12 @@ export async function GET() { TRAILING_STOP_MAX_PERCENT: parseFloat(env.TRAILING_STOP_MAX_PERCENT || '0.9'), TRAILING_STOP_ACTIVATION: parseFloat(env.TRAILING_STOP_ACTIVATION || '0.5'), + // ATR-based Dynamic Targets + USE_ATR_BASED_TARGETS: env.USE_ATR_BASED_TARGETS === 'true' || env.USE_ATR_BASED_TARGETS === undefined, + ATR_MULTIPLIER_FOR_TP2: parseFloat(env.ATR_MULTIPLIER_FOR_TP2 || '2.0'), + MIN_TP2_PERCENT: parseFloat(env.MIN_TP2_PERCENT || '0.7'), + MAX_TP2_PERCENT: parseFloat(env.MAX_TP2_PERCENT || '3.0'), + // Position Scaling ENABLE_POSITION_SCALING: env.ENABLE_POSITION_SCALING === 'true', MIN_SCALE_QUALITY_SCORE: parseInt(env.MIN_SCALE_QUALITY_SCORE || '75'), @@ -158,6 +164,12 @@ export async function POST(request: NextRequest) { TRAILING_STOP_MAX_PERCENT: (settings.TRAILING_STOP_MAX_PERCENT ?? DEFAULT_TRADING_CONFIG.trailingStopMaxPercent).toString(), TRAILING_STOP_ACTIVATION: settings.TRAILING_STOP_ACTIVATION.toString(), + // ATR-based Dynamic Targets + USE_ATR_BASED_TARGETS: (settings as any).USE_ATR_BASED_TARGETS?.toString() || 'true', + ATR_MULTIPLIER_FOR_TP2: (settings as any).ATR_MULTIPLIER_FOR_TP2?.toString() || '2.0', + MIN_TP2_PERCENT: (settings as any).MIN_TP2_PERCENT?.toString() || '0.7', + MAX_TP2_PERCENT: (settings as any).MAX_TP2_PERCENT?.toString() || '3.0', + // Position Scaling ENABLE_POSITION_SCALING: settings.ENABLE_POSITION_SCALING.toString(), MIN_SCALE_QUALITY_SCORE: settings.MIN_SCALE_QUALITY_SCORE.toString(), diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index d571255..b310c30 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -8,7 +8,7 @@ import { NextRequest, NextResponse } from 'next/server' import { initializeDriftService } from '@/lib/drift/client' import { openPosition, placeExitOrders } from '@/lib/drift/orders' -import { normalizeTradingViewSymbol } from '@/config/trading' +import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade, updateTradeExit } from '@/lib/database/trades' @@ -420,9 +420,15 @@ export async function POST(request: NextRequest): Promise
Runner Value (25%)
+${risk.runnerValue.toFixed(2)}
+
{risk.tp2Percent}
Full Win
@@ -455,6 +473,54 @@ export default function SettingsPage() { /> + {/* ATR-based Dynamic Targets */} +
+
+

+ 🎯 Capture Big Moves: When ATR is high (volatile markets), TP2 automatically scales higher to catch 4-5% moves instead of exiting early at 0.7%. +

+

+ Example: If ATR = 1.2% and multiplier = 2.0, then TP2 = 2.4% (instead of fixed 0.7%). Perfect for trending markets! +

+
+ updateSetting('USE_ATR_BASED_TARGETS', v === 1)} + min={0} + max={1} + step={1} + description="Enable dynamic TP2 based on Average True Range (market volatility). 0 = fixed TP2, 1 = adaptive TP2." + /> + updateSetting('ATR_MULTIPLIER_FOR_TP2', v)} + min={1.0} + max={4.0} + step={0.1} + description="Multiply ATR by this value to get TP2 target. Higher = more aggressive targets in volatile markets." + /> + updateSetting('MIN_TP2_PERCENT', v)} + min={0.3} + max={2.0} + step={0.1} + description="Safety floor - TP2 will never go below this level even in low-volatility markets." + /> + updateSetting('MAX_TP2_PERCENT', v)} + min={1.0} + max={5.0} + step={0.1} + description="Safety cap - TP2 will never exceed this level. Example: 3.0% = 30% account gain at 10x leverage." + /> +
+ {/* Dynamic Adjustments */}
{ takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT ? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT) : undefined, + + // ATR-based dynamic targets + useAtrBasedTargets: process.env.USE_ATR_BASED_TARGETS + ? process.env.USE_ATR_BASED_TARGETS === 'true' + : undefined, + atrMultiplierForTp2: process.env.ATR_MULTIPLIER_FOR_TP2 + ? parseFloat(process.env.ATR_MULTIPLIER_FOR_TP2) + : undefined, + minTp2Percent: process.env.MIN_TP2_PERCENT + ? parseFloat(process.env.MIN_TP2_PERCENT) + : undefined, + maxTp2Percent: process.env.MAX_TP2_PERCENT + ? parseFloat(process.env.MAX_TP2_PERCENT) + : undefined, + breakEvenTriggerPercent: process.env.BREAKEVEN_TRIGGER_PERCENT ? parseFloat(process.env.BREAKEVEN_TRIGGER_PERCENT) : undefined, diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 804e0fa..9c14624 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -468,14 +468,38 @@ export class PositionManager { // Calculate P&L first (set to 0 for phantom trades) let realizedPnL = 0 + let exitPrice = currentPrice + if (!wasPhantom) { - const profitPercent = this.calculateProfitPercent( - trade.entryPrice, - currentPrice, - trade.direction - ) - const accountPnL = profitPercent * trade.leverage - realizedPnL = (sizeForPnL * accountPnL) / 100 + // For external closures, try to estimate a more realistic exit price + // Manual closures may happen at significantly different prices than current market + const unrealizedPnL = trade.unrealizedPnL || 0 + const positionSizeUSD = trade.positionSize + + if (Math.abs(unrealizedPnL) > 1 && positionSizeUSD > 0) { + // If we have meaningful unrealized P&L, back-calculate the likely exit price + // This is more accurate than using volatile current market price + const impliedProfitPercent = (unrealizedPnL / positionSizeUSD) * 100 / trade.leverage + exitPrice = trade.direction === 'long' + ? trade.entryPrice * (1 + impliedProfitPercent / 100) + : trade.entryPrice * (1 - impliedProfitPercent / 100) + + console.log(`📊 Estimated exit price based on unrealized P&L:`) + console.log(` Unrealized P&L: $${unrealizedPnL.toFixed(2)}`) + console.log(` Market price: $${currentPrice.toFixed(6)}`) + console.log(` Estimated exit: $${exitPrice.toFixed(6)}`) + + realizedPnL = unrealizedPnL + } else { + // Fallback to current price calculation + const profitPercent = this.calculateProfitPercent( + trade.entryPrice, + currentPrice, + trade.direction + ) + const accountPnL = profitPercent * trade.leverage + realizedPnL = (sizeForPnL * accountPnL) / 100 + } } // Determine exit reason from trade state and P&L @@ -504,7 +528,7 @@ export class PositionManager { try { await updateTradeExit({ positionId: trade.positionId, - exitPrice: currentPrice, + exitPrice: exitPrice, // Use estimated exit price, not current market price exitReason, realizedPnL, exitOrderTx: 'ON_CHAIN_ORDER', @@ -516,7 +540,7 @@ export class PositionManager { maxFavorablePrice: trade.maxFavorablePrice, maxAdversePrice: trade.maxAdversePrice, }) - console.log(`💾 External closure recorded: ${exitReason} at $${currentPrice} | P&L: $${realizedPnL.toFixed(2)}`) + console.log(`💾 External closure recorded: ${exitReason} at $${exitPrice.toFixed(6)} | P&L: $${realizedPnL.toFixed(2)}`) } catch (dbError) { console.error('❌ Failed to save external closure:', dbError) }