Files
trading_bot_v3/lib/price-monitor.ts
mindesbunister 491ff51ba9 feat: Enhanced Jupiter DEX with full bidirectional trading support
MAJOR ENHANCEMENTS:

- Added SELL signal processing in automation service
- Smart position management with SOL holdings verification
- Risk-adjusted sell amounts based on current portfolio
- Proper swap direction logic (SOL → USDC for shorts)
- Enhanced stop loss/take profit for both BUY and SELL orders

- Fixed investment amount calculations (corrected from 00 to actual 4)
- Implemented proportional P&L adjustment for historical trades
- Synchronized price data between analysis-details and price-monitor APIs
- Enhanced active trades display with priority sorting and visual indicators

- checkCurrentPosition(): Verifies SOL holdings before SELL orders
- calculateSellAmount(): Risk-based position sizing for shorts
- Enhanced TP/SL calculations for bidirectional trading
- Real-time price synchronization across all endpoints
- Active trades monitoring with visual enhancements

- BUY: USDC → SOL (profit from price increases)
- SELL: SOL → USDC (profit from price decreases)
- Position-aware risk management
- Confidence-based position sizing
- Proper decimal handling (SOL=9, USDC=6)

- Comprehensive Jupiter shorting test suite
- P&L calculation verification
- Position management validation
- API endpoint testing

- P&L corrected from .15 to /bin/bash.78 for 4 investment
- Active trades display enhanced with blue borders and pulsing indicators
- Full bidirectional trading now available
- Risk-managed shorting based on actual holdings

This enables making money in both bull and bear markets! 🎯
2025-07-21 17:08:48 +02:00

481 lines
16 KiB
TypeScript

