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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user