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
|
BREAKEVEN_TRIGGER_PERCENT=0.4
|
||||||
ATR_MULTIPLIER_FOR_TP2=2
|
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
|
ENABLE_AUTO_WITHDRAWALS=false
|
||||||
WITHDRAWAL_INTERVAL_HOURS=168
|
WITHDRAWAL_INTERVAL_HOURS=168
|
||||||
WITHDRAWAL_PROFIT_PERCENT=10
|
WITHDRAWAL_PROFIT_PERCENT=10
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { scoreSignalQuality } from '@/lib/trading/signal-quality'
|
|||||||
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
||||||
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
|
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
|
||||||
import { logCriticalError, logTradeExecution } from '@/lib/utils/persistent-logger'
|
import { logCriticalError, logTradeExecution } from '@/lib/utils/persistent-logger'
|
||||||
|
import { getSmartEntryTimer } from '@/lib/trading/smart-entry-timer'
|
||||||
|
|
||||||
export interface ExecuteTradeRequest {
|
export interface ExecuteTradeRequest {
|
||||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
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(` Leverage: ${leverage}x`)
|
||||||
console.log(` Total position: $${positionSizeUSD}`)
|
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
|
// Helper function for rate limit spacing
|
||||||
const rpcDelay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
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