Enhance trailing stop with ATR-based sizing
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { DEFAULT_TRADING_CONFIG } from '@/config/trading'
|
||||
|
||||
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')
|
||||
|
||||
// Also update in-memory environment so running process sees new values immediately
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
process.env[key] = value
|
||||
})
|
||||
return true
|
||||
} catch (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'),
|
||||
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_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'),
|
||||
|
||||
// Position Scaling
|
||||
@@ -144,6 +153,9 @@ export async function POST(request: NextRequest) {
|
||||
PROFIT_LOCK_PERCENT: settings.PROFIT_LOCK_PERCENT.toString(),
|
||||
USE_TRAILING_STOP: settings.USE_TRAILING_STOP.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(),
|
||||
|
||||
// Position Scaling
|
||||
@@ -167,6 +179,15 @@ export async function POST(request: NextRequest) {
|
||||
const success = updateEnvFile(updates)
|
||||
|
||||
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 })
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface ExecuteTradeResponse {
|
||||
direction?: 'long' | 'short'
|
||||
entryPrice?: number
|
||||
positionSize?: number
|
||||
requestedPositionSize?: number
|
||||
fillCoveragePercent?: number
|
||||
leverage?: number
|
||||
stopLoss?: number
|
||||
takeProfit1?: number
|
||||
@@ -178,8 +180,16 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
|
||||
// Update Position Manager tracking
|
||||
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
|
||||
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize
|
||||
const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0)
|
||||
const actualScaleNotional = scaleResult.fillNotionalUSD ?? scaleSize
|
||||
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)
|
||||
sameDirectionPosition.timesScaled = timesScaled
|
||||
@@ -269,20 +279,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
}
|
||||
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = positionSize * leverage
|
||||
// Calculate requested position size with leverage
|
||||
const requestedPositionSizeUSD = positionSize * leverage
|
||||
|
||||
console.log(`💰 Opening ${body.direction} position:`)
|
||||
console.log(` Symbol: ${driftSymbol}`)
|
||||
console.log(` Base size: $${positionSize}`)
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
|
||||
|
||||
// Open position
|
||||
const openResult = await openPosition({
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
sizeUSD: positionSizeUSD,
|
||||
sizeUSD: requestedPositionSizeUSD,
|
||||
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)
|
||||
if (openResult.isPhantom) {
|
||||
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)}`)
|
||||
|
||||
// Save phantom trade to database for analysis
|
||||
@@ -319,7 +329,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice: openResult.fillPrice!,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
positionSizeUSD: requestedPositionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice: 0, // Not applicable for phantom
|
||||
takeProfit1Price: 0,
|
||||
@@ -339,7 +349,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
// Phantom-specific fields
|
||||
status: 'phantom',
|
||||
isPhantom: true,
|
||||
expectedSizeUSD: positionSizeUSD,
|
||||
expectedSizeUSD: requestedPositionSizeUSD,
|
||||
actualSizeUSD: openResult.actualSizeUSD,
|
||||
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
||||
})
|
||||
@@ -353,7 +363,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
{
|
||||
success: false,
|
||||
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 }
|
||||
)
|
||||
@@ -361,6 +371,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
|
||||
// Calculate stop loss and take profit prices
|
||||
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(
|
||||
entryPrice,
|
||||
@@ -421,13 +445,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
entryTime: Date.now(),
|
||||
positionSize: positionSizeUSD,
|
||||
positionSize: actualPositionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
emergencyStopPrice,
|
||||
currentSize: positionSizeUSD,
|
||||
currentSize: actualPositionSizeUSD,
|
||||
tp1Hit: false,
|
||||
tp2Hit: false,
|
||||
slMovedToBreakeven: false,
|
||||
@@ -446,6 +470,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
originalAdx: body.adx, // Store for scaling validation
|
||||
timesScaled: 0,
|
||||
totalScaleAdded: 0,
|
||||
atrAtEntry: body.atr,
|
||||
runnerTrailingPercent: undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
@@ -458,7 +484,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
try {
|
||||
const exitRes = await placeExitOrders({
|
||||
symbol: driftSymbol,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
entryPrice: entryPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
@@ -495,7 +521,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice: entryPrice,
|
||||
positionSize: positionSizeUSD,
|
||||
positionSize: actualPositionSizeUSD,
|
||||
requestedPositionSize: requestedPositionSizeUSD,
|
||||
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
||||
leverage: config.leverage,
|
||||
stopLoss: stopLossPrice,
|
||||
takeProfit1: tp1Price,
|
||||
@@ -529,7 +557,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
@@ -554,6 +582,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
volumeAtEntry: body.volumeRatio,
|
||||
pricePositionAtEntry: body.pricePosition,
|
||||
signalQualityScore: qualityResult.score,
|
||||
expectedSizeUSD: requestedPositionSizeUSD,
|
||||
actualSizeUSD: actualPositionSizeUSD,
|
||||
})
|
||||
|
||||
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,
|
||||
maxFavorablePrice: entryPrice,
|
||||
maxAdversePrice: entryPrice,
|
||||
atrAtEntry: undefined,
|
||||
runnerTrailingPercent: undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface TestTradeResponse {
|
||||
direction?: 'long' | 'short'
|
||||
entryPrice?: number
|
||||
positionSize?: number
|
||||
requestedPositionSize?: number
|
||||
fillCoveragePercent?: number
|
||||
stopLoss?: number
|
||||
takeProfit1?: number
|
||||
takeProfit2?: number
|
||||
@@ -94,19 +96,19 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
}
|
||||
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = positionSize * leverage
|
||||
const requestedPositionSizeUSD = positionSize * leverage
|
||||
|
||||
console.log(`💰 Opening ${direction} position:`)
|
||||
console.log(` Symbol: ${driftSymbol}`)
|
||||
console.log(` Base size: $${positionSize}`)
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
|
||||
|
||||
// Open position
|
||||
const openResult = await openPosition({
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
sizeUSD: positionSizeUSD,
|
||||
sizeUSD: requestedPositionSizeUSD,
|
||||
slippageTolerance: config.slippageTolerance,
|
||||
})
|
||||
|
||||
@@ -123,6 +125,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
|
||||
// Calculate stop loss and take profit prices
|
||||
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(
|
||||
entryPrice,
|
||||
@@ -183,13 +199,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
direction: direction,
|
||||
entryPrice,
|
||||
entryTime: Date.now(),
|
||||
positionSize: positionSizeUSD,
|
||||
positionSize: actualPositionSizeUSD,
|
||||
leverage: leverage,
|
||||
stopLossPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
emergencyStopPrice,
|
||||
currentSize: positionSizeUSD,
|
||||
currentSize: actualPositionSizeUSD,
|
||||
tp1Hit: false,
|
||||
tp2Hit: false,
|
||||
slMovedToBreakeven: false,
|
||||
@@ -204,6 +220,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
maxAdverseExcursion: 0,
|
||||
maxFavorablePrice: entryPrice,
|
||||
maxAdversePrice: entryPrice,
|
||||
atrAtEntry: undefined,
|
||||
runnerTrailingPercent: undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
@@ -222,7 +240,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
entryPrice: entryPrice,
|
||||
positionSize: positionSizeUSD,
|
||||
positionSize: actualPositionSizeUSD,
|
||||
requestedPositionSize: requestedPositionSizeUSD,
|
||||
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
||||
stopLoss: stopLossPrice,
|
||||
takeProfit1: tp1Price,
|
||||
takeProfit2: tp2Price,
|
||||
@@ -237,7 +257,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
try {
|
||||
const exitRes = await placeExitOrders({
|
||||
symbol: driftSymbol,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
entryPrice: entryPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
@@ -274,7 +294,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
leverage: leverage,
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
@@ -292,6 +312,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
hardStopPrice,
|
||||
signalStrength: 'test',
|
||||
timeframe: 'manual',
|
||||
expectedSizeUSD: requestedPositionSizeUSD,
|
||||
actualSizeUSD: actualPositionSizeUSD,
|
||||
})
|
||||
|
||||
console.log('💾 Trade saved to database')
|
||||
|
||||
Reference in New Issue
Block a user