feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics - Create /api/trading/market-data webhook endpoint for continuous data updates - Add /api/analytics/reentry-check endpoint for validating manual trades - Update execute endpoint to auto-cache metrics from incoming signals - Enhance Telegram bot with pre-execution analytics validation - Support --force flag to override analytics blocks - Use fresh ADX/ATR/RSI data when available, fallback to historical - Apply performance modifiers: -20 for losing streaks, +10 for winning - Minimum re-entry score 55 (vs 60 for new signals) - Fail-open design: proceeds if analytics unavailable - Show data freshness and source in Telegram responses - Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md Phase 1 implementation for smart manual trade validation.
This commit is contained in:
@@ -35,7 +35,6 @@ export interface ActiveTrade {
|
||||
slMovedToBreakeven: boolean
|
||||
slMovedToProfit: boolean
|
||||
trailingStopActive: boolean
|
||||
runnerTrailingPercent?: number // Latest dynamic trailing percent applied
|
||||
|
||||
// P&L tracking
|
||||
realizedPnL: number
|
||||
@@ -53,7 +52,6 @@ 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
|
||||
@@ -119,7 +117,6 @@ 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,
|
||||
@@ -128,7 +125,6 @@ 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(),
|
||||
@@ -136,12 +132,6 @@ export class PositionManager {
|
||||
|
||||
this.activeTrades.set(activeTrade.id, activeTrade)
|
||||
console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`)
|
||||
|
||||
// Consistency check: if TP1 hit but SL not moved to breakeven, fix it now
|
||||
if (activeTrade.tp1Hit && !activeTrade.slMovedToBreakeven) {
|
||||
console.log(`🔧 Detected inconsistent state: TP1 hit but SL not at breakeven. Fixing now...`)
|
||||
await this.handlePostTp1Adjustments(activeTrade, 'recovery after restore')
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeTrades.size > 0) {
|
||||
@@ -213,22 +203,6 @@ export class PositionManager {
|
||||
return Array.from(this.activeTrades.values())
|
||||
}
|
||||
|
||||
async reconcileTrade(symbol: string): Promise<void> {
|
||||
const trade = Array.from(this.activeTrades.values()).find(t => t.symbol === symbol)
|
||||
if (!trade) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const driftService = getDriftService()
|
||||
const marketConfig = getMarketConfig(symbol)
|
||||
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
|
||||
await this.checkTradeConditions(trade, oraclePrice)
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Failed to reconcile trade for ${symbol}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific trade
|
||||
*/
|
||||
@@ -359,7 +333,12 @@ export class PositionManager {
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = positionSizeUSD
|
||||
|
||||
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection')
|
||||
// Move SL to breakeven after TP1
|
||||
trade.stopLossPrice = trade.entryPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
console.log(`🛡️ Stop loss moved to breakeven: $${trade.stopLossPrice.toFixed(4)}`)
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
|
||||
} else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) {
|
||||
// TP2 fired (total should be ~95% closed, 5% runner left)
|
||||
@@ -367,22 +346,19 @@ export class PositionManager {
|
||||
trade.tp2Hit = true
|
||||
trade.currentSize = positionSizeUSD
|
||||
trade.trailingStopActive = true
|
||||
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
console.log(
|
||||
`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||
)
|
||||
console.log(`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with ${this.config.trailingStopPercent}% trailing stop`)
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
|
||||
// CRITICAL: Don't return early! Continue monitoring the runner position
|
||||
// The trailing stop logic at line 732 needs to run
|
||||
|
||||
} else {
|
||||
// Partial fill detected but unclear which TP - just update size
|
||||
console.log(`⚠️ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`)
|
||||
trade.currentSize = positionSizeUSD
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// Continue monitoring the remaining position
|
||||
return
|
||||
}
|
||||
|
||||
// CRITICAL: Check for entry price mismatch (NEW position opened)
|
||||
@@ -404,10 +380,10 @@ export class PositionManager {
|
||||
trade.lastPrice,
|
||||
trade.direction
|
||||
)
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
const estimatedPnL = (trade.currentSize * accountPnL) / 100
|
||||
const accountPnLPercent = profitPercent * trade.leverage
|
||||
const estimatedPnL = (trade.currentSize * profitPercent) / 100
|
||||
|
||||
console.log(`💰 Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price → ${accountPnL.toFixed(2)}% account → $${estimatedPnL.toFixed(2)} realized`)
|
||||
console.log(`💰 Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price → ${accountPnLPercent.toFixed(2)}% account → $${estimatedPnL.toFixed(2)} realized`)
|
||||
|
||||
try {
|
||||
await updateTradeExit({
|
||||
@@ -466,41 +442,22 @@ export class PositionManager {
|
||||
// CRITICAL: Use trade state flags, not current price (on-chain orders filled in the past!)
|
||||
let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL'
|
||||
|
||||
// Calculate P&L first (set to 0 for phantom trades)
|
||||
let realizedPnL = 0
|
||||
let exitPrice = currentPrice
|
||||
|
||||
// Include any previously realized profit (e.g., from TP1 partial close)
|
||||
const previouslyRealized = trade.realizedPnL
|
||||
let runnerRealized = 0
|
||||
let runnerProfitPercent = 0
|
||||
if (!wasPhantom) {
|
||||
// For external closures, try to estimate a more realistic exit price
|
||||
// Manual closures may happen at significantly different prices than current market
|
||||
const unrealizedPnL = trade.unrealizedPnL || 0
|
||||
const positionSizeUSD = trade.positionSize
|
||||
|
||||
if (Math.abs(unrealizedPnL) > 1 && positionSizeUSD > 0) {
|
||||
// If we have meaningful unrealized P&L, back-calculate the likely exit price
|
||||
// This is more accurate than using volatile current market price
|
||||
const impliedProfitPercent = (unrealizedPnL / positionSizeUSD) * 100 / trade.leverage
|
||||
exitPrice = trade.direction === 'long'
|
||||
? trade.entryPrice * (1 + impliedProfitPercent / 100)
|
||||
: trade.entryPrice * (1 - impliedProfitPercent / 100)
|
||||
|
||||
console.log(`📊 Estimated exit price based on unrealized P&L:`)
|
||||
console.log(` Unrealized P&L: $${unrealizedPnL.toFixed(2)}`)
|
||||
console.log(` Market price: $${currentPrice.toFixed(6)}`)
|
||||
console.log(` Estimated exit: $${exitPrice.toFixed(6)}`)
|
||||
|
||||
realizedPnL = unrealizedPnL
|
||||
} else {
|
||||
// Fallback to current price calculation
|
||||
const profitPercent = this.calculateProfitPercent(
|
||||
trade.entryPrice,
|
||||
currentPrice,
|
||||
trade.direction
|
||||
)
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
realizedPnL = (sizeForPnL * accountPnL) / 100
|
||||
}
|
||||
runnerProfitPercent = this.calculateProfitPercent(
|
||||
trade.entryPrice,
|
||||
currentPrice,
|
||||
trade.direction
|
||||
)
|
||||
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100
|
||||
}
|
||||
|
||||
const totalRealizedPnL = previouslyRealized + runnerRealized
|
||||
trade.realizedPnL = totalRealizedPnL
|
||||
console.log(` Realized P&L snapshot → Previous: $${previouslyRealized.toFixed(2)} | Runner: $${runnerRealized.toFixed(2)} (Δ${runnerProfitPercent.toFixed(2)}%) | Total: $${totalRealizedPnL.toFixed(2)}`)
|
||||
|
||||
// Determine exit reason from trade state and P&L
|
||||
if (trade.tp2Hit) {
|
||||
@@ -509,14 +466,14 @@ export class PositionManager {
|
||||
} else if (trade.tp1Hit) {
|
||||
// TP1 was hit, position should be 25% size, but now fully closed
|
||||
// This means either TP2 filled or runner got stopped out
|
||||
exitReason = realizedPnL > 0 ? 'TP2' : 'SL'
|
||||
exitReason = totalRealizedPnL > 0 ? 'TP2' : 'SL'
|
||||
} else {
|
||||
// No TPs hit yet - either SL or TP1 filled just now
|
||||
// Use P&L to determine: positive = TP, negative = SL
|
||||
if (realizedPnL > trade.positionSize * 0.005) {
|
||||
if (totalRealizedPnL > trade.positionSize * 0.005) {
|
||||
// More than 0.5% profit - must be TP1
|
||||
exitReason = 'TP1'
|
||||
} else if (realizedPnL < 0) {
|
||||
} else if (totalRealizedPnL < 0) {
|
||||
// Loss - must be SL
|
||||
exitReason = 'SL'
|
||||
}
|
||||
@@ -528,9 +485,9 @@ export class PositionManager {
|
||||
try {
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: exitPrice, // Use estimated exit price, not current market price
|
||||
exitPrice: currentPrice,
|
||||
exitReason,
|
||||
realizedPnL,
|
||||
realizedPnL: totalRealizedPnL,
|
||||
exitOrderTx: 'ON_CHAIN_ORDER',
|
||||
holdTimeSeconds,
|
||||
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
|
||||
@@ -540,7 +497,7 @@ export class PositionManager {
|
||||
maxFavorablePrice: trade.maxFavorablePrice,
|
||||
maxAdversePrice: trade.maxAdversePrice,
|
||||
})
|
||||
console.log(`💾 External closure recorded: ${exitReason} at $${exitPrice.toFixed(6)} | P&L: $${realizedPnL.toFixed(2)}`)
|
||||
console.log(`💾 External closure recorded: ${exitReason} at $${currentPrice} | P&L: $${totalRealizedPnL.toFixed(2)}`)
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save external closure:', dbError)
|
||||
}
|
||||
@@ -551,31 +508,15 @@ export class PositionManager {
|
||||
}
|
||||
|
||||
// Position exists but size mismatch (partial close by TP1?)
|
||||
const onChainBaseSize = Math.abs(position.size)
|
||||
const onChainSizeUSD = onChainBaseSize * currentPrice
|
||||
const trackedSizeUSD = trade.currentSize
|
||||
|
||||
if (trackedSizeUSD > 0 && onChainSizeUSD < trackedSizeUSD * 0.95) { // 5% tolerance
|
||||
const expectedBaseSize = trackedSizeUSD / currentPrice
|
||||
console.log(`⚠️ Position size mismatch: tracking $${trackedSizeUSD.toFixed(2)} (~${expectedBaseSize.toFixed(4)} units) but on-chain shows $${onChainSizeUSD.toFixed(2)} (${onChainBaseSize.toFixed(4)} units)`)
|
||||
if (position.size < trade.currentSize * 0.95) { // 5% tolerance
|
||||
console.log(`⚠️ Position size mismatch: expected ${trade.currentSize}, got ${position.size}`)
|
||||
|
||||
// CRITICAL: If mismatch is extreme (>50%), this is a phantom trade
|
||||
const sizeRatio = trackedSizeUSD > 0 ? onChainSizeUSD / trackedSizeUSD : 0
|
||||
const sizeRatio = (position.size * currentPrice) / trade.currentSize
|
||||
if (sizeRatio < 0.5) {
|
||||
const tradeAgeSeconds = (Date.now() - trade.entryTime) / 1000
|
||||
const probablyPartialRunner = trade.tp1Hit || tradeAgeSeconds > 60
|
||||
|
||||
if (probablyPartialRunner) {
|
||||
console.log(`🛠️ Detected stray remainder (${(sizeRatio * 100).toFixed(1)}%) after on-chain exit - forcing market close`)
|
||||
trade.currentSize = onChainSizeUSD
|
||||
await this.saveTradeState(trade)
|
||||
await this.executeExit(trade, 100, 'manual', currentPrice)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🚨 EXTREME SIZE MISMATCH (${(sizeRatio * 100).toFixed(1)}%) - Closing phantom trade`)
|
||||
console.log(` Expected: $${trackedSizeUSD.toFixed(2)}`)
|
||||
console.log(` Actual: $${onChainSizeUSD.toFixed(2)}`)
|
||||
console.log(` Expected: $${trade.currentSize.toFixed(2)}`)
|
||||
console.log(` Actual: $${(position.size * currentPrice).toFixed(2)}`)
|
||||
|
||||
// Close as phantom trade
|
||||
try {
|
||||
@@ -603,15 +544,10 @@ export class PositionManager {
|
||||
return
|
||||
}
|
||||
|
||||
// Update current size to match reality and run TP1 adjustments if needed
|
||||
trade.currentSize = onChainSizeUSD
|
||||
if (!trade.tp1Hit) {
|
||||
trade.tp1Hit = true
|
||||
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 size sync')
|
||||
} else {
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
return
|
||||
// Update current size to match reality (convert base asset size to USD using current price)
|
||||
trade.currentSize = position.size * currentPrice
|
||||
trade.tp1Hit = true
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -636,8 +572,8 @@ export class PositionManager {
|
||||
trade.direction
|
||||
)
|
||||
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
trade.unrealizedPnL = (trade.currentSize * accountPnL) / 100
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100
|
||||
|
||||
// Track peak P&L (MFE - Maximum Favorable Excursion)
|
||||
if (trade.unrealizedPnL > trade.peakPnL) {
|
||||
@@ -702,7 +638,56 @@ export class PositionManager {
|
||||
// Move SL based on breakEvenTriggerPercent setting
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100)
|
||||
await this.handlePostTp1Adjustments(trade, 'software TP1 execution')
|
||||
const newStopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
this.config.breakEvenTriggerPercent, // Use configured breakeven level
|
||||
trade.direction
|
||||
)
|
||||
trade.stopLossPrice = newStopLossPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`)
|
||||
|
||||
// CRITICAL: Cancel old on-chain SL orders and place new ones at updated price
|
||||
try {
|
||||
console.log('🗑️ Cancelling old stop loss orders...')
|
||||
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
|
||||
const cancelResult = await cancelAllOrders(trade.symbol)
|
||||
if (cancelResult.success) {
|
||||
console.log(`✅ Cancelled ${cancelResult.cancelledCount || 0} old orders`)
|
||||
|
||||
// Place new SL orders at breakeven/profit level for remaining position
|
||||
console.log(`🛡️ Placing new SL orders at $${newStopLossPrice.toFixed(4)} for remaining position...`)
|
||||
const exitOrdersResult = await placeExitOrders({
|
||||
symbol: trade.symbol,
|
||||
positionSizeUSD: trade.currentSize,
|
||||
entryPrice: trade.entryPrice,
|
||||
tp1Price: trade.tp2Price, // Only TP2 remains
|
||||
tp2Price: trade.tp2Price, // Dummy, won't be used
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1SizePercent: 100, // Close remaining 25% at TP2
|
||||
tp2SizePercent: 0,
|
||||
direction: trade.direction,
|
||||
useDualStops: this.config.useDualStops,
|
||||
softStopPrice: trade.direction === 'long'
|
||||
? newStopLossPrice * 1.005 // 0.5% above for long
|
||||
: newStopLossPrice * 0.995, // 0.5% below for short
|
||||
hardStopPrice: newStopLossPrice,
|
||||
})
|
||||
|
||||
if (exitOrdersResult.success) {
|
||||
console.log('✅ New SL orders placed on-chain at updated price')
|
||||
} else {
|
||||
console.error('❌ Failed to place new SL orders:', exitOrdersResult.error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update on-chain SL orders:', error)
|
||||
// Don't fail the TP1 exit if SL update fails - software monitoring will handle it
|
||||
}
|
||||
|
||||
// Save state after TP1
|
||||
await this.saveTradeState(trade)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -727,39 +712,42 @@ export class PositionManager {
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// 5. TP2 Hit - Activate runner (no close, just start trailing)
|
||||
// 5. Take profit 2 (remaining position)
|
||||
if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) {
|
||||
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}% - Activating 25% runner!`)
|
||||
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
|
||||
// Mark TP2 as hit and activate trailing stop on full remaining 25%
|
||||
trade.tp2Hit = true
|
||||
trade.peakPrice = currentPrice
|
||||
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
// Calculate how much to close based on TP2 size percent
|
||||
const percentToClose = this.config.takeProfit2SizePercent
|
||||
|
||||
console.log(
|
||||
`🏃 Runner activated on full remaining position: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% | trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||
)
|
||||
await this.executeExit(trade, percentToClose, 'TP2', currentPrice)
|
||||
|
||||
// Save state after TP2 activation
|
||||
await this.saveTradeState(trade)
|
||||
// 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 activation)
|
||||
}
|
||||
|
||||
// 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
|
||||
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,
|
||||
-trailingPercent, // Trail below peak
|
||||
-this.config.trailingStopPercent, // Trail below peak
|
||||
trade.direction
|
||||
)
|
||||
|
||||
@@ -772,7 +760,7 @@ export class PositionManager {
|
||||
const oldSL = trade.stopLossPrice
|
||||
trade.stopLossPrice = trailingStopPrice
|
||||
|
||||
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)} → ${trailingStopPrice.toFixed(4)} (${trailingPercent.toFixed(3)}% below peak $${trade.peakPrice.toFixed(4)})`)
|
||||
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) {
|
||||
@@ -813,35 +801,18 @@ export class PositionManager {
|
||||
return
|
||||
}
|
||||
|
||||
const closePriceForCalc = result.closePrice || currentPrice
|
||||
const closedSizeBase = result.closedSize || 0
|
||||
const closedUSD = closedSizeBase * closePriceForCalc
|
||||
const wasForcedFullClose = !!result.fullyClosed && percentToClose < 100
|
||||
const treatAsFullClose = percentToClose >= 100 || result.fullyClosed
|
||||
|
||||
// Calculate actual P&L based on entry vs exit price
|
||||
const profitPercent = this.calculateProfitPercent(trade.entryPrice, closePriceForCalc, trade.direction)
|
||||
const actualRealizedPnL = (closedUSD * profitPercent) / 100
|
||||
|
||||
// Update trade state
|
||||
if (treatAsFullClose) {
|
||||
trade.realizedPnL += actualRealizedPnL
|
||||
trade.currentSize = 0
|
||||
trade.trailingStopActive = false
|
||||
|
||||
if (reason === 'TP2') {
|
||||
trade.tp2Hit = true
|
||||
}
|
||||
if (reason === 'TP1') {
|
||||
trade.tp1Hit = true
|
||||
}
|
||||
|
||||
if (percentToClose >= 100) {
|
||||
// Full close - remove from monitoring
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
|
||||
// Save to database (only for valid exit reasons)
|
||||
if (reason !== 'error') {
|
||||
try {
|
||||
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: closePriceForCalc,
|
||||
exitPrice: result.closePrice || currentPrice,
|
||||
exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency',
|
||||
realizedPnL: trade.realizedPnL,
|
||||
exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE',
|
||||
@@ -856,23 +827,25 @@ export class PositionManager {
|
||||
console.log('💾 Trade saved to database')
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save trade exit to database:', dbError)
|
||||
// Don't fail the close if database fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.removeTrade(trade.id)
|
||||
const closeLabel = wasForcedFullClose
|
||||
? '✅ Forced full close (below Drift minimum)'
|
||||
: '✅ Position closed'
|
||||
console.log(`${closeLabel} | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
} else {
|
||||
// Partial close (TP1) - calculate P&L for partial amount
|
||||
const partialRealizedPnL = (closedUSD * profitPercent) / 100
|
||||
trade.realizedPnL += partialRealizedPnL
|
||||
// Partial close (TP1)
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
// result.closedSize is returned in base asset units (e.g., SOL), convert to USD using closePrice
|
||||
const closePriceForCalc = result.closePrice || currentPrice
|
||||
const closedSizeBase = result.closedSize || 0
|
||||
const closedUSD = closedSizeBase * closePriceForCalc
|
||||
trade.currentSize = Math.max(0, trade.currentSize - closedUSD)
|
||||
|
||||
console.log(
|
||||
`✅ Partial close executed | Realized: $${partialRealizedPnL.toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`
|
||||
)
|
||||
console.log(`✅ Partial close executed | Realized: $${(result.realizedPnL || 0).toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`)
|
||||
|
||||
// Persist updated trade state so analytics reflect partial profits immediately
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// TODO: Send notification
|
||||
@@ -962,131 +935,6 @@ export class PositionManager {
|
||||
console.log('✅ All positions closed')
|
||||
}
|
||||
|
||||
refreshConfig(): void {
|
||||
this.config = getMergedConfig()
|
||||
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)}`)
|
||||
await this.saveTradeState(trade)
|
||||
return
|
||||
}
|
||||
|
||||
const newStopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
this.config.breakEvenTriggerPercent,
|
||||
trade.direction
|
||||
)
|
||||
|
||||
trade.stopLossPrice = newStopLossPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 (${context}) SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`)
|
||||
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: trade.tp2Price,
|
||||
tp1SizePercent: 100,
|
||||
tp2Price: trade.tp2Price,
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
private async refreshExitOrders(
|
||||
trade: ActiveTrade,
|
||||
options: {
|
||||
stopLossPrice: number
|
||||
tp1Price: number
|
||||
tp1SizePercent: number
|
||||
tp2Price?: number
|
||||
tp2SizePercent?: number
|
||||
context: string
|
||||
}
|
||||
): Promise<void> {
|
||||
if (trade.currentSize <= 0) {
|
||||
console.log(`⚠️ Skipping exit order refresh for ${trade.symbol} (${options.context}) because tracked size is zero`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🗑️ (${options.context}) Cancelling existing exit orders before refresh...`)
|
||||
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
|
||||
const cancelResult = await cancelAllOrders(trade.symbol)
|
||||
if (cancelResult.success) {
|
||||
console.log(`✅ (${options.context}) Cancelled ${cancelResult.cancelledCount || 0} old orders`)
|
||||
} else {
|
||||
console.warn(`⚠️ (${options.context}) Failed to cancel old orders: ${cancelResult.error}`)
|
||||
}
|
||||
|
||||
const tp2Price = options.tp2Price ?? options.tp1Price
|
||||
const tp2SizePercent = options.tp2SizePercent ?? 0
|
||||
|
||||
const refreshParams: any = {
|
||||
symbol: trade.symbol,
|
||||
positionSizeUSD: trade.currentSize,
|
||||
entryPrice: trade.entryPrice,
|
||||
tp1Price: options.tp1Price,
|
||||
tp2Price,
|
||||
stopLossPrice: options.stopLossPrice,
|
||||
tp1SizePercent: options.tp1SizePercent,
|
||||
tp2SizePercent,
|
||||
direction: trade.direction,
|
||||
useDualStops: this.config.useDualStops,
|
||||
}
|
||||
|
||||
if (this.config.useDualStops) {
|
||||
const softStopBuffer = this.config.softStopBuffer ?? 0.4
|
||||
const softStopPrice = trade.direction === 'long'
|
||||
? options.stopLossPrice * (1 + softStopBuffer / 100)
|
||||
: options.stopLossPrice * (1 - softStopBuffer / 100)
|
||||
|
||||
refreshParams.softStopPrice = softStopPrice
|
||||
refreshParams.softStopBuffer = softStopBuffer
|
||||
refreshParams.hardStopPrice = options.stopLossPrice
|
||||
}
|
||||
|
||||
console.log(`🛡️ (${options.context}) Placing refreshed exit orders: size=$${trade.currentSize.toFixed(2)} SL=${options.stopLossPrice.toFixed(4)} TP=${options.tp1Price.toFixed(4)}`)
|
||||
const exitOrdersResult = await placeExitOrders(refreshParams)
|
||||
|
||||
if (exitOrdersResult.success) {
|
||||
console.log(`✅ (${options.context}) Exit orders refreshed on-chain`)
|
||||
} else {
|
||||
console.error(`❌ (${options.context}) Failed to place refreshed exit orders: ${exitOrdersResult.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ (${options.context}) Error refreshing exit orders:`, error)
|
||||
// Monitoring loop will still enforce SL logic even if on-chain refresh fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save trade state to database (for persistence across restarts)
|
||||
*/
|
||||
@@ -1103,7 +951,6 @@ 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)
|
||||
@@ -1111,6 +958,14 @@ export class PositionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configuration from merged sources (used after settings updates)
|
||||
*/
|
||||
refreshConfig(partial?: Partial<TradingConfig>): void {
|
||||
this.config = getMergedConfig(partial)
|
||||
console.log('🔄 Position Manager config refreshed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monitoring status
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user