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
This commit is contained in:
17
.env
17
.env
@@ -105,7 +105,22 @@ TAKE_PROFIT_2_PERCENT=0.7
|
|||||||
|
|
||||||
# Take Profit 2 Size: What % of remaining position to close at TP2
|
# Take Profit 2 Size: What % of remaining position to close at TP2
|
||||||
# Example: 100 = close all remaining position
|
# 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
|
# Emergency Stop: Hard stop if this level is breached
|
||||||
# Example: -2.0% on 10x = -20% account loss (rare but protects from flash crashes)
|
# Example: -2.0% on 10x = -20% account loss (rare but protects from flash crashes)
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ export async function GET() {
|
|||||||
TRAILING_STOP_MAX_PERCENT: parseFloat(env.TRAILING_STOP_MAX_PERCENT || '0.9'),
|
TRAILING_STOP_MAX_PERCENT: parseFloat(env.TRAILING_STOP_MAX_PERCENT || '0.9'),
|
||||||
TRAILING_STOP_ACTIVATION: parseFloat(env.TRAILING_STOP_ACTIVATION || '0.5'),
|
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
|
// Position Scaling
|
||||||
ENABLE_POSITION_SCALING: env.ENABLE_POSITION_SCALING === 'true',
|
ENABLE_POSITION_SCALING: env.ENABLE_POSITION_SCALING === 'true',
|
||||||
MIN_SCALE_QUALITY_SCORE: parseInt(env.MIN_SCALE_QUALITY_SCORE || '75'),
|
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_MAX_PERCENT: (settings.TRAILING_STOP_MAX_PERCENT ?? DEFAULT_TRADING_CONFIG.trailingStopMaxPercent).toString(),
|
||||||
TRAILING_STOP_ACTIVATION: settings.TRAILING_STOP_ACTIVATION.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
|
// Position Scaling
|
||||||
ENABLE_POSITION_SCALING: settings.ENABLE_POSITION_SCALING.toString(),
|
ENABLE_POSITION_SCALING: settings.ENABLE_POSITION_SCALING.toString(),
|
||||||
MIN_SCALE_QUALITY_SCORE: settings.MIN_SCALE_QUALITY_SCORE.toString(),
|
MIN_SCALE_QUALITY_SCORE: settings.MIN_SCALE_QUALITY_SCORE.toString(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { initializeDriftService } from '@/lib/drift/client'
|
import { initializeDriftService } from '@/lib/drift/client'
|
||||||
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
||||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading'
|
||||||
import { getMergedConfig } from '@/config/trading'
|
import { getMergedConfig } from '@/config/trading'
|
||||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||||
import { createTrade, updateTradeExit } from '@/lib/database/trades'
|
import { createTrade, updateTradeExit } from '@/lib/database/trades'
|
||||||
@@ -420,9 +420,15 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
body.direction
|
body.direction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dynamicTp2Percent = calculateDynamicTp2(
|
||||||
|
entryPrice,
|
||||||
|
body.atr || 0, // ATR from TradingView signal
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const tp2Price = calculatePrice(
|
const tp2Price = calculatePrice(
|
||||||
entryPrice,
|
entryPrice,
|
||||||
config.takeProfit2Percent,
|
dynamicTp2Percent,
|
||||||
body.direction
|
body.direction
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -430,7 +436,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
||||||
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
||||||
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
||||||
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
|
console.log(` TP2: $${tp2Price.toFixed(4)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based)`)
|
||||||
|
|
||||||
// Calculate emergency stop
|
// Calculate emergency stop
|
||||||
const emergencyStopPrice = calculatePrice(
|
const emergencyStopPrice = calculatePrice(
|
||||||
@@ -532,7 +538,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
takeProfit2: tp2Price,
|
takeProfit2: tp2Price,
|
||||||
stopLossPercent: config.stopLossPercent,
|
stopLossPercent: config.stopLossPercent,
|
||||||
tp1Percent: config.takeProfit1Percent,
|
tp1Percent: config.takeProfit1Percent,
|
||||||
tp2Percent: config.takeProfit2Percent,
|
tp2Percent: dynamicTp2Percent,
|
||||||
entrySlippage: openResult.slippage,
|
entrySlippage: openResult.slippage,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { initializeDriftService } from '@/lib/drift/client'
|
import { initializeDriftService } from '@/lib/drift/client'
|
||||||
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
||||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading'
|
||||||
import { getMergedConfig } from '@/config/trading'
|
import { getMergedConfig } from '@/config/trading'
|
||||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||||
import { createTrade } from '@/lib/database/trades'
|
import { createTrade } from '@/lib/database/trades'
|
||||||
@@ -172,9 +172,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
direction
|
direction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Use ATR-based dynamic TP2 with simulated ATR for testing
|
||||||
|
const simulatedATR = entryPrice * 0.008 // Simulate 0.8% ATR for testing
|
||||||
|
|
||||||
|
const dynamicTp2Percent = calculateDynamicTp2(
|
||||||
|
entryPrice,
|
||||||
|
simulatedATR,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const tp2Price = calculatePrice(
|
const tp2Price = calculatePrice(
|
||||||
entryPrice,
|
entryPrice,
|
||||||
config.takeProfit2Percent,
|
dynamicTp2Percent,
|
||||||
direction
|
direction
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,7 +191,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
||||||
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
||||||
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
||||||
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
|
console.log(` TP2: $${tp2Price.toFixed(4)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based test)`)
|
||||||
|
|
||||||
// Calculate emergency stop
|
// Calculate emergency stop
|
||||||
const emergencyStopPrice = calculatePrice(
|
const emergencyStopPrice = calculatePrice(
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ interface TradingSettings {
|
|||||||
TRAILING_STOP_PERCENT: number
|
TRAILING_STOP_PERCENT: number
|
||||||
TRAILING_STOP_ACTIVATION: number
|
TRAILING_STOP_ACTIVATION: number
|
||||||
|
|
||||||
|
// ATR-based Dynamic Targets
|
||||||
|
USE_ATR_BASED_TARGETS: boolean
|
||||||
|
ATR_MULTIPLIER_FOR_TP2: number
|
||||||
|
MIN_TP2_PERCENT: number
|
||||||
|
MAX_TP2_PERCENT: number
|
||||||
|
|
||||||
// Position Scaling
|
// Position Scaling
|
||||||
ENABLE_POSITION_SCALING: boolean
|
ENABLE_POSITION_SCALING: boolean
|
||||||
MIN_SCALE_QUALITY_SCORE: number
|
MIN_SCALE_QUALITY_SCORE: number
|
||||||
@@ -167,10 +173,21 @@ export default function SettingsPage() {
|
|||||||
// Calculate gains/losses for risk calculator
|
// Calculate gains/losses for risk calculator
|
||||||
const tp1Gain = size * lev * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100)
|
const tp1Gain = size * lev * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100)
|
||||||
const tp2RunnerSize = size * (1 - settings.TAKE_PROFIT_1_SIZE_PERCENT / 100) // 25% remaining after TP1
|
const tp2RunnerSize = size * (1 - settings.TAKE_PROFIT_1_SIZE_PERCENT / 100) // 25% remaining after TP1
|
||||||
const runnerValue = tp2RunnerSize * lev * (settings.TAKE_PROFIT_2_PERCENT / 100) // Full 25% runner value at TP2
|
|
||||||
|
// Use ATR-based TP2 if enabled, otherwise use static
|
||||||
|
const tp2Percent = settings.USE_ATR_BASED_TARGETS
|
||||||
|
? `${settings.MIN_TP2_PERCENT}-${settings.MAX_TP2_PERCENT}% (ATR-based)`
|
||||||
|
: `${settings.TAKE_PROFIT_2_PERCENT}% (static)`
|
||||||
|
|
||||||
|
// For calculation, use max potential TP2 if ATR-based
|
||||||
|
const tp2CalcPercent = settings.USE_ATR_BASED_TARGETS
|
||||||
|
? settings.MAX_TP2_PERCENT
|
||||||
|
: settings.TAKE_PROFIT_2_PERCENT
|
||||||
|
|
||||||
|
const runnerValue = tp2RunnerSize * lev * (tp2CalcPercent / 100) // Full 25% runner value at TP2
|
||||||
const fullWin = tp1Gain + runnerValue
|
const fullWin = tp1Gain + runnerValue
|
||||||
|
|
||||||
return { maxLoss, tp1Gain, runnerValue, fullWin }
|
return { maxLoss, tp1Gain, runnerValue, fullWin, tp2Percent }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -228,6 +245,7 @@ export default function SettingsPage() {
|
|||||||
<div className="bg-green-500/10 border border-green-500/50 rounded-lg p-4">
|
<div className="bg-green-500/10 border border-green-500/50 rounded-lg p-4">
|
||||||
<div className="text-green-400 text-sm mb-1">Runner Value (25%)</div>
|
<div className="text-green-400 text-sm mb-1">Runner Value (25%)</div>
|
||||||
<div className="text-white text-2xl font-bold">+${risk.runnerValue.toFixed(2)}</div>
|
<div className="text-white text-2xl font-bold">+${risk.runnerValue.toFixed(2)}</div>
|
||||||
|
<div className="text-xs text-green-300 mt-1">{risk.tp2Percent}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-500/10 border border-purple-500/50 rounded-lg p-4">
|
<div className="bg-purple-500/10 border border-purple-500/50 rounded-lg p-4">
|
||||||
<div className="text-purple-400 text-sm mb-1">Full Win</div>
|
<div className="text-purple-400 text-sm mb-1">Full Win</div>
|
||||||
@@ -455,6 +473,54 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
{/* ATR-based Dynamic Targets */}
|
||||||
|
<Section title="📈 ATR-Based Dynamic Targets" description="Automatically scale TP2 based on market volatility to capture big moves">
|
||||||
|
<div className="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<p className="text-sm text-green-400 mb-2">
|
||||||
|
<strong>🎯 Capture Big Moves:</strong> When ATR is high (volatile markets), TP2 automatically scales higher to catch 4-5% moves instead of exiting early at 0.7%.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Example: If ATR = 1.2% and multiplier = 2.0, then TP2 = 2.4% (instead of fixed 0.7%). Perfect for trending markets!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Setting
|
||||||
|
label="Enable ATR-Based Targets"
|
||||||
|
value={(settings as any).USE_ATR_BASED_TARGETS ? 1 : 0}
|
||||||
|
onChange={(v) => 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."
|
||||||
|
/>
|
||||||
|
<Setting
|
||||||
|
label="ATR Multiplier for TP2"
|
||||||
|
value={(settings as any).ATR_MULTIPLIER_FOR_TP2 || 2.0}
|
||||||
|
onChange={(v) => 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."
|
||||||
|
/>
|
||||||
|
<Setting
|
||||||
|
label="Minimum TP2 (%)"
|
||||||
|
value={(settings as any).MIN_TP2_PERCENT || 0.7}
|
||||||
|
onChange={(v) => 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."
|
||||||
|
/>
|
||||||
|
<Setting
|
||||||
|
label="Maximum TP2 (%)"
|
||||||
|
value={(settings as any).MAX_TP2_PERCENT || 3.0}
|
||||||
|
onChange={(v) => 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."
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
{/* Dynamic Adjustments */}
|
{/* Dynamic Adjustments */}
|
||||||
<Section title="🎯 Dynamic Stop Loss" description="Automatically adjust SL as trade moves in profit">
|
<Section title="🎯 Dynamic Stop Loss" description="Automatically adjust SL as trade moves in profit">
|
||||||
<Setting
|
<Setting
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export interface TradingConfig {
|
|||||||
takeProfit2Percent: number // Positive number (e.g., 1.5)
|
takeProfit2Percent: number // Positive number (e.g., 1.5)
|
||||||
emergencyStopPercent: number // Hard stop (e.g., -2.0)
|
emergencyStopPercent: number // Hard stop (e.g., -2.0)
|
||||||
|
|
||||||
|
// ATR-based dynamic targets
|
||||||
|
useAtrBasedTargets: boolean // Enable ATR-based TP2 scaling
|
||||||
|
atrMultiplierForTp2: number // Multiply ATR by this for dynamic TP2 (e.g., 2.0)
|
||||||
|
minTp2Percent: number // Minimum TP2 level regardless of ATR
|
||||||
|
maxTp2Percent: number // Maximum TP2 level cap
|
||||||
|
|
||||||
// Dual Stop System (Advanced)
|
// Dual Stop System (Advanced)
|
||||||
useDualStops: boolean // Enable dual stop system
|
useDualStops: boolean // Enable dual stop system
|
||||||
softStopPercent: number // Soft stop trigger (e.g., -1.5)
|
softStopPercent: number // Soft stop trigger (e.g., -1.5)
|
||||||
@@ -105,6 +111,12 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
|||||||
takeProfit2Percent: 1.5, // +1.5% price = +15% account gain (closes 50%)
|
takeProfit2Percent: 1.5, // +1.5% price = +15% account gain (closes 50%)
|
||||||
emergencyStopPercent: -2.0, // -2% hard stop = -20% account loss
|
emergencyStopPercent: -2.0, // -2% hard stop = -20% account loss
|
||||||
|
|
||||||
|
// ATR-based dynamic targets (NEW)
|
||||||
|
useAtrBasedTargets: true, // Enable ATR-based TP2 scaling for big moves
|
||||||
|
atrMultiplierForTp2: 2.0, // TP2 = ATR × 2.0 (adapts to volatility)
|
||||||
|
minTp2Percent: 0.7, // Minimum TP2 (safety floor)
|
||||||
|
maxTp2Percent: 3.0, // Maximum TP2 (cap at 3% for 30% account gain)
|
||||||
|
|
||||||
// Dual Stop System
|
// Dual Stop System
|
||||||
useDualStops: false, // Disabled by default
|
useDualStops: false, // Disabled by default
|
||||||
softStopPercent: -1.5, // Soft stop (TRIGGER_LIMIT)
|
softStopPercent: -1.5, // Soft stop (TRIGGER_LIMIT)
|
||||||
@@ -230,6 +242,36 @@ export function getPositionSizeForSymbol(symbol: string, baseConfig: TradingConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate dynamic TP2 level based on ATR (Average True Range)
|
||||||
|
* Higher ATR = higher volatility = larger TP2 target to capture big moves
|
||||||
|
*/
|
||||||
|
export function calculateDynamicTp2(
|
||||||
|
basePrice: number,
|
||||||
|
atrValue: number,
|
||||||
|
config: TradingConfig
|
||||||
|
): number {
|
||||||
|
if (!config.useAtrBasedTargets || !atrValue) {
|
||||||
|
return config.takeProfit2Percent // Fall back to static TP2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ATR to percentage of current price
|
||||||
|
const atrPercent = (atrValue / basePrice) * 100
|
||||||
|
|
||||||
|
// Calculate dynamic TP2: ATR × multiplier
|
||||||
|
const dynamicTp2 = atrPercent * config.atrMultiplierForTp2
|
||||||
|
|
||||||
|
// Apply min/max bounds
|
||||||
|
const boundedTp2 = Math.max(
|
||||||
|
config.minTp2Percent,
|
||||||
|
Math.min(config.maxTp2Percent, dynamicTp2)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`📊 ATR-based TP2: ATR=${atrValue.toFixed(4)} (${atrPercent.toFixed(2)}%) × ${config.atrMultiplierForTp2} = ${dynamicTp2.toFixed(2)}% → ${boundedTp2.toFixed(2)}% (bounded)`)
|
||||||
|
|
||||||
|
return boundedTp2
|
||||||
|
}
|
||||||
|
|
||||||
// Validate trading configuration
|
// Validate trading configuration
|
||||||
export function validateTradingConfig(config: TradingConfig): void {
|
export function validateTradingConfig(config: TradingConfig): void {
|
||||||
if (config.positionSize <= 0) {
|
if (config.positionSize <= 0) {
|
||||||
@@ -325,6 +367,21 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
|||||||
takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT
|
takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT
|
||||||
? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT)
|
? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT)
|
||||||
: undefined,
|
: 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
|
breakEvenTriggerPercent: process.env.BREAKEVEN_TRIGGER_PERCENT
|
||||||
? parseFloat(process.env.BREAKEVEN_TRIGGER_PERCENT)
|
? parseFloat(process.env.BREAKEVEN_TRIGGER_PERCENT)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -468,14 +468,38 @@ export class PositionManager {
|
|||||||
|
|
||||||
// Calculate P&L first (set to 0 for phantom trades)
|
// Calculate P&L first (set to 0 for phantom trades)
|
||||||
let realizedPnL = 0
|
let realizedPnL = 0
|
||||||
|
let exitPrice = currentPrice
|
||||||
|
|
||||||
if (!wasPhantom) {
|
if (!wasPhantom) {
|
||||||
const profitPercent = this.calculateProfitPercent(
|
// For external closures, try to estimate a more realistic exit price
|
||||||
trade.entryPrice,
|
// Manual closures may happen at significantly different prices than current market
|
||||||
currentPrice,
|
const unrealizedPnL = trade.unrealizedPnL || 0
|
||||||
trade.direction
|
const positionSizeUSD = trade.positionSize
|
||||||
)
|
|
||||||
const accountPnL = profitPercent * trade.leverage
|
if (Math.abs(unrealizedPnL) > 1 && positionSizeUSD > 0) {
|
||||||
realizedPnL = (sizeForPnL * accountPnL) / 100
|
// 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
|
// Determine exit reason from trade state and P&L
|
||||||
@@ -504,7 +528,7 @@ export class PositionManager {
|
|||||||
try {
|
try {
|
||||||
await updateTradeExit({
|
await updateTradeExit({
|
||||||
positionId: trade.positionId,
|
positionId: trade.positionId,
|
||||||
exitPrice: currentPrice,
|
exitPrice: exitPrice, // Use estimated exit price, not current market price
|
||||||
exitReason,
|
exitReason,
|
||||||
realizedPnL,
|
realizedPnL,
|
||||||
exitOrderTx: 'ON_CHAIN_ORDER',
|
exitOrderTx: 'ON_CHAIN_ORDER',
|
||||||
@@ -516,7 +540,7 @@ export class PositionManager {
|
|||||||
maxFavorablePrice: trade.maxFavorablePrice,
|
maxFavorablePrice: trade.maxFavorablePrice,
|
||||||
maxAdversePrice: trade.maxAdversePrice,
|
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) {
|
} catch (dbError) {
|
||||||
console.error('❌ Failed to save external closure:', dbError)
|
console.error('❌ Failed to save external closure:', dbError)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user