// Real-time price monitoring service with automatic analysis triggering
import { EventEmitter } from 'events'
import { PrismaClient } from '@prisma/client'
import PriceFetcher from './price-fetcher'
const prisma = new PrismaClient()
export interface PriceAlert {
id: string
symbol: string
tradeId: string
alertType: 'TP_APPROACH' | 'SL_APPROACH' | 'BREAKOUT' | 'REVERSAL'
currentPrice: number
targetPrice: number
threshold: number
triggered: boolean
createdAt: Date
}
export interface TradeMonitoring {
tradeId: string
symbol: string
side: 'BUY' | 'SELL'
entryPrice: number
stopLoss?: number
takeProfit?: number
currentPrice?: number
currentPnL?: number
pnlPercentage?: number
distanceToTP?: number
distanceToSL?: number
status: 'ACTIVE' | 'APPROACHING_TP' | 'APPROACHING_SL' | 'CRITICAL'
}
class PriceMonitor extends EventEmitter {
private static instance: PriceMonitor
private monitoringInterval: NodeJS.Timeout | null = null
private priceCache: Map<string, { price: number; timestamp: number }> = new Map()
private alerts: Map<string, PriceAlert> = new Map()
private isRunning = false
private readonly UPDATE_INTERVAL = 1 * 60 * 1000 // 1 minute for more responsive monitoring
private readonly CACHE_DURATION = 2 * 60 * 1000 // 2 minutes (slightly longer than update)
// Thresholds for triggering analysis
private readonly TP_APPROACH_THRESHOLD = 0.15 // 15% away from TP
private readonly SL_APPROACH_THRESHOLD = 0.25 // 25% away from SL
private readonly CRITICAL_THRESHOLD = 0.05 // 5% away from TP/SL
private constructor() {
super()
}
static getInstance(): PriceMonitor {
if (!PriceMonitor.instance) {
PriceMonitor.instance = new PriceMonitor()
}
return PriceMonitor.instance
}
async startMonitoring(): Promise<void> {
if (this.isRunning) {
console.log('📊 Price monitor already running')
return
}
this.isRunning = true
console.log('🚀 Starting real-time price monitoring service')
console.log(`⏱️ Update interval: ${this.UPDATE_INTERVAL / 1000}s (5 minutes)`)
// Initial price check
await this.updatePricesAndAnalyze()
// Set up periodic monitoring
this.monitoringInterval = setInterval(async () => {
try {
await this.updatePricesAndAnalyze()
} catch (error) {
console.error('❌ Error in price monitoring cycle:', error)
}
}, this.UPDATE_INTERVAL)
console.log('✅ Price monitoring service started')
}
async stopMonitoring(): Promise<void> {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval)
this.monitoringInterval = null
}
this.isRunning = false
console.log('⏹️ Price monitoring service stopped')
}
isMonitoring(): boolean {
return this.isRunning
}
private async updatePricesAndAnalyze(): Promise<void> {
console.log('📊 Price monitor: Updating prices and checking positions...')
try {
// Get all active trades
const activeTrades = await this.getActiveTradesForMonitoring()
// Always fetch prices for common trading symbols, even without active trades
const baseSymbols = ['SOLUSD', 'BTCUSD', 'ETHUSD'] // Common trading pairs
const tradeSymbols = activeTrades.map(trade => trade.symbol)
const symbols = [...new Set([...baseSymbols, ...tradeSymbols])]
console.log(`📊 Updating prices for symbols: ${symbols.join(', ')}`)
if (activeTrades.length === 0) {
console.log('📊 No active trades to monitor, but updating base symbol prices')
// Still update prices for base symbols
const priceUpdates = await this.fetchPricesForSymbols(symbols)
// Emit price updates for UI
this.emit('pricesUpdated', priceUpdates)
return
}
console.log(`📊 Monitoring ${activeTrades.length} active trades`)
// Update prices for all symbols
const priceUpdates = await this.fetchPricesForSymbols(symbols)
// Analyze each trade
const analysisRequests: string[] = []
const updatedTrades: TradeMonitoring[] = []
for (const trade of activeTrades) {
const currentPrice = priceUpdates.get(trade.symbol)
if (!currentPrice) {
console.log(`⚠️ Could not get price for ${trade.symbol}`)
continue
}
const monitoring = await this.analyzeTradePosition(trade, currentPrice)
updatedTrades.push(monitoring)
// Update trade in database with current PnL
await this.updateTradeCurrentData(trade.id, currentPrice, monitoring.currentPnL!)
// Check if trade should be closed (TP/SL hit)
const shouldClose = await this.checkTradeClose(trade, currentPrice)
if (shouldClose) {
await this.closeTrade(trade.id, currentPrice, shouldClose.reason)
console.log(`🔒 Trade ${trade.id.slice(-8)} closed: ${shouldClose.reason} at $${currentPrice}`)
continue // Skip further processing for this trade
}
// Check if analysis is needed
const needsAnalysis = this.shouldTriggerAnalysis(monitoring)
if (needsAnalysis) {
analysisRequests.push(trade.symbol)
console.log(`🎯 Analysis triggered for ${trade.symbol}: ${monitoring.status}`)
}
}
// Emit events for UI updates
this.emit('tradesUpdated', updatedTrades)
this.emit('pricesUpdated', priceUpdates)
// Trigger analysis for symbols that need it (deduplicated)
const uniqueAnalysisRequests = [...new Set(analysisRequests)]
for (const symbol of uniqueAnalysisRequests) {
await this.triggerSmartAnalysis(symbol, 'PRICE_MOVEMENT')
}
} catch (error) {
console.error('❌ Error in price monitoring update:', error)
}
}
private async getActiveTradesForMonitoring(): Promise<any[]> {
return await prisma.trade.findMany({
where: {
status: 'OPEN',
isAutomated: true
},
select: {
id: true,
symbol: true,
side: true,
amount: true,
price: true,
entryPrice: true,
stopLoss: true,
takeProfit: true,
leverage: true,
createdAt: true
}
})
}
private async fetchPricesForSymbols(symbols: string[]): Promise<Map<string, number>> {
const priceUpdates = new Map<string, number>()
for (const symbol of symbols) {
try {
// Check cache first
const cached = this.priceCache.get(symbol)
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
priceUpdates.set(symbol, cached.price)
continue
}
// Fetch new price
const price = await PriceFetcher.getCurrentPrice(symbol)
if (price > 0) {
priceUpdates.set(symbol, price)
this.priceCache.set(symbol, { price, timestamp: Date.now() })
console.log(`💰 ${symbol}: $${price.toFixed(2)}`)
}
// Rate limiting - small delay between requests
await new Promise(resolve => setTimeout(resolve, 100))
} catch (error) {
console.error(`❌ Error fetching price for ${symbol}:`, error)
}
}
return priceUpdates
}
private async analyzeTradePosition(trade: any, currentPrice: number): Promise<TradeMonitoring> {
const entryPrice = trade.entryPrice || trade.price
const leverage = trade.leverage || 1
// 🔥 FIX: Get actual trading amount from session settings
const session = await prisma.automationSession.findFirst({
where: { userId: trade.userId, symbol: trade.symbol },
orderBy: { createdAt: 'desc' }
})
const sessionSettings = session?.settings as any
const actualTradingAmount = trade.tradingAmount || sessionSettings?.tradingAmount || 34
const storedPositionValue = trade.amount * trade.price
const adjustmentRatio = actualTradingAmount / storedPositionValue
// Calculate PnL based on actual investment amount
const priceChange = trade.side === 'BUY' ?
(currentPrice - entryPrice) :
(entryPrice - currentPrice)
const rawPnL = priceChange * trade.amount * leverage
const currentPnL = rawPnL * adjustmentRatio // Adjust for actual investment
const pnlPercentage = (currentPnL / actualTradingAmount) * 100
console.log(`💰 Price Monitor P&L Calculation:`, {
tradeId: trade.id,
actualTradingAmount,
storedPositionValue: storedPositionValue.toFixed(2),
adjustmentRatio: adjustmentRatio.toFixed(4),
rawPnL: rawPnL.toFixed(2),
adjustedPnL: currentPnL.toFixed(2)
})
// Calculate distances to TP/SL
let distanceToTP: number | undefined
let distanceToSL: number | undefined
if (trade.takeProfit) {
distanceToTP = Math.abs(currentPrice - trade.takeProfit) / Math.abs(trade.takeProfit - entryPrice)
}
if (trade.stopLoss) {
distanceToSL = Math.abs(currentPrice - trade.stopLoss) / Math.abs(entryPrice - trade.stopLoss)
}
// Determine status
let status: TradeMonitoring['status'] = 'ACTIVE'
if (distanceToTP !== undefined && distanceToTP <= this.CRITICAL_THRESHOLD) {
status = 'CRITICAL'
} else if (distanceToSL !== undefined && distanceToSL <= this.CRITICAL_THRESHOLD) {
status = 'CRITICAL'
} else if (distanceToTP !== undefined && distanceToTP <= this.TP_APPROACH_THRESHOLD) {
status = 'APPROACHING_TP'
} else if (distanceToSL !== undefined && distanceToSL <= this.SL_APPROACH_THRESHOLD) {
status = 'APPROACHING_SL'
}
return {
tradeId: trade.id,
symbol: trade.symbol,
side: trade.side,
entryPrice,
stopLoss: trade.stopLoss,
takeProfit: trade.takeProfit,
currentPrice,
currentPnL,
pnlPercentage,
distanceToTP,
distanceToSL,
status
}
}
private shouldTriggerAnalysis(monitoring: TradeMonitoring): boolean {
// Generate alert ID
const alertId = `${monitoring.tradeId}_${monitoring.status}_${Date.now()}`
// Check if we already triggered for this situation recently
const recentAlert = Array.from(this.alerts.values()).find(alert =>
alert.tradeId === monitoring.tradeId &&
alert.alertType.includes(monitoring.status.split('_')[1] || monitoring.status) &&
Date.now() - alert.createdAt.getTime() < 30 * 60 * 1000 // 30 minutes cooldown
)
if (recentAlert) {
return false
}
// Trigger analysis for critical situations or approaches
const shouldTrigger = monitoring.status === 'CRITICAL' ||
monitoring.status === 'APPROACHING_TP' ||
monitoring.status === 'APPROACHING_SL'
if (shouldTrigger) {
// Create alert
const alert: PriceAlert = {
id: alertId,
symbol: monitoring.symbol,
tradeId: monitoring.tradeId,
alertType: monitoring.status === 'APPROACHING_TP' ? 'TP_APPROACH' :
monitoring.status === 'APPROACHING_SL' ? 'SL_APPROACH' : 'BREAKOUT',
currentPrice: monitoring.currentPrice!,
targetPrice: monitoring.status === 'APPROACHING_TP' ? monitoring.takeProfit! : monitoring.stopLoss!,
threshold: monitoring.status === 'APPROACHING_TP' ? monitoring.distanceToTP! : monitoring.distanceToSL!,
triggered: true,
createdAt: new Date()
}
this.alerts.set(alertId, alert)
this.emit('alert', alert)
}
return shouldTrigger
}
private async updateTradeCurrentData(tradeId: string, currentPrice: number, currentPnL: number): Promise<void> {
try {
await prisma.trade.update({
where: { id: tradeId },
data: {
// Store current price and PnL for reference
learningData: {
currentPrice,
currentPnL,
lastUpdated: new Date().toISOString()
}
}
})
} catch (error) {
console.error(`❌ Error updating trade ${tradeId}:`, error)
}
}
private async triggerSmartAnalysis(symbol: string, reason: string): Promise<void> {
try {
console.log(`🎯 Triggering smart analysis for ${symbol} (${reason})`)
// Import automation service
const { AutomationService } = await import('./automation-service-simple')
// Get the service instance or create analysis request
// This would trigger a new analysis cycle for the specific symbol
this.emit('analysisRequested', { symbol, reason, timestamp: new Date() })
} catch (error) {
console.error(`❌ Error triggering analysis for ${symbol}:`, error)
}
}
// Public methods for external access
getCurrentPrice(symbol: string): number | null {
const cached = this.priceCache.get(symbol)
return cached ? cached.price : null
}
getActiveAlerts(): PriceAlert[] {
return Array.from(this.alerts.values()).filter(alert => alert.triggered)
}
async getTradeMonitoringData(): Promise<TradeMonitoring[]> {
const activeTrades = await this.getActiveTradesForMonitoring()
const results: TradeMonitoring[] = []
for (const trade of activeTrades) {
const currentPrice = this.getCurrentPrice(trade.symbol)
if (currentPrice) {
const monitoring = await this.analyzeTradePosition(trade, currentPrice)
results.push(monitoring)
}
}
return results
}
// Manual price update (for immediate checks)
async forceUpdatePrice(symbol: string): Promise<number | null> {
try {
const price = await PriceFetcher.getCurrentPrice(symbol)
if (price > 0) {
this.priceCache.set(symbol, { price, timestamp: Date.now() })
return price
}
} catch (error) {
console.error(`❌ Error force updating price for ${symbol}:`, error)
}
return null
}
// Check if a trade should be closed based on TP/SL
private async checkTradeClose(trade: any, currentPrice: number): Promise<{ reason: string } | null> {
const entryPrice = trade.entryPrice || trade.price
// Check Take Profit
if (trade.takeProfit) {
const tpHit = (trade.side === 'BUY' && currentPrice >= trade.takeProfit) ||
(trade.side === 'SELL' && currentPrice <= trade.takeProfit)
if (tpHit) {
return { reason: 'TAKE_PROFIT' }
}
}
// Check Stop Loss
if (trade.stopLoss) {
const slHit = (trade.side === 'BUY' && currentPrice <= trade.stopLoss) ||
(trade.side === 'SELL' && currentPrice >= trade.stopLoss)
if (slHit) {
return { reason: 'STOP_LOSS' }
}
}
return null
}
// Close a trade by updating its status and exit data
private async closeTrade(tradeId: string, exitPrice: number, reason: string): Promise<void> {
try {
const trade = await prisma.trade.findUnique({ where: { id: tradeId } })
if (!trade) return
const entryPrice = trade.entryPrice || trade.price
const pnl = this.calculatePnL(trade.side, entryPrice, exitPrice, trade.amount)
const tradingAmount = trade.amount * entryPrice // Estimate trading amount
const pnlPercent = ((pnl / tradingAmount) * 100)
await prisma.trade.update({
where: { id: tradeId },
data: {
status: 'COMPLETED',
exitPrice: exitPrice,
closedAt: new Date(),
profit: pnl,
pnlPercent: pnlPercent,
outcome: pnl > 0 ? 'WIN' : pnl < 0 ? 'LOSS' : 'BREAK_EVEN'
}
})
} catch (error) {
console.error('Error closing trade:', error)
}
}
// Calculate P&L for a trade
private calculatePnL(side: string, entryPrice: number, exitPrice: number, amount: number): number {
if (side === 'BUY') {
return (exitPrice - entryPrice) * amount
} else {
return (entryPrice - exitPrice) * amount
}
}
}
export const priceMonitor = PriceMonitor.getInstance()
export default priceMonitor