FIXES: - Enhanced signalAnalysisCycleComplete with more intelligent cleanup logic - Added active session detection to avoid killing processes during analysis - Implemented graceful shutdown (SIGTERM) before force kill (SIGKILL) - Only kills processes older than 2 minutes to avoid disrupting active analysis - Added 10 second delay in runPostCycleCleanup to ensure trading decision is complete - Improved process age filtering to prevent premature cleanup - Cleanup now properly correlates with analysis completion + trading decision - Reduced aggressive kills that were happening during active analysis - Better CPU usage management through smarter process lifecycle - Prevents cleanup from interfering with ongoing analysis work This should significantly reduce the zombie process CPU usage issue by ensuring cleanup only happens when analysis work is truly complete and decisions are finalized.
908 lines
30 KiB
TypeScript
908 lines
30 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
||
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
||
import { jupiterDEXService } from './jupiter-dex-service'
|
||
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
||
import { TradingViewCredentials } from './tradingview-automation'
|
||
import { progressTracker } from './progress-tracker'
|
||
import aggressiveCleanup from './aggressive-cleanup'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
export interface AutomationConfig {
|
||
userId: string
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
tradingAmount: number
|
||
maxLeverage: number
|
||
stopLossPercent: number
|
||
takeProfitPercent: number
|
||
maxDailyTrades: number
|
||
riskPercentage: number
|
||
}
|
||
|
||
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 isRunning = false
|
||
private config: AutomationConfig | null = null
|
||
private intervalId: NodeJS.Timeout | null = null
|
||
private stats = {
|
||
totalTrades: 0,
|
||
successfulTrades: 0,
|
||
winRate: 0,
|
||
totalPnL: 0,
|
||
errorCount: 0,
|
||
lastError: null as string | null
|
||
}
|
||
|
||
async startAutomation(config: AutomationConfig): Promise<boolean> {
|
||
try {
|
||
if (this.isRunning) {
|
||
throw new Error('Automation is already running')
|
||
}
|
||
|
||
this.config = config
|
||
this.isRunning = true
|
||
|
||
console.log(`🤖 Starting automation for ${config.symbol} ${config.timeframe} in ${config.mode} mode`)
|
||
|
||
// Ensure user exists in database
|
||
await prisma.user.upsert({
|
||
where: { id: config.userId },
|
||
update: {},
|
||
create: {
|
||
id: config.userId,
|
||
email: `${config.userId}@example.com`,
|
||
name: config.userId,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Delete any existing automation session for this user/symbol/timeframe
|
||
await prisma.automationSession.deleteMany({
|
||
where: {
|
||
userId: config.userId,
|
||
symbol: config.symbol,
|
||
timeframe: config.timeframe
|
||
}
|
||
})
|
||
|
||
// Create new automation session in database
|
||
await prisma.automationSession.create({
|
||
data: {
|
||
userId: config.userId,
|
||
status: 'ACTIVE',
|
||
mode: config.mode,
|
||
symbol: config.symbol,
|
||
timeframe: config.timeframe,
|
||
settings: {
|
||
tradingAmount: config.tradingAmount,
|
||
maxLeverage: config.maxLeverage,
|
||
stopLossPercent: config.stopLossPercent,
|
||
takeProfitPercent: config.takeProfitPercent,
|
||
maxDailyTrades: config.maxDailyTrades,
|
||
riskPercentage: config.riskPercentage
|
||
},
|
||
startBalance: config.tradingAmount,
|
||
currentBalance: config.tradingAmount,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Start automation cycle
|
||
this.startAutomationCycle()
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to start automation:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
return false
|
||
}
|
||
}
|
||
|
||
private startAutomationCycle(): void {
|
||
if (!this.config) return
|
||
|
||
// Get interval in milliseconds based on timeframe
|
||
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
|
||
|
||
console.log(`🔄 Starting automation cycle every ${intervalMs/1000} seconds`)
|
||
|
||
this.intervalId = setInterval(async () => {
|
||
if (this.isRunning && this.config) {
|
||
await this.runAutomationCycle()
|
||
}
|
||
}, intervalMs)
|
||
|
||
// Run first cycle immediately
|
||
this.runAutomationCycle()
|
||
}
|
||
|
||
private getIntervalFromTimeframe(timeframe: string): number {
|
||
const intervals: { [key: string]: number } = {
|
||
'1m': 60 * 1000,
|
||
'3m': 3 * 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,
|
||
'1d': 24 * 60 * 60 * 1000
|
||
}
|
||
|
||
return intervals[timeframe] || intervals['1h'] // Default to 1 hour
|
||
}
|
||
|
||
private async runAutomationCycle(): Promise<void> {
|
||
if (!this.config) return
|
||
|
||
try {
|
||
console.log(`🔍 Running automation cycle for ${this.config.symbol} ${this.config.timeframe}`)
|
||
|
||
// Step 1: Check daily trade limit
|
||
const todayTrades = await this.getTodayTradeCount(this.config.userId)
|
||
if (todayTrades >= this.config.maxDailyTrades) {
|
||
console.log(`📊 Daily trade limit reached (${todayTrades}/${this.config.maxDailyTrades})`)
|
||
// Run cleanup even when trade limit is reached
|
||
await this.runPostCycleCleanup('trade_limit_reached')
|
||
return
|
||
}
|
||
|
||
// Step 2: Take screenshot and analyze
|
||
const analysisResult = await this.performAnalysis()
|
||
if (!analysisResult) {
|
||
console.log('❌ Analysis failed, skipping cycle')
|
||
// Run cleanup when analysis fails
|
||
await this.runPostCycleCleanup('analysis_failed')
|
||
return
|
||
}
|
||
|
||
// Step 3: Store analysis for learning
|
||
await this.storeAnalysisForLearning(analysisResult)
|
||
|
||
// Step 4: Update session with latest analysis
|
||
await this.updateSessionWithAnalysis(analysisResult)
|
||
|
||
// Step 5: Make trading decision
|
||
const tradeDecision = await this.makeTradeDecision(analysisResult)
|
||
if (!tradeDecision) {
|
||
console.log('📊 No trading opportunity found')
|
||
// Run cleanup when no trading opportunity
|
||
await this.runPostCycleCleanup('no_opportunity')
|
||
return
|
||
}
|
||
|
||
// Step 6: Execute trade
|
||
await this.executeTrade(tradeDecision)
|
||
|
||
// Run cleanup after successful trade execution
|
||
await this.runPostCycleCleanup('trade_executed')
|
||
|
||
} catch (error) {
|
||
console.error('Error in automation cycle:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
|
||
// Run cleanup on error
|
||
await this.runPostCycleCleanup('error')
|
||
}
|
||
}
|
||
|
||
private async runPostCycleCleanup(reason: string): Promise<void> {
|
||
console.log(`🧹 Running post-cycle cleanup (reason: ${reason})`)
|
||
|
||
// Longer delay to ensure all analysis processes AND trading decision have finished
|
||
await new Promise(resolve => setTimeout(resolve, 10000)) // 10 seconds
|
||
|
||
try {
|
||
// Signal that the complete analysis cycle is done
|
||
await aggressiveCleanup.signalAnalysisCycleComplete()
|
||
console.log(`✅ Post-cycle cleanup completed for: ${reason}`)
|
||
} catch (error) {
|
||
console.error('Error in post-cycle cleanup:', error)
|
||
}
|
||
}
|
||
|
||
private async performAnalysis(): Promise<{
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} | null> {
|
||
// Generate unique session ID for this analysis
|
||
const sessionId = `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||
|
||
try {
|
||
console.log(`📸 Starting multi-timeframe analysis with dual layouts... (Session: ${sessionId})`)
|
||
|
||
// Create progress tracking session
|
||
const progressSteps = [
|
||
{ id: 'init', title: 'Initialize', description: 'Starting multi-timeframe analysis', status: 'pending' as const },
|
||
{ id: 'capture', title: 'Capture', description: 'Capturing screenshots for all timeframes', status: 'pending' as const },
|
||
{ id: 'analysis', title: 'Analysis', description: 'Running AI analysis on screenshots', status: 'pending' as const },
|
||
{ id: 'complete', title: 'Complete', description: 'Analysis complete', status: 'pending' as const }
|
||
]
|
||
|
||
progressTracker.createSession(sessionId, progressSteps)
|
||
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting multi-timeframe analysis...')
|
||
|
||
// Multi-timeframe analysis: 15m, 1h, 2h, 4h
|
||
const timeframes = ['15', '1h', '2h', '4h']
|
||
const symbol = this.config!.symbol
|
||
|
||
console.log(`🔍 Analyzing ${symbol} across timeframes: ${timeframes.join(', ')} with AI + DIY layouts`)
|
||
|
||
progressTracker.updateStep(sessionId, 'init', 'completed', `Starting analysis for ${timeframes.length} timeframes`)
|
||
progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...')
|
||
|
||
// Analyze each timeframe with both AI and DIY layouts
|
||
const multiTimeframeResults = await this.analyzeMultiTimeframeWithDualLayouts(symbol, timeframes, sessionId)
|
||
|
||
if (multiTimeframeResults.length === 0) {
|
||
console.log('❌ No multi-timeframe analysis results')
|
||
progressTracker.updateStep(sessionId, 'capture', 'error', 'No analysis results captured')
|
||
progressTracker.deleteSession(sessionId)
|
||
return null
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${multiTimeframeResults.length} timeframe analyses`)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Processing multi-timeframe results...')
|
||
|
||
// Process and combine multi-timeframe results
|
||
const combinedResult = this.combineMultiTimeframeAnalysis(multiTimeframeResults)
|
||
|
||
if (!combinedResult.analysis) {
|
||
console.log('❌ Failed to combine multi-timeframe analysis')
|
||
progressTracker.updateStep(sessionId, 'analysis', 'error', 'Failed to combine analysis results')
|
||
progressTracker.deleteSession(sessionId)
|
||
return null
|
||
}
|
||
|
||
console.log(`✅ Multi-timeframe analysis completed: ${combinedResult.analysis.recommendation} with ${combinedResult.analysis.confidence}% confidence`)
|
||
console.log(`📊 Timeframe alignment: ${this.analyzeTimeframeAlignment(multiTimeframeResults)}`)
|
||
|
||
progressTracker.updateStep(sessionId, 'analysis', 'completed', `Analysis complete: ${combinedResult.analysis.recommendation}`)
|
||
progressTracker.updateStep(sessionId, 'complete', 'completed', 'Multi-timeframe analysis finished')
|
||
|
||
// Clean up session after successful completion
|
||
setTimeout(() => {
|
||
progressTracker.deleteSession(sessionId)
|
||
}, 2000)
|
||
|
||
return combinedResult
|
||
|
||
} catch (error) {
|
||
console.error('Error performing multi-timeframe analysis:', error)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'error', error instanceof Error ? error.message : 'Unknown error')
|
||
setTimeout(() => {
|
||
progressTracker.deleteSession(sessionId)
|
||
}, 5000)
|
||
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async analyzeMultiTimeframeWithDualLayouts(
|
||
symbol: string,
|
||
timeframes: string[],
|
||
sessionId: string
|
||
): Promise<Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>> {
|
||
const results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }> = []
|
||
|
||
for (let i = 0; i < timeframes.length; i++) {
|
||
const timeframe = timeframes[i]
|
||
try {
|
||
console.log(`📊 Analyzing ${symbol} ${timeframe} with AI + DIY layouts... (${i + 1}/${timeframes.length})`)
|
||
|
||
// Update progress for timeframe
|
||
progressTracker.updateTimeframeProgress(sessionId, i + 1, timeframes.length, timeframe)
|
||
|
||
// Use the dual-layout configuration for each timeframe
|
||
const screenshotConfig = {
|
||
symbol: symbol,
|
||
timeframe: timeframe,
|
||
layouts: ['ai', 'diy'],
|
||
sessionId: sessionId
|
||
}
|
||
|
||
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig)
|
||
|
||
if (result.analysis) {
|
||
console.log(`✅ ${timeframe} analysis: ${result.analysis.recommendation} (${result.analysis.confidence}% confidence)`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: result.analysis
|
||
})
|
||
} else {
|
||
console.log(`❌ ${timeframe} analysis failed`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
|
||
// Small delay between captures to avoid overwhelming the system
|
||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||
|
||
} catch (error) {
|
||
console.error(`Failed to analyze ${symbol} ${timeframe}:`, error)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
}
|
||
|
||
return results
|
||
}
|
||
|
||
private combineMultiTimeframeAnalysis(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} {
|
||
const validResults = results.filter(r => r.analysis !== null)
|
||
|
||
if (validResults.length === 0) {
|
||
return { screenshots: [], analysis: null }
|
||
}
|
||
|
||
// Get the primary timeframe (1h) as base
|
||
const primaryResult = validResults.find(r => r.timeframe === '1h') || validResults[0]
|
||
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
|
||
|
||
// Calculate weighted confidence based on timeframe alignment
|
||
const alignment = this.calculateTimeframeAlignment(validResults)
|
||
const baseAnalysis = primaryResult.analysis!
|
||
|
||
// Adjust confidence based on timeframe alignment
|
||
const adjustedConfidence = Math.round(baseAnalysis.confidence * alignment.score)
|
||
|
||
// Create combined analysis with multi-timeframe reasoning
|
||
const combinedAnalysis: AnalysisResult = {
|
||
...baseAnalysis,
|
||
confidence: adjustedConfidence,
|
||
reasoning: `Multi-timeframe Dual-Layout Analysis (${results.map(r => r.timeframe).join(', ')}): ${baseAnalysis.reasoning}
|
||
|
||
📊 Each timeframe analyzed with BOTH AI layout (RSI, MACD, EMAs) and DIY layout (Stochastic RSI, VWAP, OBV)
|
||
⏱️ Timeframe Alignment: ${alignment.description}
|
||
<EFBFBD> Signal Strength: ${alignment.strength}
|
||
🎯 Confidence Adjustment: ${baseAnalysis.confidence}% → ${adjustedConfidence}% (${alignment.score >= 1 ? 'Enhanced' : 'Reduced'} due to timeframe ${alignment.score >= 1 ? 'alignment' : 'divergence'})
|
||
|
||
🔬 Analysis Details:
|
||
${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.analysis?.confidence}% confidence)`).join('\n')}`,
|
||
|
||
keyLevels: this.consolidateKeyLevels(validResults),
|
||
marketSentiment: this.consolidateMarketSentiment(validResults)
|
||
}
|
||
|
||
return {
|
||
screenshots,
|
||
analysis: combinedAnalysis
|
||
}
|
||
}
|
||
|
||
private calculateTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
score: number
|
||
description: string
|
||
strength: string
|
||
} {
|
||
const recommendations = results.map(r => r.analysis?.recommendation).filter(Boolean)
|
||
const buySignals = recommendations.filter(r => r === 'BUY').length
|
||
const sellSignals = recommendations.filter(r => r === 'SELL').length
|
||
const holdSignals = recommendations.filter(r => r === 'HOLD').length
|
||
|
||
const total = recommendations.length
|
||
const maxSignals = Math.max(buySignals, sellSignals, holdSignals)
|
||
const alignmentRatio = maxSignals / total
|
||
|
||
let score = 1.0
|
||
let description = ''
|
||
let strength = ''
|
||
|
||
if (alignmentRatio >= 0.75) {
|
||
score = 1.2 // Boost confidence
|
||
description = `Strong alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Strong'
|
||
} else if (alignmentRatio >= 0.5) {
|
||
score = 1.0 // Neutral
|
||
description = `Moderate alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Moderate'
|
||
} else {
|
||
score = 0.8 // Reduce confidence
|
||
description = `Weak alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Weak'
|
||
}
|
||
|
||
return { score, description, strength }
|
||
}
|
||
|
||
private consolidateKeyLevels(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): any {
|
||
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
|
||
if (allLevels.length === 0) return {}
|
||
|
||
// Use the 1h timeframe levels as primary, or first available
|
||
const primaryLevels = results.find(r => r.timeframe === '1h')?.analysis?.keyLevels || allLevels[0]
|
||
|
||
return {
|
||
...primaryLevels,
|
||
note: `Consolidated from ${allLevels.length} timeframes`
|
||
}
|
||
}
|
||
|
||
private consolidateMarketSentiment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): 'BULLISH' | 'BEARISH' | 'NEUTRAL' {
|
||
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
|
||
if (sentiments.length === 0) return 'NEUTRAL'
|
||
|
||
// Use the 1h timeframe sentiment as primary, or first available
|
||
const primarySentiment = results.find(r => r.timeframe === '1h')?.analysis?.marketSentiment || sentiments[0]
|
||
|
||
return primarySentiment || 'NEUTRAL'
|
||
}
|
||
|
||
private analyzeTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): string {
|
||
const recommendations = results.map(r => ({
|
||
timeframe: r.timeframe,
|
||
recommendation: r.analysis?.recommendation,
|
||
confidence: r.analysis?.confidence || 0
|
||
}))
|
||
|
||
const summary = recommendations.map(r => `${r.timeframe}: ${r.recommendation} (${r.confidence}%)`).join(', ')
|
||
return summary
|
||
}
|
||
|
||
private async storeAnalysisForLearning(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
await prisma.aILearningData.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
screenshot: result.screenshots[0] || '',
|
||
analysisData: JSON.stringify(result.analysis),
|
||
marketConditions: JSON.stringify({
|
||
marketSentiment: result.analysis.marketSentiment,
|
||
keyLevels: result.analysis.keyLevels,
|
||
timestamp: new Date().toISOString()
|
||
}),
|
||
confidenceScore: result.analysis.confidence,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('Error storing analysis for learning:', error)
|
||
}
|
||
}
|
||
|
||
private async updateSessionWithAnalysis(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
// Store the analysis decision in a field that works for now
|
||
const analysisDecision = `${result.analysis.recommendation} with ${result.analysis.confidence}% confidence - ${result.analysis.summary}`
|
||
|
||
// Update the current session with the latest analysis
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
lastAnalysis: new Date(),
|
||
lastError: analysisDecision // Temporarily store analysis here
|
||
}
|
||
})
|
||
|
||
// Also log the analysis for debugging
|
||
console.log('📊 Analysis stored in database:', {
|
||
recommendation: result.analysis.recommendation,
|
||
confidence: result.analysis.confidence,
|
||
summary: result.analysis.summary
|
||
})
|
||
} catch (error) {
|
||
console.error('Failed to update session with analysis:', error)
|
||
}
|
||
}
|
||
|
||
private async makeTradeDecision(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<any | null> {
|
||
try {
|
||
const analysis = result.analysis
|
||
if (!analysis) return null
|
||
|
||
// Only trade if confidence is high enough
|
||
if (analysis.confidence < 70) {
|
||
console.log(`📊 Confidence too low: ${analysis.confidence}%`)
|
||
return null
|
||
}
|
||
|
||
// Only trade if direction is clear
|
||
if (analysis.recommendation === 'HOLD') {
|
||
console.log('📊 No clear direction signal')
|
||
return null
|
||
}
|
||
|
||
// Calculate position size based on risk percentage
|
||
const positionSize = this.calculatePositionSize(analysis)
|
||
|
||
return {
|
||
direction: analysis.recommendation,
|
||
confidence: analysis.confidence,
|
||
positionSize,
|
||
stopLoss: this.calculateStopLoss(analysis),
|
||
takeProfit: this.calculateTakeProfit(analysis),
|
||
marketSentiment: analysis.marketSentiment
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error making trade decision:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private calculatePositionSize(analysis: any): number {
|
||
const baseAmount = this.config!.tradingAmount
|
||
const riskAdjustment = this.config!.riskPercentage / 100
|
||
const confidenceAdjustment = analysis.confidence / 100
|
||
|
||
return baseAmount * riskAdjustment * confidenceAdjustment
|
||
}
|
||
|
||
private calculateStopLoss(analysis: any): number {
|
||
// Use AI analysis stopLoss if available, otherwise calculate from entry price
|
||
if (analysis.stopLoss?.price) {
|
||
return analysis.stopLoss.price
|
||
}
|
||
|
||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
||
const stopLossPercent = this.config!.stopLossPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 - stopLossPercent)
|
||
} else {
|
||
return currentPrice * (1 + stopLossPercent)
|
||
}
|
||
}
|
||
|
||
private calculateTakeProfit(analysis: any): number {
|
||
// Use AI analysis takeProfit if available, otherwise calculate from entry price
|
||
if (analysis.takeProfits?.tp1?.price) {
|
||
return analysis.takeProfits.tp1.price
|
||
}
|
||
|
||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
||
const takeProfitPercent = this.config!.takeProfitPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 + takeProfitPercent)
|
||
} else {
|
||
return currentPrice * (1 - takeProfitPercent)
|
||
}
|
||
}
|
||
|
||
private async executeTrade(decision: any): Promise<void> {
|
||
try {
|
||
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
|
||
|
||
let tradeResult: any
|
||
|
||
if (this.config!.mode === 'SIMULATION') {
|
||
// Execute simulation trade
|
||
tradeResult = await this.executeSimulationTrade(decision)
|
||
} else {
|
||
// Execute live trade via Jupiter
|
||
tradeResult = await this.executeLiveTrade(decision)
|
||
}
|
||
|
||
// Store trade in database
|
||
await this.storeTrade(decision, tradeResult)
|
||
|
||
// Update stats
|
||
this.updateStats(tradeResult)
|
||
|
||
console.log(`✅ Trade executed successfully: ${tradeResult.transactionId || 'SIMULATION'}`)
|
||
|
||
} catch (error) {
|
||
console.error('Error executing trade:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Trade execution failed'
|
||
}
|
||
}
|
||
|
||
private async executeSimulationTrade(decision: any): Promise<any> {
|
||
// Simulate trade execution with realistic parameters
|
||
const currentPrice = decision.currentPrice || 100 // Mock price
|
||
const slippage = Math.random() * 0.005 // 0-0.5% slippage
|
||
const executionPrice = currentPrice * (1 + (Math.random() > 0.5 ? slippage : -slippage))
|
||
|
||
return {
|
||
transactionId: `SIM_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||
executionPrice,
|
||
amount: decision.positionSize,
|
||
direction: decision.direction,
|
||
status: 'COMPLETED',
|
||
timestamp: new Date(),
|
||
fees: decision.positionSize * 0.001, // 0.1% fee
|
||
slippage: slippage * 100
|
||
}
|
||
}
|
||
|
||
private async executeLiveTrade(decision: any): Promise<any> {
|
||
// Execute real trade via Jupiter DEX
|
||
const inputToken = decision.direction === 'BUY' ? 'USDC' : 'SOL'
|
||
const outputToken = decision.direction === 'BUY' ? 'SOL' : 'USDC'
|
||
|
||
const tokens = {
|
||
SOL: 'So11111111111111111111111111111111111111112',
|
||
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||
}
|
||
|
||
return await jupiterDEXService.executeSwap(
|
||
tokens[inputToken as keyof typeof tokens],
|
||
tokens[outputToken as keyof typeof tokens],
|
||
decision.positionSize,
|
||
50 // 0.5% slippage
|
||
)
|
||
}
|
||
|
||
private async storeTrade(decision: any, result: any): Promise<void> {
|
||
try {
|
||
await prisma.trade.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
side: decision.direction,
|
||
amount: decision.positionSize,
|
||
price: result.executionPrice,
|
||
status: result.status,
|
||
driftTxId: result.transactionId || result.txId,
|
||
fees: result.fees || 0,
|
||
stopLoss: decision.stopLoss,
|
||
takeProfit: decision.takeProfit,
|
||
isAutomated: true,
|
||
tradingMode: this.config!.mode,
|
||
confidence: decision.confidence,
|
||
marketSentiment: decision.marketSentiment,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('Error storing trade:', error)
|
||
}
|
||
}
|
||
|
||
private updateStats(result: any): void {
|
||
this.stats.totalTrades++
|
||
|
||
if (result.status === 'COMPLETED') {
|
||
this.stats.successfulTrades++
|
||
this.stats.winRate = (this.stats.successfulTrades / this.stats.totalTrades) * 100
|
||
|
||
// Update PnL (simplified calculation)
|
||
const pnl = result.amount * 0.01 * (Math.random() > 0.5 ? 1 : -1) // Random PnL for demo
|
||
this.stats.totalPnL += pnl
|
||
}
|
||
}
|
||
|
||
private async getTodayTradeCount(userId: string): Promise<number> {
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
const count = await prisma.trade.count({
|
||
where: {
|
||
userId,
|
||
isAutomated: true,
|
||
createdAt: {
|
||
gte: today
|
||
}
|
||
}
|
||
})
|
||
|
||
return count
|
||
}
|
||
|
||
async stopAutomation(): Promise<boolean> {
|
||
try {
|
||
this.isRunning = false
|
||
|
||
// Clear the interval if it exists
|
||
if (this.intervalId) {
|
||
clearInterval(this.intervalId)
|
||
this.intervalId = null
|
||
}
|
||
|
||
// Update database session status to STOPPED
|
||
if (this.config) {
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config.userId,
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
status: 'STOPPED',
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
}
|
||
|
||
this.config = null
|
||
|
||
console.log('🛑 Automation stopped')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to stop automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async pauseAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.isRunning) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = false
|
||
console.log('⏸️ Automation paused')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to pause automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async resumeAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.config) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = true
|
||
console.log('▶️ Automation resumed')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to resume automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getStatus(): Promise<AutomationStatus | null> {
|
||
try {
|
||
// Get the latest active automation session from database first
|
||
const session = await prisma.automationSession.findFirst({
|
||
where: { status: 'ACTIVE' },
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
if (!session) {
|
||
return null
|
||
}
|
||
|
||
// If we have a session but automation is not running in memory,
|
||
// it means the server was restarted but the session is still active
|
||
const isActiveInMemory = this.isRunning && this.config !== null
|
||
|
||
// Auto-restart automation if session exists but not running in memory
|
||
if (!isActiveInMemory) {
|
||
console.log('🔄 Found active session but automation not running, attempting auto-restart...')
|
||
await this.autoRestartFromSession(session)
|
||
}
|
||
|
||
return {
|
||
isActive: this.isRunning && this.config !== null,
|
||
mode: session.mode as 'SIMULATION' | 'LIVE',
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
totalTrades: session.totalTrades,
|
||
successfulTrades: session.successfulTrades,
|
||
winRate: session.winRate,
|
||
totalPnL: session.totalPnL,
|
||
errorCount: session.errorCount,
|
||
lastError: session.lastError || undefined,
|
||
lastAnalysis: session.lastAnalysis || undefined,
|
||
lastTrade: session.lastTrade || undefined,
|
||
nextScheduled: session.nextScheduled || undefined
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get automation status:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async autoRestartFromSession(session: any): Promise<void> {
|
||
try {
|
||
const settings = session.settings || {}
|
||
const config: AutomationConfig = {
|
||
userId: session.userId,
|
||
mode: session.mode,
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
tradingAmount: settings.tradingAmount || 100,
|
||
maxLeverage: settings.maxLeverage || 3,
|
||
stopLossPercent: settings.stopLossPercent || 2,
|
||
takeProfitPercent: settings.takeProfitPercent || 6,
|
||
maxDailyTrades: settings.maxDailyTrades || 5,
|
||
riskPercentage: settings.riskPercentage || 2
|
||
}
|
||
|
||
await this.startAutomation(config)
|
||
console.log('✅ Automation auto-restarted successfully')
|
||
} catch (error) {
|
||
console.error('Failed to auto-restart automation:', error)
|
||
}
|
||
}
|
||
|
||
async getLearningInsights(userId: string): Promise<{
|
||
totalAnalyses: number
|
||
avgAccuracy: number
|
||
bestTimeframe: string
|
||
worstTimeframe: string
|
||
commonFailures: string[]
|
||
recommendations: string[]
|
||
}> {
|
||
try {
|
||
// For now, return mock data
|
||
return {
|
||
totalAnalyses: 150,
|
||
avgAccuracy: 0.72,
|
||
bestTimeframe: '1h',
|
||
worstTimeframe: '15m',
|
||
commonFailures: [
|
||
'Low confidence predictions',
|
||
'Missed support/resistance levels',
|
||
'Timeframe misalignment'
|
||
],
|
||
recommendations: [
|
||
'Focus on 1h timeframe for better accuracy',
|
||
'Wait for higher confidence signals (>75%)',
|
||
'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()
|