From 4ae9c38ad8f2201e3bd6fa8b294295cb0bc045a7 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Mon, 27 Oct 2025 12:11:10 +0100 Subject: [PATCH] Add trailing stop feature for runner position + fix settings persistence - Implemented trailing stop logic in Position Manager for remaining position after TP2 - Added new ActiveTrade fields: tp2Hit, trailingStopActive, peakPrice - New config settings: useTrailingStop, trailingStopPercent, trailingStopActivation - Added trailing stop UI section in settings page with explanations - Fixed env file parsing regex to support numbers in variable names (A-Z0-9_) - Settings now persist correctly across container restarts - Added back arrow navigation on settings page - Updated all API endpoints and test files with new fields - Trailing stop activates when runner reaches configured profit level - SL trails below peak price by configurable percentage --- .env | 16 +++--- app/api/settings/route.ts | 8 ++- app/api/trading/execute/route.ts | 3 ++ app/api/trading/test-db/route.ts | 3 ++ app/api/trading/test/route.ts | 3 ++ app/settings/page.tsx | 57 ++++++++++++++++++-- config/trading.ts | 34 ++++++++++-- lib/trading/position-manager.ts | 91 +++++++++++++++++++++++++++++--- 8 files changed, 193 insertions(+), 22 deletions(-) diff --git a/.env b/.env index 49dc603..f4bf08d 100644 --- a/.env +++ b/.env @@ -97,15 +97,15 @@ TAKE_PROFIT_1_PERCENT=0.7 # Take Profit 1 Size: What % of position to close at TP1 # Example: 50 = close 50% of position -TAKE_PROFIT_1_SIZE_PERCENT=50 +TAKE_PROFIT_1_SIZE_PERCENT=75 # Take Profit 2: Close remaining 50% at this profit level # Example: +1.5% on 10x = +15% account gain -TAKE_PROFIT_2_PERCENT=1.5 +TAKE_PROFIT_2_PERCENT=1.1 # Take Profit 2 Size: What % of remaining position to close at TP2 # Example: 100 = close all remaining position -TAKE_PROFIT_2_SIZE_PERCENT=50 +TAKE_PROFIT_2_SIZE_PERCENT=80 # Emergency Stop: Hard stop if this level is breached # Example: -2.0% on 10x = -20% account loss (rare but protects from flash crashes) @@ -113,13 +113,13 @@ EMERGENCY_STOP_PERCENT=-2 # Dynamic stop-loss adjustments # Move SL to breakeven when profit reaches this level -BREAKEVEN_TRIGGER_PERCENT=0.7 +BREAKEVEN_TRIGGER_PERCENT=0.3 # Lock in profit when price reaches this level -PROFIT_LOCK_TRIGGER_PERCENT=1.2 +PROFIT_LOCK_TRIGGER_PERCENT=1 # How much profit to lock (move SL to this profit level) -PROFIT_LOCK_PERCENT=0.2 +PROFIT_LOCK_PERCENT=0.6 # Risk limits # Stop trading if daily loss exceeds this amount (USD) @@ -348,3 +348,7 @@ NEW_RELIC_LICENSE_KEY= # - v4/QUICKREF_PHASE2.md - Quick reference # - TRADING_BOT_V4_MANUAL.md - Complete manual # - PHASE_2_COMPLETE_REPORT.md - Feature summary + +USE_TRAILING_STOP=true +TRAILING_STOP_PERCENT=0.3 +TRAILING_STOP_ACTIVATION=0.5 \ No newline at end of file diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index b12560e..8a7aaf1 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -19,7 +19,7 @@ function parseEnvFile(): Record { // Skip comments and empty lines if (line.trim().startsWith('#') || !line.trim()) return - const match = line.match(/^([A-Z_]+)=(.*)$/) + const match = line.match(/^([A-Z0-9_]+)=(.*)$/) if (match) { env[match[1]] = match[2] } @@ -73,6 +73,9 @@ export async function GET() { BREAKEVEN_TRIGGER_PERCENT: parseFloat(env.BREAKEVEN_TRIGGER_PERCENT || '0.4'), PROFIT_LOCK_TRIGGER_PERCENT: parseFloat(env.PROFIT_LOCK_TRIGGER_PERCENT || '1.0'), 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_ACTIVATION: parseFloat(env.TRAILING_STOP_ACTIVATION || '0.5'), MAX_DAILY_DRAWDOWN: parseFloat(env.MAX_DAILY_DRAWDOWN || '-50'), MAX_TRADES_PER_HOUR: parseInt(env.MAX_TRADES_PER_HOUR || '6'), MIN_TIME_BETWEEN_TRADES: parseInt(env.MIN_TIME_BETWEEN_TRADES || '600'), @@ -106,6 +109,9 @@ export async function POST(request: NextRequest) { BREAKEVEN_TRIGGER_PERCENT: settings.BREAKEVEN_TRIGGER_PERCENT.toString(), PROFIT_LOCK_TRIGGER_PERCENT: settings.PROFIT_LOCK_TRIGGER_PERCENT.toString(), 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_ACTIVATION: settings.TRAILING_STOP_ACTIVATION.toString(), MAX_DAILY_DRAWDOWN: settings.MAX_DAILY_DRAWDOWN.toString(), MAX_TRADES_PER_HOUR: settings.MAX_TRADES_PER_HOUR.toString(), MIN_TIME_BETWEEN_TRADES: settings.MIN_TIME_BETWEEN_TRADES.toString(), diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index ac82853..d707c32 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -198,11 +198,14 @@ export async function POST(request: NextRequest): Promise {/* Header */}
-

