Files
trading_bot_v4/lib/trading/smart-entry-timer.ts
mindesbunister 5aad42f25f critical: FIX smart entry timeout position sizing catastrophe (97.6% size loss) + Telegram null response
BUGS FIXED:
1. Position sizing: Smart entry timeout recalculated size fresh instead of using queued value
   - Symptom: 03.95 position instead of ,354 (97.6% loss)
   - Root cause: executeSignal() called getActualPositionSizeForSymbol() fresh
   - Fix: Store positionSizeUSD and leverage when queueing, use stored values during execution

2. Telegram null: Smart entry timeout executed outside API context, returned nothing
   - Symptom: Telegram bot receives 'null' message
   - Root cause: Timeout execution in background process doesn't return to API
   - Fix: Send Telegram notification directly from executeSignal() method

FILES CHANGED:
- app/api/trading/execute/route.ts: Pass positionSizeUSD and leverage to queueSignal()
- lib/trading/smart-entry-timer.ts:
  * Accept positionSizeUSD/leverage in queueSignal() params
  * Store values in QueuedSignal object
  * Use stored values in executeSignal() instead of recalculating
  * Send Telegram notification after successful execution

IMPACT:
- ALL smart entry timeout trades now use correct position size
- User receives proper Telegram notification for timeout executions
- ,000+ in lost profits prevented going forward

DEPLOYMENT:
- Built: Sun Dec 14 12:51:46 CET 2025
- Container restarted with --force-recreate
- Status: LIVE in production

See Common Pitfalls section for full details.
2025-12-14 12:51:46 +01:00

777 lines
28 KiB
TypeScript

