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
This commit is contained in:
mindesbunister
2025-10-27 12:11:10 +01:00
parent d3c04ea9c9
commit 4ae9c38ad8
8 changed files with 193 additions and 22 deletions

16
.env
View File

@@ -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

View File

@@ -19,7 +19,7 @@ function parseEnvFile(): Record<string, string> {
// 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(),

View File

@@ -198,11 +198,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
emergencyStopPrice,
currentSize: positionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
realizedPnL: 0,
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),

View File

@@ -170,11 +170,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
emergencyStopPrice,
currentSize: positionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
realizedPnL: 0,
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),

View File

@@ -169,11 +169,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
emergencyStopPrice,
currentSize: positionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
realizedPnL: 0,
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),

View File

@@ -20,6 +20,9 @@ interface TradingSettings {
BREAKEVEN_TRIGGER_PERCENT: number
PROFIT_LOCK_TRIGGER_PERCENT: number
PROFIT_LOCK_PERCENT: number
USE_TRAILING_STOP: boolean
TRAILING_STOP_PERCENT: number
TRAILING_STOP_ACTIVATION: number
MAX_DAILY_DRAWDOWN: number
MAX_TRADES_PER_HOUR: number
MIN_TIME_BETWEEN_TRADES: number
@@ -161,7 +164,16 @@ export default function SettingsPage() {
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-2"> Trading Bot Settings</h1>
<div className="flex items-center space-x-4 mb-4">
<a href="/" className="text-gray-400 hover:text-white transition">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</a>
<div>
<h1 className="text-4xl font-bold text-white"> Trading Bot Settings</h1>
</div>
</div>
<p className="text-slate-400">Configure your automated trading parameters</p>
</div>
@@ -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."
/>
<Setting
label="Profit Lock Trigger (%)"
@@ -302,7 +314,7 @@ export default function SettingsPage() {
min={0}
max={10}
step={0.1}
description="When profit reaches this level, lock in profit by moving SL."
description="After TP1, if price continues to this level, move SL higher to lock more profit. Must be > TP1 and < TP2."
/>
<Setting
label="Profit Lock Amount (%)"
@@ -311,7 +323,44 @@ export default function SettingsPage() {
min={0}
max={5}
step={0.1}
description="Move SL to this profit level when lock trigger is hit."
description="When Profit Lock Trigger hits, move SL to this profit level. Should be > Breakeven Trigger."
/>
</Section>
{/* Trailing Stop */}
<Section title="🏃 Trailing Stop (Runner)" description="Let a small portion run with dynamic stop loss">
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<p className="text-sm text-blue-400">
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.
</p>
</div>
<Setting
label="Use Trailing Stop"
value={settings.USE_TRAILING_STOP ? 1 : 0}
onChange={(v) => 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."
/>
<Setting
label="Trailing Stop Distance (%)"
value={settings.TRAILING_STOP_PERCENT}
onChange={(v) => 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."
/>
<Setting
label="Trailing Stop Activation (%)"
value={settings.TRAILING_STOP_ACTIVATION}
onChange={(v) => 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."
/>
</Section>

View File

@@ -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<TradingConfig> {
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,

View File

@@ -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
}
}
}
}
/**