From a100945864ea05e8c6415d4a6ae2eb3228c06df5 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 5 Nov 2025 15:28:12 +0100 Subject: [PATCH] Enhance trailing stop with ATR-based sizing --- app/api/settings/route.ts | 21 +++++++++++ app/api/trading/execute/route.ts | 60 ++++++++++++++++++++++++-------- app/api/trading/test-db/route.ts | 2 ++ app/api/trading/test/route.ts | 38 +++++++++++++++----- config/trading.ts | 31 +++++++++++++++-- lib/database/trades.ts | 2 ++ lib/drift/orders.ts | 19 ++++++---- lib/trading/position-manager.ts | 45 +++++++++++++++++++++--- 8 files changed, 183 insertions(+), 35 deletions(-) diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 7771a88..28f8260 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -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) { }) 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( diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index f49a47c..1dd53af 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -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 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 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 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 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 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 @@ -321,6 +339,15 @@ export function getConfigFromEnv(): Partial { trailingStopPercent: process.env.TRAILING_STOP_PERCENT ? parseFloat(process.env.TRAILING_STOP_PERCENT) : 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 ? parseFloat(process.env.TRAILING_STOP_ACTIVATION) : undefined, diff --git a/lib/database/trades.ts b/lib/database/trades.ts index d139c5b..3002660 100644 --- a/lib/database/trades.ts +++ b/lib/database/trades.ts @@ -75,6 +75,7 @@ export interface UpdateTradeStateParams { maxAdverseExcursion?: number maxFavorablePrice?: number maxAdversePrice?: number + runnerTrailingPercent?: number } export interface UpdateTradeExitParams { @@ -235,6 +236,7 @@ export async function updateTradeState(params: UpdateTradeStateParams) { maxAdverseExcursion: params.maxAdverseExcursion, maxFavorablePrice: params.maxFavorablePrice, maxAdversePrice: params.maxAdversePrice, + runnerTrailingPercent: params.runnerTrailingPercent, lastUpdate: new Date().toISOString(), } } diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 9965595..f679398 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -27,6 +27,7 @@ export interface OpenPositionResult { transactionSignature?: string fillPrice?: number fillSize?: number + fillNotionalUSD?: number slippage?: number error?: string isPhantom?: boolean // Position opened but size mismatch detected @@ -124,6 +125,7 @@ export async function openPosition( transactionSignature: mockTxSig, fillPrice: oraclePrice, fillSize: baseAssetSize, + fillNotionalUSD: baseAssetSize * oraclePrice, slippage: 0, } } @@ -179,19 +181,22 @@ export async function openPosition( if (position && position.side !== 'none') { const fillPrice = position.entryPrice + const filledBaseSize = Math.abs(position.size) + const fillNotionalUSD = filledBaseSize * fillPrice const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100 // CRITICAL: Validate actual position size vs expected // Phantom trade detection: Check if position is significantly smaller than expected - const actualSizeUSD = position.size * fillPrice const expectedSizeUSD = params.sizeUSD - const sizeRatio = actualSizeUSD / expectedSizeUSD + const sizeRatio = expectedSizeUSD > 0 ? fillNotionalUSD / expectedSizeUSD : 1 console.log(`💰 Fill details:`) 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(` 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)}%`) // Flag as phantom if actual size is less than 50% of expected @@ -200,7 +205,7 @@ export async function openPosition( if (isPhantom) { console.error(`🚨 PHANTOM POSITION DETECTED!`) 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`) } @@ -208,10 +213,11 @@ export async function openPosition( success: true, transactionSignature: txSig, fillPrice, - fillSize: position.size, // Use actual size from Drift, not calculated + fillSize: filledBaseSize, + fillNotionalUSD, slippage, isPhantom, - actualSizeUSD, + actualSizeUSD: fillNotionalUSD, } } else { // Position not found yet (may be DRY_RUN mode) @@ -223,6 +229,7 @@ export async function openPosition( transactionSignature: txSig, fillPrice: oraclePrice, fillSize: baseAssetSize, + fillNotionalUSD: baseAssetSize * oraclePrice, slippage: 0, } } diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index ee8e631..3a8e277 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -35,6 +35,7 @@ export interface ActiveTrade { slMovedToBreakeven: boolean slMovedToProfit: boolean trailingStopActive: boolean + runnerTrailingPercent?: number // Latest dynamic trailing percent applied // P&L tracking realizedPnL: number @@ -52,6 +53,7 @@ export interface ActiveTrade { originalAdx?: number // ADX at initial entry (for scaling validation) timesScaled?: number // How many times position has been scaled totalScaleAdded?: number // Total USD added through scaling + atrAtEntry?: number // ATR (absolute) when trade was opened // Monitoring priceCheckCount: number @@ -117,6 +119,7 @@ export class PositionManager { slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false, slMovedToProfit: pmState?.slMovedToProfit ?? false, trailingStopActive: pmState?.trailingStopActive ?? false, + runnerTrailingPercent: pmState?.runnerTrailingPercent, realizedPnL: pmState?.realizedPnL ?? 0, unrealizedPnL: pmState?.unrealizedPnL ?? 0, peakPnL: pmState?.peakPnL ?? 0, @@ -125,6 +128,7 @@ export class PositionManager { maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0, maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice, maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice, + atrAtEntry: dbTrade.atrAtEntry ?? undefined, priceCheckCount: 0, lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice, lastUpdateTime: Date.now(), @@ -341,7 +345,10 @@ export class PositionManager { trade.tp2Hit = true trade.currentSize = positionSizeUSD 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) @@ -687,8 +694,11 @@ export class PositionManager { if (percentToClose < 100) { trade.tp2Hit = true 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 await this.saveTradeState(trade) @@ -702,14 +712,17 @@ export class PositionManager { // Check if trailing stop should be activated if (!trade.trailingStopActive && profitPercent >= this.config.trailingStopActivation) { trade.trailingStopActive = true + trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade) console.log(`🎯 Trailing stop activated at +${profitPercent.toFixed(2)}%`) } // If trailing stop is active, adjust SL dynamically if (trade.trailingStopActive) { + const trailingPercent = this.getRunnerTrailingPercent(trade) + trade.runnerTrailingPercent = trailingPercent const trailingStopPrice = this.calculatePrice( trade.peakPrice, - -this.config.trailingStopPercent, // Trail below peak + -trailingPercent, // Trail below peak trade.direction ) @@ -722,7 +735,7 @@ export class PositionManager { const oldSL = trade.stopLossPrice 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) if (trade.priceCheckCount % 10 === 0) { @@ -899,6 +912,29 @@ export class PositionManager { 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 { if (trade.currentSize <= 0) { 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, peakPnL: trade.peakPnL, lastPrice: trade.lastPrice, + runnerTrailingPercent: trade.runnerTrailingPercent, }) } catch (error) { console.error('❌ Failed to save trade state:', error)