feat: Phase 2 Smart Entry Timing - COMPLETE
Implementation of 1-minute data enhancements Phase 2: - Queue signals when price not at favorable pullback level - Monitor every 15s for 0.15-0.5% pullback (LONG=dip, SHORT=bounce) - Validate ADX hasn't dropped >2 points (trend still strong) - Timeout at 2 minutes → execute at current price - Expected improvement: 0.2-0.5% per trade = ,600-4,000 over 100 trades Files: - lib/trading/smart-entry-timer.ts (616 lines, zero TS errors) - app/api/trading/execute/route.ts (integrated smart entry check) - .env (SMART_ENTRY_* configuration, disabled by default) Next steps: - Test with SMART_ENTRY_ENABLED=true in development - Monitor first 5-10 trades for improvement verification - Enable in production after successful testing
This commit is contained in:
21
.env
21
.env
@@ -420,6 +420,27 @@ USE_PERCENTAGE_SIZE=false
|
||||
|
||||
BREAKEVEN_TRIGGER_PERCENT=0.4
|
||||
ATR_MULTIPLIER_FOR_TP2=2
|
||||
|
||||
# ================================
|
||||
# SMART ENTRY TIMING (Phase 2 - Nov 27, 2025)
|
||||
# ================================
|
||||
# Wait for optimal pullback within 2 minutes after 5-min signal
|
||||
# Expected impact: 0.2-0.5% better entry per trade = $1,600-4,000 over 100 trades
|
||||
|
||||
SMART_ENTRY_ENABLED=false # Set to true to enable smart entry timing
|
||||
SMART_ENTRY_MAX_WAIT_MS=120000 # 120,000ms = 2 minutes max wait
|
||||
SMART_ENTRY_PULLBACK_MIN=0.15 # 0.15% minimum pullback to consider
|
||||
SMART_ENTRY_PULLBACK_MAX=0.50 # 0.50% maximum pullback (beyond = possible reversal)
|
||||
SMART_ENTRY_ADX_TOLERANCE=2 # Max ADX drop allowed (2 points)
|
||||
|
||||
# How it works:
|
||||
# - 5-min signal arrives at candle close
|
||||
# - Bot waits up to 2 minutes for price to pullback
|
||||
# - LONG: Waits for dip 0.15-0.5% below signal price
|
||||
# - SHORT: Waits for bounce 0.15-0.5% above signal price
|
||||
# - Validates ADX hasn't dropped >2 points
|
||||
# - Timeout: Executes at market if no pullback within 2 minutes
|
||||
|
||||
ENABLE_AUTO_WITHDRAWALS=false
|
||||
WITHDRAWAL_INTERVAL_HOURS=168
|
||||
WITHDRAWAL_PROFIT_PERCENT=10
|
||||
|
||||
@@ -16,6 +16,7 @@ import { scoreSignalQuality } from '@/lib/trading/signal-quality'
|
||||
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
||||
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
|
||||
import { logCriticalError, logTradeExecution } from '@/lib/utils/persistent-logger'
|
||||
import { getSmartEntryTimer } from '@/lib/trading/smart-entry-timer'
|
||||
|
||||
export interface ExecuteTradeRequest {
|
||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||
@@ -427,6 +428,73 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
|
||||
// 🎯 SMART ENTRY TIMING - Check if we should wait for better entry (Phase 2 - Nov 27, 2025)
|
||||
const smartEntryTimer = getSmartEntryTimer()
|
||||
if (smartEntryTimer.isEnabled() && body.signalPrice) {
|
||||
console.log(`🎯 Smart Entry: Evaluating entry timing...`)
|
||||
|
||||
// Get current price to check if already at favorable level
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
const latestPrice = priceMonitor.getCachedPrice(driftSymbol)
|
||||
const currentPrice = latestPrice?.price || body.signalPrice
|
||||
|
||||
const priceChange = ((currentPrice - body.signalPrice) / body.signalPrice) * 100
|
||||
const isPullbackDirection = body.direction === 'long' ? priceChange < 0 : priceChange > 0
|
||||
const pullbackMagnitude = Math.abs(priceChange)
|
||||
|
||||
const pullbackMin = parseFloat(process.env.SMART_ENTRY_PULLBACK_MIN || '0.15')
|
||||
const pullbackMax = parseFloat(process.env.SMART_ENTRY_PULLBACK_MAX || '0.50')
|
||||
|
||||
console.log(` Signal Price: $${body.signalPrice.toFixed(2)}`)
|
||||
console.log(` Current Price: $${currentPrice.toFixed(2)} (${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%)`)
|
||||
|
||||
if (isPullbackDirection && pullbackMagnitude >= pullbackMin && pullbackMagnitude <= pullbackMax) {
|
||||
// Already at favorable entry - execute immediately!
|
||||
console.log(`✅ Smart Entry: Already at favorable level (${pullbackMagnitude.toFixed(2)}% pullback)`)
|
||||
console.log(` Executing immediately - no need to wait`)
|
||||
} else if (!isPullbackDirection || pullbackMagnitude < pullbackMin) {
|
||||
// Not favorable yet - queue for smart entry
|
||||
console.log(`⏳ Smart Entry: Queuing signal for optimal entry timing`)
|
||||
console.log(` Waiting for ${body.direction === 'long' ? 'dip' : 'bounce'} of ${pullbackMin}-${pullbackMax}%`)
|
||||
|
||||
// Queue the signal with full context
|
||||
const queuedSignal = smartEntryTimer.queueSignal({
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
signalPrice: body.signalPrice,
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio,
|
||||
pricePosition: body.pricePosition,
|
||||
indicatorVersion: body.indicatorVersion,
|
||||
qualityScore: qualityResult.score,
|
||||
})
|
||||
|
||||
// Return success immediately (n8n workflow continues)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Signal queued for smart entry timing',
|
||||
smartEntry: {
|
||||
enabled: true,
|
||||
queuedAt: new Date().toISOString(),
|
||||
signalId: queuedSignal.id,
|
||||
targetPullback: `${pullbackMin}-${pullbackMax}%`,
|
||||
maxWait: `${parseInt(process.env.SMART_ENTRY_MAX_WAIT_MS || '120000') / 1000}s`,
|
||||
currentPullback: `${priceChange.toFixed(2)}%`,
|
||||
},
|
||||
positionId: `queued-${queuedSignal.id}`,
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
qualityScore: qualityResult.score,
|
||||
}, { status: 200 })
|
||||
} else if (pullbackMagnitude > pullbackMax) {
|
||||
// Pullback too large - might be reversal, execute with caution
|
||||
console.log(`⚠️ Smart Entry: Pullback too large (${pullbackMagnitude.toFixed(2)}% > ${pullbackMax}%)`)
|
||||
console.log(` Possible reversal - executing at current price with caution`)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for rate limit spacing
|
||||
const rpcDelay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
|
||||
621
lib/trading/smart-entry-timer.ts
Normal file
621
lib/trading/smart-entry-timer.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 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 { 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
|
||||
}
|
||||
|
||||
console.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
|
||||
}): 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
|
||||
}
|
||||
|
||||
this.queuedSignals.set(signal.id, signal)
|
||||
|
||||
console.log(`📥 Smart Entry: Queued signal ${signal.id}`)
|
||||
console.log(` ${signal.direction.toUpperCase()} ${signal.symbol} @ $${signal.signalPrice.toFixed(2)}`)
|
||||
console.log(` Target pullback: ${this.config.pullbackMin}-${this.config.pullbackMax}%`)
|
||||
console.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
|
||||
|
||||
console.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
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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
|
||||
console.log(`🔍 Smart Entry: Checking ${signal.symbol} (check #${signal.checksPerformed})`)
|
||||
console.log(` Signal: $${signal.signalPrice.toFixed(2)} → Current: $${currentPrice.toFixed(2)}`)
|
||||
console.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`)
|
||||
|
||||
// Check if pullback is in target range
|
||||
if (pullbackMagnitude < signal.targetPullbackMin) {
|
||||
console.log(` ⏳ Waiting for pullback (${pullbackMagnitude.toFixed(2)}% < ${signal.targetPullbackMin}%)`)
|
||||
return
|
||||
}
|
||||
|
||||
if (pullbackMagnitude > signal.targetPullbackMax) {
|
||||
console.log(` ⚠️ Pullback too large (${pullbackMagnitude.toFixed(2)}% > ${signal.targetPullbackMax}%), might be reversal - waiting`)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate ADX hasn't degraded
|
||||
const marketCache = getMarketDataCache()
|
||||
const latestMetrics = marketCache.get(signal.symbol)
|
||||
|
||||
if (latestMetrics && latestMetrics.adx) {
|
||||
const adxDrop = signal.signalADX - latestMetrics.adx
|
||||
|
||||
if (adxDrop > signal.adxTolerance) {
|
||||
console.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)
|
||||
console.log(` 🚫 Signal cancelled: ADX degradation exceeded tolerance`)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(` ✅ ADX validation: ${signal.signalADX.toFixed(1)} → ${latestMetrics.adx.toFixed(1)} (within tolerance)`)
|
||||
}
|
||||
|
||||
// All conditions met - execute!
|
||||
console.log(`✅ Smart Entry: OPTIMAL ENTRY CONFIRMED`)
|
||||
console.log(` Pullback: ${pullbackMagnitude.toFixed(2)}% (target ${signal.targetPullbackMin}-${signal.targetPullbackMax}%)`)
|
||||
console.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
|
||||
|
||||
console.log(`🎯 Smart Entry: EXECUTING ${signal.direction.toUpperCase()} ${signal.symbol}`)
|
||||
console.log(` Signal Price: $${signal.signalPrice.toFixed(2)}`)
|
||||
console.log(` Entry Price: $${entryPrice.toFixed(2)}`)
|
||||
console.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
|
||||
}
|
||||
|
||||
// Get position size from config
|
||||
const config = getMergedConfig()
|
||||
const { size: positionSizeUSD, leverage } = await getActualPositionSizeForSymbol(
|
||||
signal.symbol,
|
||||
config,
|
||||
signal.qualityScore
|
||||
)
|
||||
|
||||
console.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!
|
||||
console.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)
|
||||
|
||||
// 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: config.takeProfit2SizePercent ?? 0,
|
||||
direction: signal.direction,
|
||||
useDualStops: config.useDualStops,
|
||||
softStopPrice,
|
||||
softStopBuffer: config.softStopBuffer,
|
||||
hardStopPrice
|
||||
})
|
||||
|
||||
if (exitRes.success) {
|
||||
exitOrderSignatures = exitRes.signatures || []
|
||||
console.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: config.takeProfit2SizePercent,
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.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)
|
||||
console.log(`📊 Smart Entry: Added to Position Manager`)
|
||||
} catch (pmError) {
|
||||
console.error(`❌ Smart Entry: Failed to add to Position Manager:`, pmError)
|
||||
}
|
||||
|
||||
console.log(`✅ Smart Entry: Execution complete for ${signal.symbol}`)
|
||||
console.log(` Entry improvement: ${improvementDirection >= 0 ? '+' : ''}${improvementDirection.toFixed(2)}%`)
|
||||
console.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)
|
||||
console.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)
|
||||
console.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()
|
||||
console.log('✅ Smart Entry Timer service initialized')
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user