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:
mindesbunister
2025-11-07 20:40:07 +01:00
parent 6d5991172a
commit 9b767342dc
14 changed files with 1150 additions and 568 deletions

View File

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