Enhance trailing stop with ATR-based sizing
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user