import { PrismaClient } from '@prisma/client' import { aiAnalysisService, AnalysisResult } from './ai-analysis' import { jupiterDEXService } from './jupiter-dex-service' import { TradingViewCredentials } from './tradingview-automation' const prisma = new PrismaClient() export interface AutomationConfig { userId: string mode: 'SIMULATION' | 'LIVE' symbol: string timeframe: string selectedTimeframes: string[] // Multi-timeframe support tradingAmount: number maxLeverage: number // stopLossPercent and takeProfitPercent removed - AI calculates these automatically maxDailyTrades: number riskPercentage: number dexProvider: 'JUPITER' | 'DRIFT' } export interface AutomationStatus { isActive: boolean mode: 'SIMULATION' | 'LIVE' symbol: string timeframe: string totalTrades: number successfulTrades: number winRate: number totalPnL: number lastAnalysis?: Date lastTrade?: Date nextScheduled?: Date errorCount: number lastError?: string } export class AutomationService { private activeSession: any = null private intervalId: NodeJS.Timeout | null = null private isRunning = false private credentials: TradingViewCredentials | null = null constructor() { this.initialize() } private async initialize() { // Load credentials from environment or database this.credentials = { email: process.env.TRADINGVIEW_EMAIL || '', password: process.env.TRADINGVIEW_PASSWORD || '' } } async startAutomation(config: AutomationConfig): Promise { try { if (this.isRunning) { throw new Error('Automation is already running') } // Validate configuration if (!config.userId || !config.symbol || !config.timeframe) { throw new Error('Invalid automation configuration') } // Create or update automation session const existingSession = await prisma.automationSession.findFirst({ where: { userId: config.userId, symbol: config.symbol, timeframe: config.timeframe } }) let session if (existingSession) { session = await prisma.automationSession.update({ where: { id: existingSession.id }, data: { status: 'ACTIVE', mode: config.mode, settings: config as any, updatedAt: new Date() } }) } else { session = await prisma.automationSession.create({ data: { userId: config.userId, status: 'ACTIVE', mode: config.mode, symbol: config.symbol, timeframe: config.timeframe, settings: config as any } }) } this.activeSession = session this.isRunning = true // Start the automation loop this.startAutomationLoop(config) console.log(`πŸ€– Automation started for ${config.symbol} ${config.timeframe} in ${config.mode} mode`) return true } catch (error) { console.error('Failed to start automation:', error) return false } } async stopAutomation(): Promise { try { if (!this.isRunning) { return true } // Clear interval if (this.intervalId) { clearInterval(this.intervalId) this.intervalId = null } // Update session status if (this.activeSession) { await prisma.automationSession.update({ where: { id: this.activeSession.id }, data: { status: 'STOPPED', updatedAt: new Date() } }) } this.isRunning = false this.activeSession = null console.log('πŸ›‘ Automation stopped') return true } catch (error) { console.error('Failed to stop automation:', error) return false } } async pauseAutomation(): Promise { try { if (!this.isRunning || !this.activeSession) { return false } // Clear interval but keep session if (this.intervalId) { clearInterval(this.intervalId) this.intervalId = null } // Update session status await prisma.automationSession.update({ where: { id: this.activeSession.id }, data: { status: 'PAUSED', updatedAt: new Date() } }) console.log('⏸️ Automation paused') return true } catch (error) { console.error('Failed to pause automation:', error) return false } } async resumeAutomation(): Promise { try { if (!this.activeSession) { return false } // Update session status await prisma.automationSession.update({ where: { id: this.activeSession.id }, data: { status: 'ACTIVE', updatedAt: new Date() } }) // Restart automation loop const config = this.activeSession.settings as AutomationConfig this.startAutomationLoop(config) console.log('▢️ Automation resumed') return true } catch (error) { console.error('Failed to resume automation:', error) return false } } private startAutomationLoop(config: AutomationConfig) { // Calculate interval based on timeframe const intervalMs = this.getIntervalFromTimeframe(config.timeframe) console.log(`πŸ”„ Starting automation loop every ${intervalMs/1000/60} minutes`) this.intervalId = setInterval(async () => { try { await this.executeAutomationCycle(config) } catch (error) { console.error('Automation cycle error:', error) await this.handleAutomationError(error) } }, intervalMs) // Execute first cycle immediately setTimeout(async () => { try { await this.executeAutomationCycle(config) } catch (error) { console.error('Initial automation cycle error:', error) await this.handleAutomationError(error) } }, 5000) // 5 second delay for initialization } private async executeAutomationCycle(config: AutomationConfig) { console.log(`πŸ”„ Executing automation cycle for ${config.symbol} ${config.timeframe}`) // Check for open positions first (instead of daily trade limit) const hasOpenPosition = await this.checkForOpenPositions(config) if (hasOpenPosition) { console.log(`πŸ“Š Open position detected for ${config.symbol}, monitoring only`) return } // Generate session ID for progress tracking const sessionId = `auto_${Date.now()}_${Math.random().toString(36).substr(2, 8)}` // Step 1: Capture screenshot and analyze const screenshotConfig = { symbol: config.symbol, timeframe: config.timeframe, layouts: ['ai', 'diy'], sessionId, analyze: true } const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig) if (!result.analysis || result.screenshots.length === 0) { console.log('❌ Failed to capture or analyze chart') return } // Step 2: Store analysis in database for learning (only if analysis exists) if (result.analysis) { await this.storeAnalysisForLearning(config, { ...result, analysis: result.analysis }, sessionId) } // Step 3: Check if we should execute trade const shouldTrade = await this.shouldExecuteTrade(result.analysis, config) if (!shouldTrade) { console.log('πŸ“Š Analysis does not meet trading criteria') return } // Step 4: Execute trade based on analysis await this.executeTrade(config, result.analysis, result.screenshots[0]) // Step 5: Update session statistics await this.updateSessionStats(config.userId) } private async storeAnalysisForLearning( config: AutomationConfig, result: { screenshots: string[], analysis: AnalysisResult }, sessionId: string ) { try { // Store in trading journal await prisma.tradingJournal.create({ data: { userId: config.userId, screenshotUrl: result.screenshots.join(','), aiAnalysis: JSON.stringify(result.analysis), marketSentiment: result.analysis.marketSentiment, keyLevels: result.analysis.keyLevels, recommendation: result.analysis.recommendation, confidence: result.analysis.confidence, symbol: config.symbol, timeframe: config.timeframe, tradingMode: config.mode, sessionId: sessionId, priceAtAnalysis: result.analysis.entry?.price } }) // Store in AI learning data await prisma.aILearningData.create({ data: { userId: config.userId, sessionId: sessionId, analysisData: result.analysis as any, marketConditions: { timeframe: config.timeframe, symbol: config.symbol, timestamp: new Date().toISOString() }, confidenceScore: result.analysis.confidence, timeframe: config.timeframe, symbol: config.symbol, screenshot: result.screenshots[0], predictedPrice: result.analysis.entry?.price } }) console.log('πŸ“š Analysis stored for learning') } catch (error) { console.error('Failed to store analysis for learning:', error) } } private async shouldExecuteTrade(analysis: AnalysisResult, config: AutomationConfig): Promise { // Check minimum confidence threshold if (analysis.confidence < 70) { console.log(`πŸ“Š Confidence too low: ${analysis.confidence}%`) return false } // Check if recommendation is actionable if (analysis.recommendation === 'HOLD') { console.log('πŸ“Š Recommendation is HOLD') return false } // Check if we have required trading levels if (!analysis.entry || !analysis.stopLoss) { console.log('πŸ“Š Missing entry or stop loss levels') return false } // Check risk/reward ratio if (analysis.riskToReward) { const rr = this.parseRiskReward(analysis.riskToReward) if (rr < 2) { console.log(`πŸ“Š Risk/reward ratio too low: ${rr}`) return false } } // Check recent performance for dynamic adjustments const recentPerformance = await this.getRecentPerformance(config.userId) if (recentPerformance.winRate < 0.4 && recentPerformance.totalTrades > 10) { console.log('πŸ“Š Recent performance too poor, requiring higher confidence') return analysis.confidence > 80 } return true } private async checkForOpenPositions(config: AutomationConfig): Promise { try { console.log(`πŸ” Checking for open positions for ${config.symbol}`) // For Jupiter DEX, we don't have persistent positions like in Drift // This method would need to be implemented based on your specific needs // For now, return false to allow trading if (config.dexProvider === 'DRIFT') { // Check Drift positions via API const response = await fetch('http://localhost:3000/api/drift/positions') if (!response.ok) { console.warn('⚠️ Could not fetch Drift positions, assuming no open positions') return false } const data = await response.json() if (!data.success || !data.positions) { return false } // Check if there's an open position for the current symbol const symbolBase = config.symbol.replace('USD', '') // SOLUSD -> SOL const openPosition = data.positions.find((pos: any) => pos.symbol.includes(symbolBase) && pos.size > 0.001 ) if (openPosition) { console.log(`πŸ“Š Found open ${openPosition.side} position: ${openPosition.symbol} ${openPosition.size}`) return true } } return false } catch (error) { console.error('❌ Error checking positions:', error) // On error, assume no positions to allow trading return false } } private async executeTrade(config: AutomationConfig, analysis: AnalysisResult, screenshotUrl: string) { try { console.log(`πŸš€ Executing ${config.mode} trade: ${analysis.recommendation} ${config.symbol}`) const side = analysis.recommendation === 'BUY' ? 'BUY' : 'SELL' const amount = await this.calculateTradeAmount(config, analysis) const leverage = Math.min(config.maxLeverage, 3) // Cap at 3x for safety let tradeResult: any = null if (config.mode === 'SIMULATION') { // Simulate trade tradeResult = await this.simulateTrade({ symbol: config.symbol, side, amount, price: analysis.entry?.price || 0, stopLoss: analysis.stopLoss?.price, takeProfit: analysis.takeProfits?.tp1?.price, leverage }) } else { // Execute real trade via unified trading endpoint tradeResult = await this.executeUnifiedTrade({ symbol: config.symbol, side, amount, stopLoss: analysis.stopLoss?.price, takeProfit: analysis.takeProfits?.tp1?.price, leverage, dexProvider: config.dexProvider }) } // Store trade in database await prisma.trade.create({ data: { userId: config.userId, symbol: config.symbol, side, amount, price: analysis.entry?.price || 0, status: tradeResult?.success ? 'FILLED' : 'FAILED', isAutomated: true, entryPrice: analysis.entry?.price, stopLoss: analysis.stopLoss?.price, takeProfit: analysis.takeProfits?.tp1?.price, leverage, timeframe: config.timeframe, tradingMode: config.mode, confidence: analysis.confidence, marketSentiment: analysis.marketSentiment, screenshotUrl, aiAnalysis: JSON.stringify(analysis), driftTxId: tradeResult?.txId, executedAt: new Date() } }) console.log(`βœ… Trade executed: ${tradeResult?.success ? 'SUCCESS' : 'FAILED'}`) } catch (error) { console.error('Trade execution error:', error) // Store failed trade await prisma.trade.create({ data: { userId: config.userId, symbol: config.symbol, side: analysis.recommendation === 'BUY' ? 'BUY' : 'SELL', amount: config.tradingAmount, price: analysis.entry?.price || 0, status: 'FAILED', isAutomated: true, timeframe: config.timeframe, tradingMode: config.mode, confidence: analysis.confidence, marketSentiment: analysis.marketSentiment, screenshotUrl, aiAnalysis: JSON.stringify(analysis) } }) } } private async executeUnifiedTrade(params: { symbol: string side: string amount: number stopLoss?: number takeProfit?: number leverage?: number dexProvider: 'JUPITER' | 'DRIFT' }): Promise<{ success: boolean; txId?: string }> { try { console.log(`πŸš€ Executing ${params.dexProvider} trade: ${params.side} ${params.amount} ${params.symbol}`) const response = await fetch('http://localhost:3000/api/automation/trade', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ symbol: params.symbol, side: params.side, amount: params.amount, leverage: params.leverage, stopLoss: params.stopLoss, takeProfit: params.takeProfit, dexProvider: params.dexProvider, mode: 'LIVE' }) }) if (!response.ok) { throw new Error(`Trade request failed: ${response.statusText}`) } const result = await response.json() return { success: result.success, txId: result.txId || result.transactionId } } catch (error) { console.error('Unified trade execution error:', error) return { success: false } } } private async simulateTrade(params: { symbol: string side: string amount: number price: number stopLoss?: number takeProfit?: number leverage?: number }): Promise<{ success: boolean; txId?: string }> { // Simulate realistic execution with small random variation const priceVariation = 0.001 * (Math.random() - 0.5) // Β±0.1% const executedPrice = params.price * (1 + priceVariation) // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)) return { success: true, txId: `sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}` } } private async calculateTradeAmount(config: AutomationConfig, analysis: AnalysisResult): Promise { try { // Fetch actual account balance from Drift console.log('πŸ’° Fetching account balance for position sizing...') const balanceResponse = await fetch(`http://localhost:3000/api/drift/balance`) if (!balanceResponse.ok) { console.log('⚠️ Failed to fetch balance, using fallback calculation') // Fallback to config amount let amount = Math.min(config.tradingAmount, 35) // Cap at $35 max const riskAdjustment = config.riskPercentage / 100 return Math.max(amount * riskAdjustment, 5) } const balanceData = await balanceResponse.json() const availableBalance = parseFloat(balanceData.availableBalance || '0') console.log(`πŸ’° Available balance: $${availableBalance}`) if (availableBalance <= 0) { throw new Error('No available balance') } // Calculate position size based on risk percentage of available balance const riskAmount = availableBalance * (config.riskPercentage / 100) // Adjust based on confidence (reduce risk for low confidence signals) const confidenceMultiplier = Math.min(analysis.confidence / 100, 1) let amount = riskAmount * confidenceMultiplier // Apply leverage to get position size amount *= Math.min(config.maxLeverage, 10) // Ensure minimum trade amount but cap at available balance amount = Math.max(amount, 5) // Minimum $5 position amount = Math.min(amount, availableBalance * 0.8) // Never use more than 80% of balance console.log(`πŸ“Š Position sizing calculation:`) console.log(` - Available balance: $${availableBalance}`) console.log(` - Risk percentage: ${config.riskPercentage}%`) console.log(` - Risk amount: $${riskAmount.toFixed(2)}`) console.log(` - Confidence multiplier: ${confidenceMultiplier}`) console.log(` - Leverage: ${Math.min(config.maxLeverage, 10)}x`) console.log(` - Final position size: $${amount.toFixed(2)}`) return Math.round(amount * 100) / 100 // Round to 2 decimal places } catch (error) { console.log(`⚠️ Error calculating trade amount: ${error instanceof Error ? error.message : String(error)}`) // Safe fallback - use small fixed amount return 5 } } private parseRiskReward(rrString: string): number { // Parse "1:2.5" format const parts = rrString.split(':') if (parts.length === 2) { return parseFloat(parts[1]) / parseFloat(parts[0]) } return 0 } private getIntervalFromTimeframe(timeframe: string): number { const intervals: { [key: string]: number } = { '1m': 1 * 60 * 1000, '5m': 5 * 60 * 1000, '15m': 15 * 60 * 1000, '30m': 30 * 60 * 1000, '1h': 60 * 60 * 1000, '2h': 2 * 60 * 60 * 1000, '4h': 4 * 60 * 60 * 1000, '6h': 6 * 60 * 60 * 1000, '12h': 12 * 60 * 60 * 1000, '1d': 24 * 60 * 60 * 1000 } return intervals[timeframe] || intervals['1h'] // Default to 1 hour } private async getRecentPerformance(userId: string): Promise<{ winRate: number totalTrades: number avgRR: number }> { const thirtyDaysAgo = new Date() thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30) const trades = await prisma.trade.findMany({ where: { userId, isAutomated: true, createdAt: { gte: thirtyDaysAgo }, status: 'FILLED' } }) const totalTrades = trades.length const winningTrades = trades.filter(t => (t.pnlPercent || 0) > 0).length const winRate = totalTrades > 0 ? winningTrades / totalTrades : 0 const avgRR = trades.reduce((sum, t) => sum + (t.actualRR || 0), 0) / Math.max(totalTrades, 1) return { winRate, totalTrades, avgRR } } private async updateSessionStats(userId: string) { try { if (!this.activeSession) return const stats = await this.getRecentPerformance(userId) await prisma.automationSession.update({ where: { id: this.activeSession.id }, data: { totalTrades: stats.totalTrades, successfulTrades: Math.round(stats.totalTrades * stats.winRate), winRate: stats.winRate, avgRiskReward: stats.avgRR, lastAnalysis: new Date() } }) } catch (error) { console.error('Failed to update session stats:', error) } } private async handleAutomationError(error: any) { try { if (this.activeSession) { await prisma.automationSession.update({ where: { id: this.activeSession.id }, data: { errorCount: { increment: 1 }, lastError: error.message || 'Unknown error', updatedAt: new Date() } }) // Stop automation if too many errors if (this.activeSession.errorCount >= 5) { console.log('❌ Too many errors, stopping automation') await this.stopAutomation() } } } catch (dbError) { console.error('Failed to handle automation error:', dbError) } } async getStatus(): Promise { try { if (!this.activeSession) { return null } const session = await prisma.automationSession.findUnique({ where: { id: this.activeSession.id } }) if (!session) { return null } return { isActive: this.isRunning, mode: session.mode as 'SIMULATION' | 'LIVE', symbol: session.symbol, timeframe: session.timeframe, totalTrades: session.totalTrades, successfulTrades: session.successfulTrades, winRate: session.winRate, totalPnL: session.totalPnL, lastAnalysis: session.lastAnalysis || undefined, lastTrade: session.lastTrade || undefined, nextScheduled: session.nextScheduled || undefined, errorCount: session.errorCount, lastError: session.lastError || undefined } } catch (error) { console.error('Failed to get automation status:', error) return null } } async getLearningInsights(userId: string): Promise<{ totalAnalyses: number avgAccuracy: number bestTimeframe: string worstTimeframe: string commonFailures: string[] recommendations: string[] }> { try { const learningData = await prisma.aILearningData.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, take: 100 }) const totalAnalyses = learningData.length const avgAccuracy = learningData.reduce((sum, d) => sum + (d.accuracyScore || 0), 0) / Math.max(totalAnalyses, 1) // Group by timeframe to find best/worst const timeframeStats = learningData.reduce((acc, d) => { if (!acc[d.timeframe]) { acc[d.timeframe] = { count: 0, accuracy: 0 } } acc[d.timeframe].count += 1 acc[d.timeframe].accuracy += d.accuracyScore || 0 return acc }, {} as { [key: string]: { count: number, accuracy: number } }) const timeframes = Object.entries(timeframeStats).map(([tf, stats]) => ({ timeframe: tf, avgAccuracy: stats.accuracy / stats.count })) const bestTimeframe = timeframes.sort((a, b) => b.avgAccuracy - a.avgAccuracy)[0]?.timeframe || 'Unknown' const worstTimeframe = timeframes.sort((a, b) => a.avgAccuracy - b.avgAccuracy)[0]?.timeframe || 'Unknown' return { totalAnalyses, avgAccuracy, bestTimeframe, worstTimeframe, commonFailures: [ 'Low confidence predictions', 'Missed support/resistance levels', 'Timeframe misalignment' ], recommendations: [ 'Focus on higher timeframes for better accuracy', 'Wait for higher confidence signals', 'Use multiple timeframe confirmation' ] } } catch (error) { console.error('Failed to get learning insights:', error) return { totalAnalyses: 0, avgAccuracy: 0, bestTimeframe: 'Unknown', worstTimeframe: 'Unknown', commonFailures: [], recommendations: [] } } } } export const automationService = new AutomationService()