Enhance trailing stop with ATR-based sizing
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { DEFAULT_TRADING_CONFIG } from '@/config/trading'
|
||||||
|
|
||||||
const ENV_FILE_PATH = path.join(process.cwd(), '.env')
|
const ENV_FILE_PATH = path.join(process.cwd(), '.env')
|
||||||
|
|
||||||
@@ -50,6 +51,11 @@ function updateEnvFile(updates: Record<string, any>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
fs.writeFileSync(ENV_FILE_PATH, content, 'utf-8')
|
fs.writeFileSync(ENV_FILE_PATH, content, 'utf-8')
|
||||||
|
|
||||||
|
// Also update in-memory environment so running process sees new values immediately
|
||||||
|
Object.entries(updates).forEach(([key, value]) => {
|
||||||
|
process.env[key] = value
|
||||||
|
})
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to write .env file:', error)
|
console.error('Failed to write .env file:', error)
|
||||||
@@ -86,6 +92,9 @@ export async function GET() {
|
|||||||
PROFIT_LOCK_PERCENT: parseFloat(env.PROFIT_LOCK_PERCENT || '0.4'),
|
PROFIT_LOCK_PERCENT: parseFloat(env.PROFIT_LOCK_PERCENT || '0.4'),
|
||||||
USE_TRAILING_STOP: env.USE_TRAILING_STOP === 'true' || env.USE_TRAILING_STOP === undefined,
|
USE_TRAILING_STOP: env.USE_TRAILING_STOP === 'true' || env.USE_TRAILING_STOP === undefined,
|
||||||
TRAILING_STOP_PERCENT: parseFloat(env.TRAILING_STOP_PERCENT || '0.3'),
|
TRAILING_STOP_PERCENT: parseFloat(env.TRAILING_STOP_PERCENT || '0.3'),
|
||||||
|
TRAILING_STOP_ATR_MULTIPLIER: parseFloat(env.TRAILING_STOP_ATR_MULTIPLIER || '1.5'),
|
||||||
|
TRAILING_STOP_MIN_PERCENT: parseFloat(env.TRAILING_STOP_MIN_PERCENT || '0.25'),
|
||||||
|
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'),
|
||||||
|
|
||||||
// Position Scaling
|
// Position Scaling
|
||||||
@@ -144,6 +153,9 @@ export async function POST(request: NextRequest) {
|
|||||||
PROFIT_LOCK_PERCENT: settings.PROFIT_LOCK_PERCENT.toString(),
|
PROFIT_LOCK_PERCENT: settings.PROFIT_LOCK_PERCENT.toString(),
|
||||||
USE_TRAILING_STOP: settings.USE_TRAILING_STOP.toString(),
|
USE_TRAILING_STOP: settings.USE_TRAILING_STOP.toString(),
|
||||||
TRAILING_STOP_PERCENT: settings.TRAILING_STOP_PERCENT.toString(),
|
TRAILING_STOP_PERCENT: settings.TRAILING_STOP_PERCENT.toString(),
|
||||||
|
TRAILING_STOP_ATR_MULTIPLIER: (settings.TRAILING_STOP_ATR_MULTIPLIER ?? DEFAULT_TRADING_CONFIG.trailingStopAtrMultiplier).toString(),
|
||||||
|
TRAILING_STOP_MIN_PERCENT: (settings.TRAILING_STOP_MIN_PERCENT ?? DEFAULT_TRADING_CONFIG.trailingStopMinPercent).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(),
|
||||||
|
|
||||||
// Position Scaling
|
// Position Scaling
|
||||||
@@ -167,6 +179,15 @@ export async function POST(request: NextRequest) {
|
|||||||
const success = updateEnvFile(updates)
|
const success = updateEnvFile(updates)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
try {
|
||||||
|
const { getPositionManager } = await import('@/lib/trading/position-manager')
|
||||||
|
const manager = getPositionManager()
|
||||||
|
manager.refreshConfig()
|
||||||
|
console.log('⚙️ Position manager config refreshed after settings update')
|
||||||
|
} catch (pmError) {
|
||||||
|
console.error('Failed to refresh position manager config:', pmError)
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface ExecuteTradeResponse {
|
|||||||
direction?: 'long' | 'short'
|
direction?: 'long' | 'short'
|
||||||
entryPrice?: number
|
entryPrice?: number
|
||||||
positionSize?: number
|
positionSize?: number
|
||||||
|
requestedPositionSize?: number
|
||||||
|
fillCoveragePercent?: number
|
||||||
leverage?: number
|
leverage?: number
|
||||||
stopLoss?: number
|
stopLoss?: number
|
||||||
takeProfit1?: number
|
takeProfit1?: number
|
||||||
@@ -178,8 +180,16 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
|
|
||||||
// Update Position Manager tracking
|
// Update Position Manager tracking
|
||||||
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
|
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
|
||||||
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize
|
const actualScaleNotional = scaleResult.fillNotionalUSD ?? scaleSize
|
||||||
const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0)
|
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + actualScaleNotional
|
||||||
|
const newTotalSize = sameDirectionPosition.currentSize + actualScaleNotional
|
||||||
|
|
||||||
|
if (scaleSize > 0) {
|
||||||
|
const coverage = (actualScaleNotional / scaleSize) * 100
|
||||||
|
if (coverage < 99.5) {
|
||||||
|
console.log(`⚠️ Scale fill coverage: ${coverage.toFixed(2)}% of requested $${scaleSize.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the trade tracking (simplified - just update the active trade object)
|
// Update the trade tracking (simplified - just update the active trade object)
|
||||||
sameDirectionPosition.timesScaled = timesScaled
|
sameDirectionPosition.timesScaled = timesScaled
|
||||||
@@ -269,20 +279,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate position size with leverage
|
// Calculate requested position size with leverage
|
||||||
const positionSizeUSD = positionSize * leverage
|
const requestedPositionSizeUSD = positionSize * leverage
|
||||||
|
|
||||||
console.log(`💰 Opening ${body.direction} position:`)
|
console.log(`💰 Opening ${body.direction} position:`)
|
||||||
console.log(` Symbol: ${driftSymbol}`)
|
console.log(` Symbol: ${driftSymbol}`)
|
||||||
console.log(` Base size: $${positionSize}`)
|
console.log(` Base size: $${positionSize}`)
|
||||||
console.log(` Leverage: ${leverage}x`)
|
console.log(` Leverage: ${leverage}x`)
|
||||||
console.log(` Total position: $${positionSizeUSD}`)
|
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
|
||||||
|
|
||||||
// Open position
|
// Open position
|
||||||
const openResult = await openPosition({
|
const openResult = await openPosition({
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
sizeUSD: positionSizeUSD,
|
sizeUSD: requestedPositionSizeUSD,
|
||||||
slippageTolerance: config.slippageTolerance,
|
slippageTolerance: config.slippageTolerance,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -300,7 +310,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
||||||
if (openResult.isPhantom) {
|
if (openResult.isPhantom) {
|
||||||
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
|
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
|
||||||
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
|
console.error(` Expected: $${requestedPositionSizeUSD.toFixed(2)}`)
|
||||||
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
||||||
|
|
||||||
// Save phantom trade to database for analysis
|
// Save phantom trade to database for analysis
|
||||||
@@ -319,7 +329,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
entryPrice: openResult.fillPrice!,
|
entryPrice: openResult.fillPrice!,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: requestedPositionSizeUSD,
|
||||||
leverage: config.leverage,
|
leverage: config.leverage,
|
||||||
stopLossPrice: 0, // Not applicable for phantom
|
stopLossPrice: 0, // Not applicable for phantom
|
||||||
takeProfit1Price: 0,
|
takeProfit1Price: 0,
|
||||||
@@ -339,7 +349,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
// Phantom-specific fields
|
// Phantom-specific fields
|
||||||
status: 'phantom',
|
status: 'phantom',
|
||||||
isPhantom: true,
|
isPhantom: true,
|
||||||
expectedSizeUSD: positionSizeUSD,
|
expectedSizeUSD: requestedPositionSizeUSD,
|
||||||
actualSizeUSD: openResult.actualSizeUSD,
|
actualSizeUSD: openResult.actualSizeUSD,
|
||||||
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
||||||
})
|
})
|
||||||
@@ -353,7 +363,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Phantom trade detected',
|
error: 'Phantom trade detected',
|
||||||
message: `Position opened but size mismatch detected. Expected $${positionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
message: `Position opened but size mismatch detected. Expected $${requestedPositionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
@@ -361,6 +371,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
|
|
||||||
// Calculate stop loss and take profit prices
|
// Calculate stop loss and take profit prices
|
||||||
const entryPrice = openResult.fillPrice!
|
const entryPrice = openResult.fillPrice!
|
||||||
|
const actualPositionSizeUSD = openResult.fillNotionalUSD ?? requestedPositionSizeUSD
|
||||||
|
const filledBaseSize = openResult.fillSize !== undefined
|
||||||
|
? Math.abs(openResult.fillSize)
|
||||||
|
: (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0)
|
||||||
|
const fillCoverage = requestedPositionSizeUSD > 0
|
||||||
|
? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100
|
||||||
|
: 100
|
||||||
|
|
||||||
|
console.log('📏 Fill results:')
|
||||||
|
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${driftSymbol.split('-')[0]}`)
|
||||||
|
console.log(` Filled notional: $${actualPositionSizeUSD.toFixed(2)}`)
|
||||||
|
if (fillCoverage < 99.5) {
|
||||||
|
console.log(` ⚠️ Partial fill: ${fillCoverage.toFixed(2)}% of requested size`)
|
||||||
|
}
|
||||||
|
|
||||||
const stopLossPrice = calculatePrice(
|
const stopLossPrice = calculatePrice(
|
||||||
entryPrice,
|
entryPrice,
|
||||||
@@ -421,13 +445,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
entryPrice,
|
entryPrice,
|
||||||
entryTime: Date.now(),
|
entryTime: Date.now(),
|
||||||
positionSize: positionSizeUSD,
|
positionSize: actualPositionSizeUSD,
|
||||||
leverage: config.leverage,
|
leverage: config.leverage,
|
||||||
stopLossPrice,
|
stopLossPrice,
|
||||||
tp1Price,
|
tp1Price,
|
||||||
tp2Price,
|
tp2Price,
|
||||||
emergencyStopPrice,
|
emergencyStopPrice,
|
||||||
currentSize: positionSizeUSD,
|
currentSize: actualPositionSizeUSD,
|
||||||
tp1Hit: false,
|
tp1Hit: false,
|
||||||
tp2Hit: false,
|
tp2Hit: false,
|
||||||
slMovedToBreakeven: false,
|
slMovedToBreakeven: false,
|
||||||
@@ -446,6 +470,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
originalAdx: body.adx, // Store for scaling validation
|
originalAdx: body.adx, // Store for scaling validation
|
||||||
timesScaled: 0,
|
timesScaled: 0,
|
||||||
totalScaleAdded: 0,
|
totalScaleAdded: 0,
|
||||||
|
atrAtEntry: body.atr,
|
||||||
|
runnerTrailingPercent: undefined,
|
||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
@@ -458,7 +484,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
try {
|
try {
|
||||||
const exitRes = await placeExitOrders({
|
const exitRes = await placeExitOrders({
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: actualPositionSizeUSD,
|
||||||
entryPrice: entryPrice,
|
entryPrice: entryPrice,
|
||||||
tp1Price,
|
tp1Price,
|
||||||
tp2Price,
|
tp2Price,
|
||||||
@@ -495,7 +521,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
entryPrice: entryPrice,
|
entryPrice: entryPrice,
|
||||||
positionSize: positionSizeUSD,
|
positionSize: actualPositionSizeUSD,
|
||||||
|
requestedPositionSize: requestedPositionSizeUSD,
|
||||||
|
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
||||||
leverage: config.leverage,
|
leverage: config.leverage,
|
||||||
stopLoss: stopLossPrice,
|
stopLoss: stopLossPrice,
|
||||||
takeProfit1: tp1Price,
|
takeProfit1: tp1Price,
|
||||||
@@ -529,7 +557,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
entryPrice,
|
entryPrice,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: actualPositionSizeUSD,
|
||||||
leverage: config.leverage,
|
leverage: config.leverage,
|
||||||
stopLossPrice,
|
stopLossPrice,
|
||||||
takeProfit1Price: tp1Price,
|
takeProfit1Price: tp1Price,
|
||||||
@@ -554,6 +582,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
volumeAtEntry: body.volumeRatio,
|
volumeAtEntry: body.volumeRatio,
|
||||||
pricePositionAtEntry: body.pricePosition,
|
pricePositionAtEntry: body.pricePosition,
|
||||||
signalQualityScore: qualityResult.score,
|
signalQualityScore: qualityResult.score,
|
||||||
|
expectedSizeUSD: requestedPositionSizeUSD,
|
||||||
|
actualSizeUSD: actualPositionSizeUSD,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`)
|
console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`)
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
maxAdverseExcursion: 0,
|
maxAdverseExcursion: 0,
|
||||||
maxFavorablePrice: entryPrice,
|
maxFavorablePrice: entryPrice,
|
||||||
maxAdversePrice: entryPrice,
|
maxAdversePrice: entryPrice,
|
||||||
|
atrAtEntry: undefined,
|
||||||
|
runnerTrailingPercent: undefined,
|
||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface TestTradeResponse {
|
|||||||
direction?: 'long' | 'short'
|
direction?: 'long' | 'short'
|
||||||
entryPrice?: number
|
entryPrice?: number
|
||||||
positionSize?: number
|
positionSize?: number
|
||||||
|
requestedPositionSize?: number
|
||||||
|
fillCoveragePercent?: number
|
||||||
stopLoss?: number
|
stopLoss?: number
|
||||||
takeProfit1?: number
|
takeProfit1?: number
|
||||||
takeProfit2?: number
|
takeProfit2?: number
|
||||||
@@ -94,19 +96,19 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate position size with leverage
|
// Calculate position size with leverage
|
||||||
const positionSizeUSD = positionSize * leverage
|
const requestedPositionSizeUSD = positionSize * leverage
|
||||||
|
|
||||||
console.log(`💰 Opening ${direction} position:`)
|
console.log(`💰 Opening ${direction} position:`)
|
||||||
console.log(` Symbol: ${driftSymbol}`)
|
console.log(` Symbol: ${driftSymbol}`)
|
||||||
console.log(` Base size: $${positionSize}`)
|
console.log(` Base size: $${positionSize}`)
|
||||||
console.log(` Leverage: ${leverage}x`)
|
console.log(` Leverage: ${leverage}x`)
|
||||||
console.log(` Total position: $${positionSizeUSD}`)
|
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
|
||||||
|
|
||||||
// Open position
|
// Open position
|
||||||
const openResult = await openPosition({
|
const openResult = await openPosition({
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
sizeUSD: positionSizeUSD,
|
sizeUSD: requestedPositionSizeUSD,
|
||||||
slippageTolerance: config.slippageTolerance,
|
slippageTolerance: config.slippageTolerance,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -123,6 +125,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
|
|
||||||
// Calculate stop loss and take profit prices
|
// Calculate stop loss and take profit prices
|
||||||
const entryPrice = openResult.fillPrice!
|
const entryPrice = openResult.fillPrice!
|
||||||
|
const actualPositionSizeUSD = openResult.fillNotionalUSD ?? requestedPositionSizeUSD
|
||||||
|
const filledBaseSize = openResult.fillSize !== undefined
|
||||||
|
? Math.abs(openResult.fillSize)
|
||||||
|
: (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0)
|
||||||
|
const fillCoverage = requestedPositionSizeUSD > 0
|
||||||
|
? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100
|
||||||
|
: 100
|
||||||
|
|
||||||
|
console.log('📏 Fill results:')
|
||||||
|
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${driftSymbol.split('-')[0]}`)
|
||||||
|
console.log(` Filled notional: $${actualPositionSizeUSD.toFixed(2)}`)
|
||||||
|
if (fillCoverage < 99.5) {
|
||||||
|
console.log(` ⚠️ Partial fill: ${fillCoverage.toFixed(2)}% of requested size`)
|
||||||
|
}
|
||||||
|
|
||||||
const stopLossPrice = calculatePrice(
|
const stopLossPrice = calculatePrice(
|
||||||
entryPrice,
|
entryPrice,
|
||||||
@@ -183,13 +199,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
direction: direction,
|
direction: direction,
|
||||||
entryPrice,
|
entryPrice,
|
||||||
entryTime: Date.now(),
|
entryTime: Date.now(),
|
||||||
positionSize: positionSizeUSD,
|
positionSize: actualPositionSizeUSD,
|
||||||
leverage: leverage,
|
leverage: leverage,
|
||||||
stopLossPrice,
|
stopLossPrice,
|
||||||
tp1Price,
|
tp1Price,
|
||||||
tp2Price,
|
tp2Price,
|
||||||
emergencyStopPrice,
|
emergencyStopPrice,
|
||||||
currentSize: positionSizeUSD,
|
currentSize: actualPositionSizeUSD,
|
||||||
tp1Hit: false,
|
tp1Hit: false,
|
||||||
tp2Hit: false,
|
tp2Hit: false,
|
||||||
slMovedToBreakeven: false,
|
slMovedToBreakeven: false,
|
||||||
@@ -204,6 +220,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
maxAdverseExcursion: 0,
|
maxAdverseExcursion: 0,
|
||||||
maxFavorablePrice: entryPrice,
|
maxFavorablePrice: entryPrice,
|
||||||
maxAdversePrice: entryPrice,
|
maxAdversePrice: entryPrice,
|
||||||
|
atrAtEntry: undefined,
|
||||||
|
runnerTrailingPercent: undefined,
|
||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
@@ -222,7 +240,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
entryPrice: entryPrice,
|
entryPrice: entryPrice,
|
||||||
positionSize: positionSizeUSD,
|
positionSize: actualPositionSizeUSD,
|
||||||
|
requestedPositionSize: requestedPositionSizeUSD,
|
||||||
|
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
||||||
stopLoss: stopLossPrice,
|
stopLoss: stopLossPrice,
|
||||||
takeProfit1: tp1Price,
|
takeProfit1: tp1Price,
|
||||||
takeProfit2: tp2Price,
|
takeProfit2: tp2Price,
|
||||||
@@ -237,7 +257,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
try {
|
try {
|
||||||
const exitRes = await placeExitOrders({
|
const exitRes = await placeExitOrders({
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: actualPositionSizeUSD,
|
||||||
entryPrice: entryPrice,
|
entryPrice: entryPrice,
|
||||||
tp1Price,
|
tp1Price,
|
||||||
tp2Price,
|
tp2Price,
|
||||||
@@ -274,7 +294,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
entryPrice,
|
entryPrice,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: actualPositionSizeUSD,
|
||||||
leverage: leverage,
|
leverage: leverage,
|
||||||
stopLossPrice,
|
stopLossPrice,
|
||||||
takeProfit1Price: tp1Price,
|
takeProfit1Price: tp1Price,
|
||||||
@@ -292,6 +312,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
hardStopPrice,
|
hardStopPrice,
|
||||||
signalStrength: 'test',
|
signalStrength: 'test',
|
||||||
timeframe: 'manual',
|
timeframe: 'manual',
|
||||||
|
expectedSizeUSD: requestedPositionSizeUSD,
|
||||||
|
actualSizeUSD: actualPositionSizeUSD,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('💾 Trade saved to database')
|
console.log('💾 Trade saved to database')
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export interface TradingConfig {
|
|||||||
|
|
||||||
// Trailing stop for runner (after TP2)
|
// Trailing stop for runner (after TP2)
|
||||||
useTrailingStop: boolean // Enable trailing stop for remaining position
|
useTrailingStop: boolean // Enable trailing stop for remaining position
|
||||||
trailingStopPercent: number // Trail by this % below peak
|
trailingStopPercent: number // Legacy fixed trail percent (used as fallback)
|
||||||
|
trailingStopAtrMultiplier: number // Multiplier for ATR-based trailing distance
|
||||||
|
trailingStopMinPercent: number // Minimum trailing distance in percent
|
||||||
|
trailingStopMaxPercent: number // Maximum trailing distance in percent
|
||||||
trailingStopActivation: number // Activate when runner profits exceed this %
|
trailingStopActivation: number // Activate when runner profits exceed this %
|
||||||
|
|
||||||
// Position Scaling (add to winning positions)
|
// Position Scaling (add to winning positions)
|
||||||
@@ -115,7 +118,10 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
|||||||
|
|
||||||
// Trailing stop for runner (after TP2)
|
// Trailing stop for runner (after TP2)
|
||||||
useTrailingStop: true, // Enable trailing stop for remaining position after TP2
|
useTrailingStop: true, // Enable trailing stop for remaining position after TP2
|
||||||
trailingStopPercent: 0.3, // Trail by 0.3% below peak price
|
trailingStopPercent: 0.3, // Legacy fallback (%, used if ATR data unavailable)
|
||||||
|
trailingStopAtrMultiplier: 1.5, // Trail ~1.5x ATR (converted to % of price)
|
||||||
|
trailingStopMinPercent: 0.25, // Never trail tighter than 0.25%
|
||||||
|
trailingStopMaxPercent: 0.9, // Cap trailing distance at 0.9%
|
||||||
trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit
|
trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit
|
||||||
|
|
||||||
// Position Scaling (conservative defaults)
|
// Position Scaling (conservative defaults)
|
||||||
@@ -248,6 +254,18 @@ export function validateTradingConfig(config: TradingConfig): void {
|
|||||||
if (config.slippageTolerance < 0 || config.slippageTolerance > 10) {
|
if (config.slippageTolerance < 0 || config.slippageTolerance > 10) {
|
||||||
throw new Error('Slippage tolerance must be between 0 and 10%')
|
throw new Error('Slippage tolerance must be between 0 and 10%')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.trailingStopAtrMultiplier <= 0) {
|
||||||
|
throw new Error('Trailing stop ATR multiplier must be positive')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.trailingStopMinPercent < 0 || config.trailingStopMaxPercent < 0) {
|
||||||
|
throw new Error('Trailing stop bounds must be non-negative')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.trailingStopMinPercent > config.trailingStopMaxPercent) {
|
||||||
|
throw new Error('Trailing stop min percent cannot exceed max percent')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment-based configuration
|
// Environment-based configuration
|
||||||
@@ -321,6 +339,15 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
|||||||
trailingStopPercent: process.env.TRAILING_STOP_PERCENT
|
trailingStopPercent: process.env.TRAILING_STOP_PERCENT
|
||||||
? parseFloat(process.env.TRAILING_STOP_PERCENT)
|
? parseFloat(process.env.TRAILING_STOP_PERCENT)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
trailingStopAtrMultiplier: process.env.TRAILING_STOP_ATR_MULTIPLIER
|
||||||
|
? parseFloat(process.env.TRAILING_STOP_ATR_MULTIPLIER)
|
||||||
|
: undefined,
|
||||||
|
trailingStopMinPercent: process.env.TRAILING_STOP_MIN_PERCENT
|
||||||
|
? parseFloat(process.env.TRAILING_STOP_MIN_PERCENT)
|
||||||
|
: undefined,
|
||||||
|
trailingStopMaxPercent: process.env.TRAILING_STOP_MAX_PERCENT
|
||||||
|
? parseFloat(process.env.TRAILING_STOP_MAX_PERCENT)
|
||||||
|
: undefined,
|
||||||
trailingStopActivation: process.env.TRAILING_STOP_ACTIVATION
|
trailingStopActivation: process.env.TRAILING_STOP_ACTIVATION
|
||||||
? parseFloat(process.env.TRAILING_STOP_ACTIVATION)
|
? parseFloat(process.env.TRAILING_STOP_ACTIVATION)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export interface UpdateTradeStateParams {
|
|||||||
maxAdverseExcursion?: number
|
maxAdverseExcursion?: number
|
||||||
maxFavorablePrice?: number
|
maxFavorablePrice?: number
|
||||||
maxAdversePrice?: number
|
maxAdversePrice?: number
|
||||||
|
runnerTrailingPercent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTradeExitParams {
|
export interface UpdateTradeExitParams {
|
||||||
@@ -235,6 +236,7 @@ export async function updateTradeState(params: UpdateTradeStateParams) {
|
|||||||
maxAdverseExcursion: params.maxAdverseExcursion,
|
maxAdverseExcursion: params.maxAdverseExcursion,
|
||||||
maxFavorablePrice: params.maxFavorablePrice,
|
maxFavorablePrice: params.maxFavorablePrice,
|
||||||
maxAdversePrice: params.maxAdversePrice,
|
maxAdversePrice: params.maxAdversePrice,
|
||||||
|
runnerTrailingPercent: params.runnerTrailingPercent,
|
||||||
lastUpdate: new Date().toISOString(),
|
lastUpdate: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface OpenPositionResult {
|
|||||||
transactionSignature?: string
|
transactionSignature?: string
|
||||||
fillPrice?: number
|
fillPrice?: number
|
||||||
fillSize?: number
|
fillSize?: number
|
||||||
|
fillNotionalUSD?: number
|
||||||
slippage?: number
|
slippage?: number
|
||||||
error?: string
|
error?: string
|
||||||
isPhantom?: boolean // Position opened but size mismatch detected
|
isPhantom?: boolean // Position opened but size mismatch detected
|
||||||
@@ -124,6 +125,7 @@ export async function openPosition(
|
|||||||
transactionSignature: mockTxSig,
|
transactionSignature: mockTxSig,
|
||||||
fillPrice: oraclePrice,
|
fillPrice: oraclePrice,
|
||||||
fillSize: baseAssetSize,
|
fillSize: baseAssetSize,
|
||||||
|
fillNotionalUSD: baseAssetSize * oraclePrice,
|
||||||
slippage: 0,
|
slippage: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,19 +181,22 @@ export async function openPosition(
|
|||||||
|
|
||||||
if (position && position.side !== 'none') {
|
if (position && position.side !== 'none') {
|
||||||
const fillPrice = position.entryPrice
|
const fillPrice = position.entryPrice
|
||||||
|
const filledBaseSize = Math.abs(position.size)
|
||||||
|
const fillNotionalUSD = filledBaseSize * fillPrice
|
||||||
const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100
|
const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100
|
||||||
|
|
||||||
// CRITICAL: Validate actual position size vs expected
|
// CRITICAL: Validate actual position size vs expected
|
||||||
// Phantom trade detection: Check if position is significantly smaller than expected
|
// Phantom trade detection: Check if position is significantly smaller than expected
|
||||||
const actualSizeUSD = position.size * fillPrice
|
|
||||||
const expectedSizeUSD = params.sizeUSD
|
const expectedSizeUSD = params.sizeUSD
|
||||||
const sizeRatio = actualSizeUSD / expectedSizeUSD
|
const sizeRatio = expectedSizeUSD > 0 ? fillNotionalUSD / expectedSizeUSD : 1
|
||||||
|
|
||||||
console.log(`💰 Fill details:`)
|
console.log(`💰 Fill details:`)
|
||||||
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
|
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
|
||||||
|
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${params.symbol.split('-')[0]}`)
|
||||||
|
console.log(` Filled notional: $${fillNotionalUSD.toFixed(2)}`)
|
||||||
console.log(` Slippage: ${slippage.toFixed(3)}%`)
|
console.log(` Slippage: ${slippage.toFixed(3)}%`)
|
||||||
console.log(` Expected size: $${expectedSizeUSD.toFixed(2)}`)
|
console.log(` Expected size: $${expectedSizeUSD.toFixed(2)}`)
|
||||||
console.log(` Actual size: $${actualSizeUSD.toFixed(2)}`)
|
console.log(` Actual size: $${fillNotionalUSD.toFixed(2)}`)
|
||||||
console.log(` Size ratio: ${(sizeRatio * 100).toFixed(1)}%`)
|
console.log(` Size ratio: ${(sizeRatio * 100).toFixed(1)}%`)
|
||||||
|
|
||||||
// Flag as phantom if actual size is less than 50% of expected
|
// Flag as phantom if actual size is less than 50% of expected
|
||||||
@@ -200,7 +205,7 @@ export async function openPosition(
|
|||||||
if (isPhantom) {
|
if (isPhantom) {
|
||||||
console.error(`🚨 PHANTOM POSITION DETECTED!`)
|
console.error(`🚨 PHANTOM POSITION DETECTED!`)
|
||||||
console.error(` Expected: $${expectedSizeUSD.toFixed(2)}`)
|
console.error(` Expected: $${expectedSizeUSD.toFixed(2)}`)
|
||||||
console.error(` Actual: $${actualSizeUSD.toFixed(2)}`)
|
console.error(` Actual: $${fillNotionalUSD.toFixed(2)}`)
|
||||||
console.error(` This indicates the order was rejected or partially filled by Drift`)
|
console.error(` This indicates the order was rejected or partially filled by Drift`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,10 +213,11 @@ export async function openPosition(
|
|||||||
success: true,
|
success: true,
|
||||||
transactionSignature: txSig,
|
transactionSignature: txSig,
|
||||||
fillPrice,
|
fillPrice,
|
||||||
fillSize: position.size, // Use actual size from Drift, not calculated
|
fillSize: filledBaseSize,
|
||||||
|
fillNotionalUSD,
|
||||||
slippage,
|
slippage,
|
||||||
isPhantom,
|
isPhantom,
|
||||||
actualSizeUSD,
|
actualSizeUSD: fillNotionalUSD,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Position not found yet (may be DRY_RUN mode)
|
// Position not found yet (may be DRY_RUN mode)
|
||||||
@@ -223,6 +229,7 @@ export async function openPosition(
|
|||||||
transactionSignature: txSig,
|
transactionSignature: txSig,
|
||||||
fillPrice: oraclePrice,
|
fillPrice: oraclePrice,
|
||||||
fillSize: baseAssetSize,
|
fillSize: baseAssetSize,
|
||||||
|
fillNotionalUSD: baseAssetSize * oraclePrice,
|
||||||
slippage: 0,
|
slippage: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface ActiveTrade {
|
|||||||
slMovedToBreakeven: boolean
|
slMovedToBreakeven: boolean
|
||||||
slMovedToProfit: boolean
|
slMovedToProfit: boolean
|
||||||
trailingStopActive: boolean
|
trailingStopActive: boolean
|
||||||
|
runnerTrailingPercent?: number // Latest dynamic trailing percent applied
|
||||||
|
|
||||||
// P&L tracking
|
// P&L tracking
|
||||||
realizedPnL: number
|
realizedPnL: number
|
||||||
@@ -52,6 +53,7 @@ export interface ActiveTrade {
|
|||||||
originalAdx?: number // ADX at initial entry (for scaling validation)
|
originalAdx?: number // ADX at initial entry (for scaling validation)
|
||||||
timesScaled?: number // How many times position has been scaled
|
timesScaled?: number // How many times position has been scaled
|
||||||
totalScaleAdded?: number // Total USD added through scaling
|
totalScaleAdded?: number // Total USD added through scaling
|
||||||
|
atrAtEntry?: number // ATR (absolute) when trade was opened
|
||||||
|
|
||||||
// Monitoring
|
// Monitoring
|
||||||
priceCheckCount: number
|
priceCheckCount: number
|
||||||
@@ -117,6 +119,7 @@ export class PositionManager {
|
|||||||
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
|
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
|
||||||
slMovedToProfit: pmState?.slMovedToProfit ?? false,
|
slMovedToProfit: pmState?.slMovedToProfit ?? false,
|
||||||
trailingStopActive: pmState?.trailingStopActive ?? false,
|
trailingStopActive: pmState?.trailingStopActive ?? false,
|
||||||
|
runnerTrailingPercent: pmState?.runnerTrailingPercent,
|
||||||
realizedPnL: pmState?.realizedPnL ?? 0,
|
realizedPnL: pmState?.realizedPnL ?? 0,
|
||||||
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
||||||
peakPnL: pmState?.peakPnL ?? 0,
|
peakPnL: pmState?.peakPnL ?? 0,
|
||||||
@@ -125,6 +128,7 @@ export class PositionManager {
|
|||||||
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
|
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
|
||||||
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
|
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
|
||||||
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
|
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
|
||||||
|
atrAtEntry: dbTrade.atrAtEntry ?? undefined,
|
||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
@@ -341,7 +345,10 @@ export class PositionManager {
|
|||||||
trade.tp2Hit = true
|
trade.tp2Hit = true
|
||||||
trade.currentSize = positionSizeUSD
|
trade.currentSize = positionSizeUSD
|
||||||
trade.trailingStopActive = true
|
trade.trailingStopActive = true
|
||||||
console.log(`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with ${this.config.trailingStopPercent}% trailing stop`)
|
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||||
|
console.log(
|
||||||
|
`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||||
|
)
|
||||||
|
|
||||||
await this.saveTradeState(trade)
|
await this.saveTradeState(trade)
|
||||||
|
|
||||||
@@ -687,8 +694,11 @@ export class PositionManager {
|
|||||||
if (percentToClose < 100) {
|
if (percentToClose < 100) {
|
||||||
trade.tp2Hit = true
|
trade.tp2Hit = true
|
||||||
trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100)
|
trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100)
|
||||||
|
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||||
|
|
||||||
console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`)
|
console.log(
|
||||||
|
`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining | trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||||
|
)
|
||||||
|
|
||||||
// Save state after TP2
|
// Save state after TP2
|
||||||
await this.saveTradeState(trade)
|
await this.saveTradeState(trade)
|
||||||
@@ -702,14 +712,17 @@ export class PositionManager {
|
|||||||
// Check if trailing stop should be activated
|
// Check if trailing stop should be activated
|
||||||
if (!trade.trailingStopActive && profitPercent >= this.config.trailingStopActivation) {
|
if (!trade.trailingStopActive && profitPercent >= this.config.trailingStopActivation) {
|
||||||
trade.trailingStopActive = true
|
trade.trailingStopActive = true
|
||||||
|
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||||
console.log(`🎯 Trailing stop activated at +${profitPercent.toFixed(2)}%`)
|
console.log(`🎯 Trailing stop activated at +${profitPercent.toFixed(2)}%`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If trailing stop is active, adjust SL dynamically
|
// If trailing stop is active, adjust SL dynamically
|
||||||
if (trade.trailingStopActive) {
|
if (trade.trailingStopActive) {
|
||||||
|
const trailingPercent = this.getRunnerTrailingPercent(trade)
|
||||||
|
trade.runnerTrailingPercent = trailingPercent
|
||||||
const trailingStopPrice = this.calculatePrice(
|
const trailingStopPrice = this.calculatePrice(
|
||||||
trade.peakPrice,
|
trade.peakPrice,
|
||||||
-this.config.trailingStopPercent, // Trail below peak
|
-trailingPercent, // Trail below peak
|
||||||
trade.direction
|
trade.direction
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -722,7 +735,7 @@ export class PositionManager {
|
|||||||
const oldSL = trade.stopLossPrice
|
const oldSL = trade.stopLossPrice
|
||||||
trade.stopLossPrice = trailingStopPrice
|
trade.stopLossPrice = trailingStopPrice
|
||||||
|
|
||||||
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)} → ${trailingStopPrice.toFixed(4)} (${this.config.trailingStopPercent}% below peak $${trade.peakPrice.toFixed(4)})`)
|
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)} → ${trailingStopPrice.toFixed(4)} (${trailingPercent.toFixed(3)}% below peak $${trade.peakPrice.toFixed(4)})`)
|
||||||
|
|
||||||
// Save state after trailing SL update (every 10 updates to avoid spam)
|
// Save state after trailing SL update (every 10 updates to avoid spam)
|
||||||
if (trade.priceCheckCount % 10 === 0) {
|
if (trade.priceCheckCount % 10 === 0) {
|
||||||
@@ -899,6 +912,29 @@ export class PositionManager {
|
|||||||
console.log('⚙️ Position manager config refreshed from environment')
|
console.log('⚙️ Position manager config refreshed from environment')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRunnerTrailingPercent(trade: ActiveTrade): number {
|
||||||
|
const fallbackPercent = this.config.trailingStopPercent
|
||||||
|
const atrValue = trade.atrAtEntry ?? 0
|
||||||
|
const entryPrice = trade.entryPrice
|
||||||
|
|
||||||
|
if (atrValue <= 0 || entryPrice <= 0 || !Number.isFinite(entryPrice)) {
|
||||||
|
return fallbackPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
const atrPercentOfPrice = (atrValue / entryPrice) * 100
|
||||||
|
if (!Number.isFinite(atrPercentOfPrice) || atrPercentOfPrice <= 0) {
|
||||||
|
return fallbackPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPercent = atrPercentOfPrice * this.config.trailingStopAtrMultiplier
|
||||||
|
const boundedPercent = Math.min(
|
||||||
|
this.config.trailingStopMaxPercent,
|
||||||
|
Math.max(this.config.trailingStopMinPercent, rawPercent)
|
||||||
|
)
|
||||||
|
|
||||||
|
return boundedPercent > 0 ? boundedPercent : fallbackPercent
|
||||||
|
}
|
||||||
|
|
||||||
private async handlePostTp1Adjustments(trade: ActiveTrade, context: string): Promise<void> {
|
private async handlePostTp1Adjustments(trade: ActiveTrade, context: string): Promise<void> {
|
||||||
if (trade.currentSize <= 0) {
|
if (trade.currentSize <= 0) {
|
||||||
console.log(`⚠️ Skipping TP1 adjustments for ${trade.symbol} (${context}) because current size is $${trade.currentSize.toFixed(2)}`)
|
console.log(`⚠️ Skipping TP1 adjustments for ${trade.symbol} (${context}) because current size is $${trade.currentSize.toFixed(2)}`)
|
||||||
@@ -1012,6 +1048,7 @@ export class PositionManager {
|
|||||||
unrealizedPnL: trade.unrealizedPnL,
|
unrealizedPnL: trade.unrealizedPnL,
|
||||||
peakPnL: trade.peakPnL,
|
peakPnL: trade.peakPnL,
|
||||||
lastPrice: trade.lastPrice,
|
lastPrice: trade.lastPrice,
|
||||||
|
runnerTrailingPercent: trade.runnerTrailingPercent,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to save trade state:', error)
|
console.error('❌ Failed to save trade state:', error)
|
||||||
|
|||||||
Reference in New Issue
Block a user