⚙️ Trading Bot Settings

+
+ + + + + +
+

⚙️ Trading Bot Settings

+
+

Configure your automated trading parameters

@@ -293,7 +305,7 @@ export default function SettingsPage() { min={0} max={5} step={0.1} - description="Move SL to breakeven (entry price) when profit reaches this level." + description="After TP1 closes, move SL to this profit level. Should be between 0% (breakeven) and TP1%. Example: 0.4% = locks in +4% account profit on remaining position." /> + + + {/* Trailing Stop */} +
+
+

+ After TP2 closes, the remaining position (your "runner") can use a trailing stop loss that follows price. + This lets you capture big moves while protecting profit. +

+
+ updateSetting('USE_TRAILING_STOP', v === 1)} + min={0} + max={1} + step={1} + description="Enable trailing stop for runner position after TP2. 0 = disabled, 1 = enabled." + /> + updateSetting('TRAILING_STOP_PERCENT', v)} + min={0.1} + max={2} + step={0.1} + description="How far below peak price (for longs) to trail the stop loss. Example: 0.3% = SL trails 0.3% below highest price reached." + /> + updateSetting('TRAILING_STOP_ACTIVATION', v)} + min={0.1} + max={5} + step={0.1} + description="Runner must reach this profit % before trailing stop activates. Prevents premature stops. Example: 0.5% = wait until runner is +0.5% profit." />
diff --git a/config/trading.ts b/config/trading.ts index e390209..2ed9e1f 100644 --- a/config/trading.ts +++ b/config/trading.ts @@ -26,6 +26,11 @@ export interface TradingConfig { profitLockTriggerPercent: number // When to lock in profit profitLockPercent: number // How much profit to lock + // Trailing stop for runner (after TP2) + useTrailingStop: boolean // Enable trailing stop for remaining position + trailingStopPercent: number // Trail by this % below peak + trailingStopActivation: number // Activate when runner profits exceed this % + // DEX specific priceCheckIntervalMs: number // How often to check prices slippageTolerance: number // Max acceptable slippage (%) @@ -70,10 +75,15 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = { hardStopPercent: -2.5, // Hard stop (TRIGGER_MARKET) // Dynamic adjustments - breakEvenTriggerPercent: 0.4, // Move SL to breakeven at +0.4% + breakEvenTriggerPercent: 0.4, // Move SL to this profit level after TP1 hits profitLockTriggerPercent: 1.0, // Lock profit at +1.0% profitLockPercent: 0.4, // Lock +0.4% profit + // Trailing stop for runner (after TP2) + useTrailingStop: true, // Enable trailing stop for remaining position after TP2 + trailingStopPercent: 0.3, // Trail by 0.3% below peak price + trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit + // DEX settings priceCheckIntervalMs: 2000, // Check every 2 seconds slippageTolerance: 1.0, // 1% max slippage on market orders @@ -86,8 +96,8 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = { // Execution useMarketOrders: true, // Use market orders for reliable fills confirmationTimeout: 30000, // 30 seconds max wait - takeProfit1SizePercent: 75, - takeProfit2SizePercent: 100, + takeProfit1SizePercent: 75, // Close 75% at TP1 to lock in profit + takeProfit2SizePercent: 100, // Close remaining 25% at TP2 } // Supported markets on Drift Protocol @@ -200,6 +210,24 @@ export function getConfigFromEnv(): Partial { takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT ? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT) : undefined, + breakEvenTriggerPercent: process.env.BREAKEVEN_TRIGGER_PERCENT + ? parseFloat(process.env.BREAKEVEN_TRIGGER_PERCENT) + : undefined, + profitLockTriggerPercent: process.env.PROFIT_LOCK_TRIGGER_PERCENT + ? parseFloat(process.env.PROFIT_LOCK_TRIGGER_PERCENT) + : undefined, + profitLockPercent: process.env.PROFIT_LOCK_PERCENT + ? parseFloat(process.env.PROFIT_LOCK_PERCENT) + : undefined, + useTrailingStop: process.env.USE_TRAILING_STOP + ? process.env.USE_TRAILING_STOP === 'true' + : undefined, + trailingStopPercent: process.env.TRAILING_STOP_PERCENT + ? parseFloat(process.env.TRAILING_STOP_PERCENT) + : undefined, + trailingStopActivation: process.env.TRAILING_STOP_ACTIVATION + ? parseFloat(process.env.TRAILING_STOP_ACTIVATION) + : undefined, maxDailyDrawdown: process.env.MAX_DAILY_DRAWDOWN ? parseFloat(process.env.MAX_DAILY_DRAWDOWN) : undefined, diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index bbeb43b..8e4a4b7 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -31,13 +31,16 @@ export interface ActiveTrade { // State currentSize: number // Changes after TP1 tp1Hit: boolean + tp2Hit: boolean slMovedToBreakeven: boolean slMovedToProfit: boolean + trailingStopActive: boolean // P&L tracking realizedPnL: number unrealizedPnL: number peakPnL: number + peakPrice: number // Track highest price reached (for trailing) // Monitoring priceCheckCount: number @@ -99,11 +102,14 @@ export class PositionManager { emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02), currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD, tp1Hit: pmState?.tp1Hit ?? false, + tp2Hit: pmState?.tp2Hit ?? false, slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false, slMovedToProfit: pmState?.slMovedToProfit ?? false, + trailingStopActive: pmState?.trailingStopActive ?? false, realizedPnL: pmState?.realizedPnL ?? 0, unrealizedPnL: pmState?.unrealizedPnL ?? 0, peakPnL: pmState?.peakPnL ?? 0, + peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice, priceCheckCount: 0, lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice, lastUpdateTime: Date.now(), @@ -271,6 +277,17 @@ export class PositionManager { if (trade.unrealizedPnL > trade.peakPnL) { trade.peakPnL = trade.unrealizedPnL } + + // Track peak price for trailing stop + if (trade.direction === 'long') { + if (currentPrice > trade.peakPrice) { + trade.peakPrice = currentPrice + } + } else { + if (currentPrice < trade.peakPrice || trade.peakPrice === 0) { + trade.peakPrice = currentPrice + } + } // Log status every 10 checks (~20 seconds) if (trade.priceCheckCount % 10 === 0) { @@ -299,22 +316,22 @@ export class PositionManager { return } - // 3. Take profit 1 (50%) + // 3. Take profit 1 (closes configured %) if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) { console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) - await this.executeExit(trade, 50, 'TP1', currentPrice) + await this.executeExit(trade, this.config.takeProfit1SizePercent, 'TP1', currentPrice) - // Move SL to secure profit after TP1 + // Move SL based on breakEvenTriggerPercent setting trade.tp1Hit = true - trade.currentSize = trade.positionSize * 0.5 + trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100) trade.stopLossPrice = this.calculatePrice( trade.entryPrice, - 0.35, // +0.35% to secure profit and avoid stop-out on retracement + this.config.breakEvenTriggerPercent, // Use configured breakeven level trade.direction ) trade.slMovedToBreakeven = true - console.log(`🔒 SL moved to +0.35% (half of TP1): ${trade.stopLossPrice.toFixed(4)}`) + console.log(`🔒 SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${trade.stopLossPrice.toFixed(4)}`) // Save state after TP1 await this.saveTradeState(trade) @@ -342,12 +359,70 @@ export class PositionManager { await this.saveTradeState(trade) } - // 5. Take profit 2 (remaining 50%) + // 5. Take profit 2 (remaining position) if (trade.tp1Hit && this.shouldTakeProfit2(currentPrice, trade)) { console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) - await this.executeExit(trade, 100, 'TP2', currentPrice) + + // Calculate how much to close based on TP2 size percent + const percentToClose = this.config.takeProfit2SizePercent + + await this.executeExit(trade, percentToClose, 'TP2', currentPrice) + + // If some position remains, mark TP2 as hit and activate trailing stop + if (percentToClose < 100) { + trade.tp2Hit = true + trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100) + + console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`) + + // Save state after TP2 + await this.saveTradeState(trade) + } + return } + + // 6. Trailing stop for runner (after TP2) + if (trade.tp2Hit && this.config.useTrailingStop) { + // Check if trailing stop should be activated + if (!trade.trailingStopActive && profitPercent >= this.config.trailingStopActivation) { + trade.trailingStopActive = true + console.log(`🎯 Trailing stop activated at +${profitPercent.toFixed(2)}%`) + } + + // If trailing stop is active, adjust SL dynamically + if (trade.trailingStopActive) { + const trailingStopPrice = this.calculatePrice( + trade.peakPrice, + -this.config.trailingStopPercent, // Trail below peak + trade.direction + ) + + // Only move SL up (for long) or down (for short), never backwards + const shouldUpdate = trade.direction === 'long' + ? trailingStopPrice > trade.stopLossPrice + : trailingStopPrice < trade.stopLossPrice + + if (shouldUpdate) { + 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)})`) + + // Save state after trailing SL update (every 10 updates to avoid spam) + if (trade.priceCheckCount % 10 === 0) { + await this.saveTradeState(trade) + } + } + + // Check if trailing stop hit + if (this.shouldStopLoss(currentPrice, trade)) { + console.log(`🔴 TRAILING STOP HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) + await this.executeExit(trade, 100, 'SL', currentPrice) + return + } + } + } } /**