/**
* Smart Entry Timer Service
*
* Implements Phase 2: Smart Entry Timing
* Waits up to 2 minutes for favorable pullback after signal arrival
*
* Strategy:
* - LONG: Wait for 0.15-0.5% dip below signal price
* - SHORT: Wait for 0.15-0.5% bounce above signal price
* - Validate ADX hasn't dropped >2 points (trend still strong)
* - Timeout at 2 minutes → execute at whatever price
*
* Expected improvement: 0.2-0.5% per trade = $1,600-4,000 over 100 trades
*/
import { getMarketDataCache } from './market-data-cache'
import { logger } from '../utils/logger'
import { getPythPriceMonitor } from '../pyth/price-monitor'
export interface QueuedSignal {
id: string
symbol: string
direction: 'long' | 'short'
signalPrice: number
signalTime: number
receivedAt: number
expiresAt: number
// Pullback targets
targetPullbackMin: number // 0.15%
targetPullbackMax: number // 0.50%
// ADX validation
signalADX: number
adxTolerance: number // 2 points
// Original signal data (for execution)
originalSignalData: {
atr?: number
adx?: number
rsi?: number
volumeRatio?: number
pricePosition?: number
indicatorVersion?: string
}
// Tracking
status: 'pending' | 'executed' | 'cancelled' | 'expired'
checksPerformed: number
bestPriceObserved: number
executedAt?: number
executedPrice?: number
executionReason?: 'pullback_confirmed' | 'timeout' | 'manual_override'
// Quality tracking
qualityScore: number
// Position sizing (passed from execute endpoint)
positionSize?: number // USD amount for position
leverage?: number // Leverage for position
positionSizeUSD?: number // Alternative field name used by execute endpoint
}
interface SmartEntryConfig {
enabled: boolean
maxWaitMs: number // 2 minutes
pullbackMin: number // 0.15%
pullbackMax: number // 0.50%
adxTolerance: number // 2 points
monitorIntervalMs: number // 15 seconds
}
export class SmartEntryTimer {
private queuedSignals: Map<string, QueuedSignal> = new Map()
private monitoringInterval: NodeJS.Timeout | null = null
private config: SmartEntryConfig
constructor() {
// Load configuration from ENV
this.config = {
enabled: process.env.SMART_ENTRY_ENABLED === 'true',
maxWaitMs: parseInt(process.env.SMART_ENTRY_MAX_WAIT_MS || '120000'),
pullbackMin: parseFloat(process.env.SMART_ENTRY_PULLBACK_MIN || '0.15'),
pullbackMax: parseFloat(process.env.SMART_ENTRY_PULLBACK_MAX || '0.50'),
adxTolerance: parseFloat(process.env.SMART_ENTRY_ADX_TOLERANCE || '2'),
monitorIntervalMs: 15000 // 15 seconds
}
logger.log('💡 Smart Entry Timer initialized:', {
enabled: this.config.enabled,
maxWait: `${this.config.maxWaitMs / 1000}s`,
pullback: `${this.config.pullbackMin}-${this.config.pullbackMax}%`,
adxTolerance: `${this.config.adxTolerance} points`
})
}
/**
* Queue a signal for smart entry timing
*/
queueSignal(signalData: {
symbol: string
direction: 'long' | 'short'
signalPrice: number
atr?: number
adx?: number
rsi?: number
volumeRatio?: number
pricePosition?: number
indicatorVersion?: string
qualityScore: number
positionSizeUSD?: number // CRITICAL FIX (Dec 13, 2025): Store calculated USD size
leverage?: number // CRITICAL FIX (Dec 13, 2025): Store calculated leverage
}): QueuedSignal {
const now = Date.now()
const signal: QueuedSignal = {
id: `${signalData.symbol}-${now}`,
symbol: signalData.symbol,
direction: signalData.direction,
signalPrice: signalData.signalPrice,
signalTime: now,
receivedAt: now,
expiresAt: now + this.config.maxWaitMs,
targetPullbackMin: this.config.pullbackMin,
targetPullbackMax: this.config.pullbackMax,
signalADX: signalData.adx || 25, // Default if not provided
adxTolerance: this.config.adxTolerance,
originalSignalData: {
atr: signalData.atr,
adx: signalData.adx,
rsi: signalData.rsi,
volumeRatio: signalData.volumeRatio,
pricePosition: signalData.pricePosition,
indicatorVersion: signalData.indicatorVersion
},
status: 'pending',
checksPerformed: 0,
bestPriceObserved: signalData.signalPrice,
qualityScore: signalData.qualityScore,
positionSizeUSD: signalData.positionSizeUSD, // Store calculated size
leverage: signalData.leverage, // Store calculated leverage
}
this.queuedSignals.set(signal.id, signal)
logger.log(`📥 Smart Entry: Queued signal ${signal.id}`)
logger.log(` ${signal.direction.toUpperCase()} ${signal.symbol} @ $${signal.signalPrice.toFixed(2)}`)
logger.log(` Target pullback: ${this.config.pullbackMin}-${this.config.pullbackMax}%`)
logger.log(` Max wait: ${this.config.maxWaitMs / 1000}s`)
// Start monitoring if not already running
if (!this.monitoringInterval) {
this.startMonitoring()
}
return signal
}
/**
* Start monitoring loop
*/
private startMonitoring(): void {
if (this.monitoringInterval) return
logger.log(`👁️ Smart Entry: Starting monitoring loop (${this.config.monitorIntervalMs / 1000}s interval)`)
this.monitoringInterval = setInterval(() => {
this.checkAllSignals()
}, this.config.monitorIntervalMs)
}
/**
* Stop monitoring loop
*/
private stopMonitoring(): void {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval)
this.monitoringInterval = null
logger.log(`⏸️ Smart Entry: Monitoring stopped (no active signals)`)
}
}
/**
* Check all queued signals
*/
private async checkAllSignals(): Promise<void> {
const now = Date.now()
for (const [id, signal] of this.queuedSignals) {
if (signal.status !== 'pending') continue
signal.checksPerformed++
// Check for timeout
if (now >= signal.expiresAt) {
logger.log(`⏰ Smart Entry: Timeout for ${signal.symbol} (waited ${this.config.maxWaitMs / 1000}s)`)
const priceMonitor = getPythPriceMonitor()
const latestPrice = priceMonitor.getCachedPrice(signal.symbol)
const currentPrice = latestPrice?.price || signal.signalPrice
await this.executeSignal(signal, currentPrice, 'timeout')
continue
}
// Check for optimal entry
await this.checkSignalForEntry(signal)
}
}
/**
* Check if signal should be executed now
*/
private async checkSignalForEntry(signal: QueuedSignal): Promise<void> {
// Get current price
const priceMonitor = getPythPriceMonitor()
const latestPrice = priceMonitor.getCachedPrice(signal.symbol)
if (!latestPrice || !latestPrice.price) {
logger.log(`⚠️ Smart Entry: No price available for ${signal.symbol}, skipping check`)
return
}
const currentPrice = latestPrice.price
// Update best price
if (signal.direction === 'long' && currentPrice < signal.bestPriceObserved) {
signal.bestPriceObserved = currentPrice
} else if (signal.direction === 'short' && currentPrice > signal.bestPriceObserved) {
signal.bestPriceObserved = currentPrice
}
// Calculate pullback magnitude
let pullbackMagnitude: number
if (signal.direction === 'long') {
// LONG: Want price BELOW signal (pullback = dip)
pullbackMagnitude = ((signal.signalPrice - currentPrice) / signal.signalPrice) * 100
} else {
// SHORT: Want price ABOVE signal (pullback = bounce)
pullbackMagnitude = ((currentPrice - signal.signalPrice) / signal.signalPrice) * 100
}
// Log check
logger.log(`🔍 Smart Entry: Checking ${signal.symbol} (check #${signal.checksPerformed})`)
logger.log(` Signal: $${signal.signalPrice.toFixed(2)} → Current: $${currentPrice.toFixed(2)}`)
logger.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`)
// Check if pullback is in target range
if (pullbackMagnitude < signal.targetPullbackMin) {
logger.log(` ⏳ Waiting for pullback (${pullbackMagnitude.toFixed(2)}% < ${signal.targetPullbackMin}%)`)
return
}
if (pullbackMagnitude > signal.targetPullbackMax) {
logger.log(` ⚠️ Pullback too large (${pullbackMagnitude.toFixed(2)}% > ${signal.targetPullbackMax}%), might be reversal - waiting`)
return
}
// ============================================================
// PHASE 7.2: REAL-TIME QUALITY VALIDATION (Nov 27, 2025)
// ============================================================
// Re-validate signal quality before entry using fresh market data
// Prevents execution if conditions degraded during wait period
const marketCache = getMarketDataCache()
const latestMetrics = marketCache.get(signal.symbol)
if (latestMetrics) {
const now = Date.now()
const dataAge = (now - latestMetrics.timestamp) / 1000
logger.log(` 📊 Real-time validation (data age: ${dataAge.toFixed(0)}s):`)
// 1. ADX degradation check (original logic)
if (latestMetrics.adx) {
const adxDrop = signal.signalADX - latestMetrics.adx
if (adxDrop > signal.adxTolerance) {
logger.log(` ❌ ADX degraded: ${signal.signalADX.toFixed(1)}${latestMetrics.adx.toFixed(1)} (dropped ${adxDrop.toFixed(1)} points, max ${signal.adxTolerance})`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: ADX degradation exceeded tolerance`)
return
}
logger.log(` ✅ ADX: ${signal.signalADX.toFixed(1)}${latestMetrics.adx.toFixed(1)} (within tolerance)`)
}
// 2. Volume degradation check (NEW)
// If volume drops significantly, momentum may be fading
if (signal.originalSignalData.volumeRatio && latestMetrics.volumeRatio) {
const originalVolume = signal.originalSignalData.volumeRatio
const currentVolume = latestMetrics.volumeRatio
const volumeDrop = ((originalVolume - currentVolume) / originalVolume) * 100
// Cancel if volume dropped >40%
if (volumeDrop > 40) {
logger.log(` ❌ Volume collapsed: ${originalVolume.toFixed(2)}x → ${currentVolume.toFixed(2)}x (${volumeDrop.toFixed(0)}% drop)`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: Volume degradation - momentum fading`)
return
}
logger.log(` ✅ Volume: ${originalVolume.toFixed(2)}x → ${currentVolume.toFixed(2)}x`)
}
// 3. RSI reversal check (NEW)
// If RSI crossed into opposite territory, trend may be reversing
if (signal.originalSignalData.rsi && latestMetrics.rsi) {
const originalRSI = signal.originalSignalData.rsi
const currentRSI = latestMetrics.rsi
if (signal.direction === 'long') {
// LONG: Cancel if RSI dropped into oversold (<30)
if (originalRSI >= 40 && currentRSI < 30) {
logger.log(` ❌ RSI collapsed: ${originalRSI.toFixed(1)}${currentRSI.toFixed(1)} (now oversold)`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`)
return
}
} else {
// SHORT: Cancel if RSI rose into overbought (>70)
if (originalRSI <= 60 && currentRSI > 70) {
logger.log(` ❌ RSI spiked: ${originalRSI.toFixed(1)}${currentRSI.toFixed(1)} (now overbought)`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: RSI reversal - trend weakening`)
return
}
}
logger.log(` ✅ RSI: ${originalRSI.toFixed(1)}${currentRSI.toFixed(1)}`)
}
// 4. MAGAP divergence check (NEW)
// If MA gap widened in opposite direction, structure changing
if (latestMetrics.maGap !== undefined) {
const currentMAGap = latestMetrics.maGap
if (signal.direction === 'long' && currentMAGap < -1.0) {
// LONG but MAs now bearish diverging
logger.log(` ❌ MA structure bearish: MAGAP ${currentMAGap.toFixed(2)}% (death cross accelerating)`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: MA structure turned bearish`)
return
}
if (signal.direction === 'short' && currentMAGap > 1.0) {
// SHORT but MAs now bullish diverging
logger.log(` ❌ MA structure bullish: MAGAP ${currentMAGap.toFixed(2)}% (golden cross accelerating)`)
signal.status = 'cancelled'
signal.executionReason = 'manual_override'
this.queuedSignals.delete(signal.id)
logger.log(` 🚫 Signal cancelled: MA structure turned bullish`)
return
}
logger.log(` ✅ MAGAP: ${currentMAGap.toFixed(2)}%`)
}
logger.log(` ✅ All real-time validations passed - signal quality maintained`)
} else {
logger.log(` ⚠️ No fresh market data available - proceeding with original signal`)
}
// All conditions met - execute!
logger.log(`✅ Smart Entry: OPTIMAL ENTRY CONFIRMED`)
logger.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`)
logger.log(` Price improvement: $${signal.signalPrice.toFixed(2)}$${currentPrice.toFixed(2)}`)
await this.executeSignal(signal, currentPrice, 'pullback_confirmed')
}
/**
* Execute the trade with actual entry price
*/
private async executeSignal(
signal: QueuedSignal,
entryPrice: number,
reason: 'pullback_confirmed' | 'timeout' | 'manual_override'
): Promise<void> {
signal.status = 'executed'
signal.executedAt = Date.now()
signal.executedPrice = entryPrice
signal.executionReason = reason
const improvement = ((signal.signalPrice - entryPrice) / signal.signalPrice) * 100
const improvementDirection = signal.direction === 'long' ? improvement : -improvement
logger.log(`🎯 Smart Entry: EXECUTING ${signal.direction.toUpperCase()} ${signal.symbol}`)
logger.log(` Signal Price: $${signal.signalPrice.toFixed(2)}`)
logger.log(` Entry Price: $${entryPrice.toFixed(2)}`)
logger.log(` Improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`)
// Execute the actual trade through Drift
try {
const { openPosition, placeExitOrders } = await import('../drift/orders')
const { initializeDriftService } = await import('../drift/client')
const { createTrade } = await import('../database/trades')
const { getInitializedPositionManager } = await import('./position-manager')
const { getMergedConfig, getActualPositionSizeForSymbol, SUPPORTED_MARKETS } = await import('../../config/trading')
// Get Drift service
const driftService = await initializeDriftService()
if (!driftService) {
console.error('❌ Smart Entry: Drift service not available')
return
}
// Get market config
const marketConfig = SUPPORTED_MARKETS[signal.symbol]
if (!marketConfig) {
console.error(`❌ Smart Entry: No market config for ${signal.symbol}`)
return
}
// CRITICAL FIX (Dec 13, 2025): Use stored positionSizeUSD from queue time
// Bug: Recalculating fresh causes $10.40 instead of $435 (97.6% size loss)
// Fix: Use stored values when available, only recalculate as fallback
const config = getMergedConfig()
let positionSizeUSD: number
let leverage: number
if (signal.positionSizeUSD && signal.leverage) {
// Use stored values from queue time (FIX for timeout sizing bug)
positionSizeUSD = signal.positionSizeUSD
leverage = signal.leverage
logger.log(` Using stored position size: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage (queued values)`)
} else {
// Fallback: Recalculate (backwards compatibility for old queued signals)
const sizing = await getActualPositionSizeForSymbol(
signal.symbol,
config,
signal.qualityScore
)
positionSizeUSD = sizing.size
leverage = sizing.leverage
logger.log(` Recalculated position size: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage (fallback)`)
}
logger.log(` Opening position: $${positionSizeUSD.toFixed(2)} at ${leverage}x leverage`)
// Open position
const openResult = await openPosition({
symbol: signal.symbol,
direction: signal.direction,
sizeUSD: positionSizeUSD,
slippageTolerance: 1.0 // 1% slippage tolerance
})
if (!openResult.success || !openResult.transactionSignature) {
console.error(`❌ Smart Entry: Position open failed - ${openResult.error}`)
return
}
const fillPrice = openResult.fillPrice!
logger.log(`✅ Smart Entry: Position opened at $${fillPrice.toFixed(2)}`)
// Calculate TP/SL prices
let tp1Percent = config.takeProfit1Percent
let tp2Percent = config.takeProfit2Percent
let slPercent = config.stopLossPercent
if (config.useAtrBasedTargets && signal.originalSignalData.atr && signal.originalSignalData.atr > 0) {
// ATR-based targets
tp1Percent = this.calculatePercentFromAtr(
signal.originalSignalData.atr,
fillPrice,
config.atrMultiplierTp1,
config.minTp1Percent,
config.maxTp1Percent
)
tp2Percent = this.calculatePercentFromAtr(
signal.originalSignalData.atr,
fillPrice,
config.atrMultiplierTp2,
config.minTp2Percent,
config.maxTp2Percent
)
slPercent = -Math.abs(this.calculatePercentFromAtr(
signal.originalSignalData.atr,
fillPrice,
config.atrMultiplierSl,
config.minSlPercent,
config.maxSlPercent
))
}
const stopLossPrice = this.calculatePrice(fillPrice, slPercent, signal.direction)
const tp1Price = this.calculatePrice(fillPrice, tp1Percent, signal.direction)
const tp2Price = this.calculatePrice(fillPrice, tp2Percent, signal.direction)
const effectiveTp2SizePercent =
config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0
? 0
: (config.takeProfit2SizePercent ?? 0)
// Dual stops if enabled
let softStopPrice: number | undefined
let hardStopPrice: number | undefined
if (config.useDualStops) {
softStopPrice = this.calculatePrice(fillPrice, config.softStopPercent, signal.direction)
hardStopPrice = this.calculatePrice(fillPrice, config.hardStopPercent, signal.direction)
}
// Place exit orders
let exitOrderSignatures: string[] = []
try {
const exitRes = await placeExitOrders({
symbol: signal.symbol,
positionSizeUSD,
entryPrice: fillPrice,
tp1Price,
tp2Price,
stopLossPrice,
tp1SizePercent: config.takeProfit1SizePercent ?? 75,
tp2SizePercent: effectiveTp2SizePercent,
direction: signal.direction,
useDualStops: config.useDualStops,
softStopPrice,
softStopBuffer: config.softStopBuffer,
hardStopPrice
})
if (exitRes.success) {
exitOrderSignatures = exitRes.signatures || []
logger.log(`✅ Smart Entry: Exit orders placed - ${exitOrderSignatures.length} orders`)
}
} catch (err) {
console.error(`❌ Smart Entry: Error placing exit orders:`, err)
}
// Save to database
try {
await createTrade({
positionId: openResult.transactionSignature,
symbol: signal.symbol,
direction: signal.direction,
entryPrice: fillPrice,
positionSizeUSD,
leverage,
stopLossPrice,
takeProfit1Price: tp1Price,
takeProfit2Price: tp2Price,
tp1SizePercent: config.takeProfit1SizePercent,
tp2SizePercent: effectiveTp2SizePercent,
entryOrderTx: openResult.transactionSignature,
atrAtEntry: signal.originalSignalData.atr,
adxAtEntry: signal.originalSignalData.adx,
rsiAtEntry: signal.originalSignalData.rsi,
signalQualityScore: signal.qualityScore,
indicatorVersion: signal.originalSignalData.indicatorVersion,
signalSource: 'tradingview',
tp1OrderTx: exitOrderSignatures[0],
tp2OrderTx: exitOrderSignatures[1],
slOrderTx: exitOrderSignatures[2],
configSnapshot: {
leverage,
stopLossPercent: slPercent,
takeProfit1Percent: tp1Percent,
takeProfit2Percent: tp2Percent,
useDualStops: config.useDualStops,
smartEntry: {
used: true,
improvement: improvementDirection,
waitTime: Math.round((signal.executedAt! - signal.signalTime) / 1000),
reason: reason,
checksPerformed: signal.checksPerformed
}
}
})
logger.log(`💾 Smart Entry: Trade saved to database`)
} catch (dbError) {
console.error(`❌ Smart Entry: Failed to save trade:`, dbError)
const { logCriticalError } = await import('../utils/persistent-logger')
logCriticalError('SMART_ENTRY_DATABASE_FAILURE', {
error: dbError,
symbol: signal.symbol,
direction: signal.direction,
entryPrice: fillPrice,
transactionSignature: openResult.transactionSignature
})
}
// Add to Position Manager
try {
const positionManager = await getInitializedPositionManager()
const emergencyStopPrice = this.calculatePrice(
fillPrice,
-Math.abs(config.emergencyStopPercent),
signal.direction
)
const activeTrade: import('./position-manager').ActiveTrade = {
id: `trade-${Date.now()}`,
positionId: openResult.transactionSignature,
symbol: signal.symbol,
direction: signal.direction,
entryPrice: fillPrice,
entryTime: Date.now(),
positionSize: positionSizeUSD,
leverage,
stopLossPrice,
tp1Price,
tp2Price,
emergencyStopPrice,
currentSize: positionSizeUSD,
originalPositionSize: positionSizeUSD,
takeProfitPrice1: tp1Price,
takeProfitPrice2: tp2Price,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
realizedPnL: 0,
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: fillPrice,
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: fillPrice,
maxAdversePrice: fillPrice,
originalAdx: signal.originalSignalData.adx,
timesScaled: 0,
totalScaleAdded: 0,
priceCheckCount: 0,
lastPrice: fillPrice,
lastUpdateTime: Date.now(),
atrAtEntry: signal.originalSignalData.atr,
adxAtEntry: signal.originalSignalData.adx,
signalQualityScore: signal.qualityScore
}
await positionManager.addTrade(activeTrade)
logger.log(`📊 Smart Entry: Added to Position Manager`)
// CRITICAL FIX (Dec 13, 2025): Send Telegram notification for timeout executions
// Bug: Telegram receives "null" when smart entry times out
// Fix: Send notification directly from executeSignal since it runs outside API context
try {
const { sendTelegramMessage } = await import('../notifications/telegram')
const timeWaited = Math.round((Date.now() - signal.receivedAt) / 1000)
const message = `
🎯 POSITION OPENED (Smart Entry ${reason})
📈 ${signal.symbol} ${signal.direction.toUpperCase()}
💰 Size: $${positionSizeUSD.toFixed(2)}
⚡ Leverage: ${leverage}x
🎯 Quality: ${signal.qualityScore}
📍 Entry: $${fillPrice.toFixed(4)}
🎯 TP1: $${tp1Price.toFixed(4)} (+${tp1Percent.toFixed(2)}%)
🎯 TP2: $${tp2Price.toFixed(4)} (+${tp2Percent.toFixed(2)}%)
🛡️ SL: $${stopLossPrice.toFixed(4)} (${slPercent.toFixed(2)}%)
⏱️ Wait time: ${timeWaited}s
📊 Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%
💵 Value saved: $${(Math.abs(improvementDirection) / 100 * positionSizeUSD).toFixed(2)}
${reason === 'timeout' ? '⏰ Executed at timeout (max wait reached)' : '✅ Optimal entry confirmed'}
`.trim()
await sendTelegramMessage(message)
logger.log(`📱 Smart Entry: Telegram notification sent`)
} catch (telegramError) {
console.error(`❌ Smart Entry: Telegram notification failed:`, telegramError)
// Don't fail the trade execution just because notification failed
}
} catch (pmError) {
console.error(`❌ Smart Entry: Failed to add to Position Manager:`, pmError)
}
logger.log(`✅ Smart Entry: Execution complete for ${signal.symbol}`)
logger.log(` Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`)
logger.log(` Estimated value: $${(Math.abs(improvementDirection) / 100 * positionSizeUSD).toFixed(2)}`)
} catch (error) {
console.error(`❌ Smart Entry: Execution error:`, error)
}
// Remove from queue after brief delay (for logging)
setTimeout(() => {
this.queuedSignals.delete(signal.id)
logger.log(`🗑️ Smart Entry: Cleaned up signal ${signal.id}`)
if (this.queuedSignals.size === 0) {
this.stopMonitoring()
}
}, 5000)
}
/**
* Helper: Calculate price from percentage
*/
private calculatePrice(
entryPrice: number,
percent: number,
direction: 'long' | 'short'
): number {
if (direction === 'long') {
return entryPrice * (1 + percent / 100)
} else {
return entryPrice * (1 - percent / 100)
}
}
/**
* Helper: Calculate percentage from ATR with safety bounds
*/
private calculatePercentFromAtr(
atrValue: number,
entryPrice: number,
atrMultiplier: number,
minPercent: number,
maxPercent: number
): number {
const atrPercent = (atrValue / entryPrice) * 100
const targetPercent = atrPercent * atrMultiplier
return Math.max(minPercent, Math.min(maxPercent, targetPercent))
}
/**
* Get status of all queued signals (for debugging)
*/
getQueueStatus(): QueuedSignal[] {
return Array.from(this.queuedSignals.values())
}
/**
* Cancel a specific queued signal
*/
cancelSignal(signalId: string): boolean {
const signal = this.queuedSignals.get(signalId)
if (!signal) {
return false
}
signal.status = 'cancelled'
this.queuedSignals.delete(signalId)
logger.log(`🚫 Smart Entry: Cancelled signal ${signalId}`)
return true
}
/**
* Check if smart entry is enabled
*/
isEnabled(): boolean {
return this.config.enabled
}
}
// Singleton instance
let smartEntryTimerInstance: SmartEntryTimer | null = null
export function getSmartEntryTimer(): SmartEntryTimer {
if (!smartEntryTimerInstance) {
smartEntryTimerInstance = new SmartEntryTimer()
}
return smartEntryTimerInstance
}
export function startSmartEntryTracking(): void {
getSmartEntryTimer()
logger.log('✅ Smart Entry Timer service initialized')
}