- Remove Jupiter DEX import and dependencies from automation-service-simple.ts - Replace executeLiveTrade method to use Drift Protocol via /api/automation/trade - Add dexProvider field to AutomationConfig interface - Update AI risk management to use calculateAIStopLoss/calculateAITakeProfit methods - Fix all Jupiter references to use Drift Protocol instead - Ensure automation uses proper Drift leverage trading instead of Jupiter spot trading - Route trades through unified API that defaults to DRIFT provider This resolves the issue where automation was incorrectly using Jupiter DEX instead of the configured Drift Protocol for leveraged trading.
1344 lines
49 KiB
TypeScript
1344 lines
49 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
||
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
||
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
||
import { TradingViewCredentials } from './tradingview-automation'
|
||
import { progressTracker, ProgressStatus } from './progress-tracker'
|
||
import aggressiveCleanup from './aggressive-cleanup'
|
||
import { analysisCompletionFlag } from './analysis-completion-flag'
|
||
import priceMonitorService from './price-monitor'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
export interface AutomationConfig {
|
||
userId: string
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
selectedTimeframes?: string[] // Multi-timeframe support from UI
|
||
tradingAmount: number
|
||
maxLeverage: number
|
||
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||
maxDailyTrades: number
|
||
riskPercentage: number
|
||
dexProvider?: string // DEX provider (DRIFT or JUPITER)
|
||
}
|
||
|
||
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 and takeProfitPercent removed - AI calculates these automatically
|
||
maxDailyTrades: config.maxDailyTrades,
|
||
riskPercentage: config.riskPercentage
|
||
},
|
||
startBalance: config.tradingAmount,
|
||
currentBalance: config.tradingAmount,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Start automation cycle
|
||
this.startAutomationCycle()
|
||
|
||
// Start price monitoring
|
||
await priceMonitorService.startMonitoring()
|
||
|
||
// Set up price monitor event listeners for automatic analysis triggering
|
||
priceMonitorService.on('tp_approach', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`🎯 TP approach detected for ${data.symbol}, triggering analysis...`)
|
||
await this.triggerPriceBasedAnalysis('TP_APPROACH', data)
|
||
}
|
||
})
|
||
|
||
priceMonitorService.on('sl_approach', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`⚠️ SL approach detected for ${data.symbol}, triggering analysis...`)
|
||
await this.triggerPriceBasedAnalysis('SL_APPROACH', data)
|
||
}
|
||
})
|
||
|
||
priceMonitorService.on('critical_level', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`🚨 Critical level reached for ${data.symbol}, triggering urgent analysis...`)
|
||
await this.triggerPriceBasedAnalysis('CRITICAL', data)
|
||
}
|
||
})
|
||
|
||
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 {
|
||
// Use the new post-analysis cleanup that respects completion flags
|
||
await aggressiveCleanup.runPostAnalysisCleanup()
|
||
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)}`
|
||
|
||
// Mark the start of analysis cycle to prevent cleanup interruption
|
||
analysisCompletionFlag.startAnalysisCycle(sessionId)
|
||
|
||
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...')
|
||
|
||
// Use selected timeframes from UI, fallback to default if not provided
|
||
const timeframes = this.config!.selectedTimeframes || ['1h']
|
||
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)
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(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)
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(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)
|
||
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
|
||
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)
|
||
|
||
// Mark analysis as complete even on error to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
|
||
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 (first selected or default) as base
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primaryResult = validResults.find(r => r.timeframe === primaryTimeframe) || 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 primary timeframe levels (first selected) as primary, or first available
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primaryLevels = results.find(r => r.timeframe === primaryTimeframe)?.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 primary timeframe sentiment (first selected) as primary, or first available
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primarySentiment = results.find(r => r.timeframe === primaryTimeframe)?.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
|
||
}
|
||
|
||
// ✅ ENHANCED: Support both BUY and SELL signals
|
||
if (analysis.recommendation === 'SELL') {
|
||
// Check if we have SOL position to sell
|
||
const hasPosition = await this.checkCurrentPosition()
|
||
if (!hasPosition) {
|
||
console.log('📊 SELL signal but no SOL position to sell - skipping')
|
||
return null
|
||
}
|
||
console.log('📉 SELL signal detected with existing SOL position')
|
||
} else if (analysis.recommendation === 'BUY') {
|
||
console.log('📈 BUY signal detected')
|
||
}
|
||
|
||
// Calculate position size based on risk percentage
|
||
const positionSize = await this.calculatePositionSize(analysis)
|
||
|
||
return {
|
||
direction: analysis.recommendation,
|
||
confidence: analysis.confidence,
|
||
positionSize,
|
||
stopLoss: this.calculateStopLoss(analysis),
|
||
takeProfit: this.calculateTakeProfit(analysis),
|
||
marketSentiment: analysis.marketSentiment,
|
||
currentPrice: analysis.entry?.price || 190 // Store current price for calculations
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error making trade decision:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// ✅ NEW: Check if we have SOL position available to sell
|
||
private async checkCurrentPosition(): Promise<boolean> {
|
||
try {
|
||
// Check recent trades to see current position
|
||
const recentTrades = await prisma.trade.findMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
status: 'OPEN'
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 5
|
||
})
|
||
|
||
// Count open positions
|
||
let netPosition = 0
|
||
for (const trade of recentTrades) {
|
||
if (trade.side === 'BUY') {
|
||
netPosition += trade.amount
|
||
} else if (trade.side === 'SELL') {
|
||
netPosition -= trade.amount
|
||
}
|
||
}
|
||
|
||
console.log(`🔍 Current SOL position: ${netPosition.toFixed(4)} SOL`)
|
||
return netPosition > 0.001 // Have at least 0.001 SOL to sell
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error checking current position:', error)
|
||
// If we can't check, default to allowing the trade (fail-safe)
|
||
return true
|
||
}
|
||
}
|
||
|
||
private async calculatePositionSize(analysis: any): Promise<number> {
|
||
const baseAmount = this.config!.tradingAmount // This is the USD amount to invest
|
||
const riskAdjustment = this.config!.riskPercentage / 100
|
||
const confidenceAdjustment = analysis.confidence / 100
|
||
|
||
// ✅ ENHANCED: Handle both BUY and SELL position sizing
|
||
if (analysis.recommendation === 'SELL') {
|
||
// For SELL orders, calculate how much SOL to sell based on current holdings
|
||
return await this.calculateSellAmount(analysis)
|
||
}
|
||
|
||
// For BUY orders, calculate USD amount to invest
|
||
const usdAmount = baseAmount * riskAdjustment * confidenceAdjustment
|
||
|
||
// Get current price to convert USD to token amount
|
||
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
||
|
||
if (!currentPrice) {
|
||
try {
|
||
const { default: PriceFetcher } = await import('./price-fetcher')
|
||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||
console.log(`📊 Using current ${this.config?.symbol || 'SOLUSD'} price for position size: $${currentPrice}`)
|
||
} catch (error) {
|
||
console.error('Error fetching price for position size, using fallback:', error)
|
||
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
||
}
|
||
}
|
||
|
||
// Calculate token amount: USD investment / token price
|
||
const tokenAmount = usdAmount / currentPrice
|
||
console.log(`💰 BUY Position calculation: $${usdAmount} ÷ $${currentPrice} = ${tokenAmount.toFixed(4)} tokens`)
|
||
|
||
return tokenAmount
|
||
}
|
||
|
||
// ✅ NEW: Calculate SOL amount to sell for SELL orders
|
||
private async calculateSellAmount(analysis: any): Promise<number> {
|
||
try {
|
||
// Get current SOL holdings from recent open trades
|
||
const openTrades = await prisma.trade.findMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
status: 'OPEN',
|
||
side: 'BUY' // Only BUY trades represent SOL holdings
|
||
},
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
let totalSOLHoldings = 0
|
||
for (const trade of openTrades) {
|
||
totalSOLHoldings += trade.amount
|
||
}
|
||
|
||
// Risk-adjusted sell amount (don't sell everything at once)
|
||
const riskAdjustment = this.config!.riskPercentage / 100
|
||
const confidenceAdjustment = analysis.confidence / 100
|
||
const sellAmount = totalSOLHoldings * riskAdjustment * confidenceAdjustment
|
||
|
||
console.log(`💰 SELL Position calculation: ${totalSOLHoldings.toFixed(4)} SOL holdings × ${(riskAdjustment * confidenceAdjustment * 100).toFixed(1)}% = ${sellAmount.toFixed(4)} SOL to sell`)
|
||
|
||
return Math.max(sellAmount, 0.001) // Minimum 0.001 SOL
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error calculating sell amount:', error)
|
||
return 0.01 // Fallback: sell 0.01 SOL
|
||
}
|
||
}
|
||
|
||
private calculateStopLoss(analysis: any): number {
|
||
// ✅ AI-FIRST: Use AI analysis stopLoss if available
|
||
if (analysis.stopLoss?.price) {
|
||
return analysis.stopLoss.price
|
||
}
|
||
|
||
// If AI provides explicit stop loss percentage, use it
|
||
if (analysis.stopLossPercent) {
|
||
const currentPrice = analysis.entry?.price || 189
|
||
const stopLossPercent = analysis.stopLossPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 - stopLossPercent)
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return currentPrice * (1 + stopLossPercent)
|
||
}
|
||
}
|
||
|
||
// Fallback: Dynamic stop loss based on market volatility (AI-calculated)
|
||
const currentPrice = analysis.entry?.price || 189
|
||
// AI determines volatility-based stop loss (0.5% to 2% range)
|
||
const aiStopLossPercent = this.calculateAIStopLoss(analysis) / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 - aiStopLossPercent)
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return currentPrice * (1 + aiStopLossPercent)
|
||
} else {
|
||
return currentPrice * (1 - aiStopLossPercent)
|
||
}
|
||
}
|
||
|
||
private calculateTakeProfit(analysis: any): number {
|
||
// ✅ AI-FIRST: Use AI analysis takeProfit if available
|
||
if (analysis.takeProfits?.tp1?.price) {
|
||
return analysis.takeProfits.tp1.price
|
||
}
|
||
|
||
// If AI provides explicit take profit percentage, use it
|
||
if (analysis.takeProfitPercent) {
|
||
const currentPrice = analysis.entry?.price || 150
|
||
const takeProfitPercent = analysis.takeProfitPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 + takeProfitPercent)
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return currentPrice * (1 - takeProfitPercent)
|
||
}
|
||
}
|
||
|
||
// Fallback: Dynamic take profit based on AI risk/reward optimization
|
||
const currentPrice = analysis.entry?.price || 150
|
||
const aiTakeProfitPercent = this.calculateAITakeProfit(analysis) / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 + aiTakeProfitPercent)
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return currentPrice * (1 - aiTakeProfitPercent)
|
||
} else {
|
||
return currentPrice * (1 + aiTakeProfitPercent)
|
||
}
|
||
}
|
||
|
||
// AI-calculated dynamic stop loss based on volatility and market conditions
|
||
private calculateAIStopLoss(analysis: any): number {
|
||
// Extract confidence and market sentiment for adaptive stop loss
|
||
const confidence = analysis.confidence || 70
|
||
const volatility = analysis.marketConditions?.volatility || 'MEDIUM'
|
||
|
||
// Base stop loss percentages (proven to work from our testing)
|
||
let baseStopLoss = 0.8 // 0.8% base (proven effective)
|
||
|
||
// Adjust based on volatility
|
||
if (volatility === 'HIGH') {
|
||
baseStopLoss = 1.2 // Wider stop loss for high volatility
|
||
} else if (volatility === 'LOW') {
|
||
baseStopLoss = 0.5 // Tighter stop loss for low volatility
|
||
}
|
||
|
||
// Adjust based on confidence (higher confidence = tighter stop loss)
|
||
if (confidence > 85) {
|
||
baseStopLoss *= 0.8 // 20% tighter for high confidence
|
||
} else if (confidence < 70) {
|
||
baseStopLoss *= 1.3 // 30% wider for low confidence
|
||
}
|
||
|
||
return Math.max(0.3, Math.min(2.0, baseStopLoss)) // Cap between 0.3% and 2%
|
||
}
|
||
|
||
// AI-calculated dynamic take profit based on market conditions and risk/reward
|
||
private calculateAITakeProfit(analysis: any): number {
|
||
const stopLossPercent = this.calculateAIStopLoss(analysis)
|
||
const confidence = analysis.confidence || 70
|
||
|
||
// Target minimum 1.5:1 risk/reward ratio, scaled by confidence
|
||
let baseRiskReward = 1.5
|
||
|
||
if (confidence > 85) {
|
||
baseRiskReward = 2.0 // Higher reward target for high confidence
|
||
} else if (confidence < 70) {
|
||
baseRiskReward = 1.2 // Lower reward target for low confidence
|
||
}
|
||
|
||
const takeProfitPercent = stopLossPercent * baseRiskReward
|
||
return Math.max(0.5, Math.min(5.0, takeProfitPercent)) // Cap between 0.5% and 5%
|
||
}
|
||
|
||
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 Drift Protocol
|
||
console.log(`💰 LIVE TRADE: $${this.config!.tradingAmount} trading amount configured`)
|
||
tradeResult = await this.executeLiveTrade(decision)
|
||
|
||
// If live trade failed, fall back to simulation for data consistency
|
||
if (!tradeResult || !tradeResult.success) {
|
||
console.log('⚠️ Live trade failed, falling back to simulation for record keeping')
|
||
tradeResult = await this.executeSimulationTrade(decision)
|
||
tradeResult.status = 'FAILED'
|
||
tradeResult.error = 'Drift Protocol execution failed'
|
||
}
|
||
}
|
||
|
||
// Store trade in database
|
||
await this.storeTrade(decision, tradeResult)
|
||
|
||
// Update stats
|
||
this.updateStats(tradeResult)
|
||
|
||
console.log(`✅ Trade executed successfully: ${tradeResult.transactionId || 'SIMULATION'}`)
|
||
|
||
// Force cleanup after successful trade execution
|
||
if (tradeResult.status !== 'FAILED') {
|
||
setTimeout(async () => {
|
||
try {
|
||
await aggressiveCleanup.forceCleanupAfterTrade()
|
||
} catch (error) {
|
||
console.error('Error in post-trade cleanup:', error)
|
||
}
|
||
}, 2000) // 2 second delay
|
||
}
|
||
|
||
} 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
|
||
let currentPrice = decision.currentPrice
|
||
|
||
// If no current price provided, fetch real price
|
||
if (!currentPrice) {
|
||
try {
|
||
const { default: PriceFetcher } = await import('./price-fetcher')
|
||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||
console.log(`📊 Fetched real ${this.config?.symbol || 'SOLUSD'} price: $${currentPrice}`)
|
||
} catch (error) {
|
||
console.error('Error fetching real price, using fallback:', error)
|
||
// Use a more realistic fallback based on symbol
|
||
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
||
}
|
||
}
|
||
|
||
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: 'OPEN', // Trades start as OPEN, not 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 Drift Protocol
|
||
console.log(`🌊 Executing Drift trade: ${decision.direction} ${this.config!.symbol}`)
|
||
|
||
// Calculate AI-generated stop loss and take profit from analysis
|
||
const stopLossPercent = decision.stopLoss || this.calculateAIStopLoss(decision)
|
||
const takeProfitPercent = decision.takeProfit || this.calculateAITakeProfit(decision)
|
||
|
||
console.log(`🎯 AI Risk Management: SL=${stopLossPercent}%, TP=${takeProfitPercent}%`)
|
||
|
||
// Call the unified trading API endpoint that routes to Drift
|
||
const tradeResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/automation/trade`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
dexProvider: this.config!.dexProvider || 'DRIFT',
|
||
action: 'place_order',
|
||
symbol: this.config!.symbol,
|
||
amount: this.config!.tradingAmount,
|
||
side: decision.direction.toLowerCase(),
|
||
leverage: this.config!.maxLeverage || 2,
|
||
stopLoss: true,
|
||
takeProfit: true,
|
||
stopLossPercent: stopLossPercent,
|
||
takeProfitPercent: takeProfitPercent,
|
||
mode: this.config!.mode || 'SIMULATION'
|
||
})
|
||
})
|
||
|
||
const tradeResult = await tradeResponse.json()
|
||
|
||
// Convert Drift result to standard trade result format
|
||
if (tradeResult.success) {
|
||
return {
|
||
transactionId: tradeResult.result?.transactionId || tradeResult.result?.txId,
|
||
executionPrice: tradeResult.result?.executionPrice,
|
||
amount: tradeResult.result?.amount,
|
||
direction: decision.direction,
|
||
status: 'COMPLETED',
|
||
timestamp: new Date(),
|
||
leverage: tradeResult.leverageUsed || this.config!.maxLeverage,
|
||
stopLoss: stopLossPercent,
|
||
takeProfit: takeProfitPercent,
|
||
tradingAmount: this.config!.tradingAmount,
|
||
dexProvider: 'DRIFT'
|
||
}
|
||
} else {
|
||
throw new Error(tradeResult.error || 'Drift trade execution failed')
|
||
}
|
||
}
|
||
|
||
private async storeTrade(decision: any, result: any): Promise<void> {
|
||
try {
|
||
// Ensure we have a valid price for database storage
|
||
const executionPrice = result.executionPrice || decision.currentPrice || decision.entryPrice
|
||
|
||
if (!executionPrice) {
|
||
console.error('❌ No valid price available for trade storage. Result:', result)
|
||
console.error('❌ Decision data:', { currentPrice: decision.currentPrice, entryPrice: decision.entryPrice })
|
||
return
|
||
}
|
||
|
||
// For live trades, use the actual amounts from Drift
|
||
const tradeAmount = result.tradingAmount ? this.config!.tradingAmount : decision.positionSize
|
||
const actualAmount = result.amount || decision.positionSize
|
||
|
||
console.log(`💾 Storing trade: ${decision.direction} ${actualAmount} ${this.config!.symbol} at $${executionPrice}`)
|
||
|
||
await prisma.trade.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
side: decision.direction,
|
||
amount: actualAmount,
|
||
price: executionPrice,
|
||
status: result.status || 'COMPLETED',
|
||
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(),
|
||
// Add Drift-specific fields for live trades
|
||
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
|
||
realTradingAmount: this.config!.tradingAmount,
|
||
leverage: result.leverage,
|
||
driftTxId: result.transactionId
|
||
})
|
||
}
|
||
})
|
||
|
||
console.log('✅ Trade stored in database successfully')
|
||
} 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
|
||
}
|
||
|
||
// Stop price monitoring
|
||
try {
|
||
await priceMonitorService.stopMonitoring()
|
||
console.log('📊 Price monitoring stopped')
|
||
} catch (error) {
|
||
console.error('Failed to stop price monitoring:', error)
|
||
}
|
||
|
||
// 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 and takeProfitPercent removed - AI calculates these automatically
|
||
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 with dynamic timeframe
|
||
const selectedTimeframes = this.config?.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
|
||
return {
|
||
totalAnalyses: 150,
|
||
avgAccuracy: 0.72,
|
||
bestTimeframe: primaryTimeframe,
|
||
worstTimeframe: '15m',
|
||
commonFailures: [
|
||
'Low confidence predictions',
|
||
'Missed support/resistance levels',
|
||
'Timeframe misalignment'
|
||
],
|
||
recommendations: [
|
||
`Focus on ${primaryTimeframe} 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: []
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Trigger analysis based on price movement alerts
|
||
*/
|
||
private async triggerPriceBasedAnalysis(
|
||
trigger: 'TP_APPROACH' | 'SL_APPROACH' | 'CRITICAL',
|
||
data: any
|
||
): Promise<void> {
|
||
if (!this.config || !this.isRunning) {
|
||
console.log('❌ Cannot trigger price-based analysis: automation not running')
|
||
return
|
||
}
|
||
|
||
const sessionId = `price-trigger-${Date.now()}`
|
||
|
||
try {
|
||
console.log(`🔥 Price-based analysis triggered by ${trigger} for ${data.symbol}`)
|
||
console.log(`📊 Current price: $${data.currentPrice}, Target: $${data.targetPrice}`)
|
||
|
||
// Create progress tracker for this analysis
|
||
const steps = [
|
||
{ id: 'trigger', title: 'Triggered by price movement', description: 'Analysis initiated by price alert', status: 'pending' as ProgressStatus },
|
||
{ id: 'screenshot', title: 'Capturing screenshots', description: 'Taking fresh market screenshots', status: 'pending' as ProgressStatus },
|
||
{ id: 'analysis', title: 'Running AI analysis', description: 'Analyzing current market conditions', status: 'pending' as ProgressStatus },
|
||
{ id: 'evaluation', title: 'Evaluating position', description: 'Checking position adjustments', status: 'pending' as ProgressStatus },
|
||
{ id: 'complete', title: 'Analysis complete', description: 'Price-based analysis finished', status: 'pending' as ProgressStatus }
|
||
]
|
||
|
||
progressTracker.createSession(sessionId, steps)
|
||
|
||
progressTracker.updateStep(sessionId, 'trigger', 'active', `${trigger}: ${data.symbol} at $${data.currentPrice}`)
|
||
|
||
// Run enhanced screenshot capture with current symbol/timeframe
|
||
progressTracker.updateStep(sessionId, 'screenshot', 'active')
|
||
|
||
const screenshotConfig = {
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
layouts: ['ai', 'diy'],
|
||
sessionId
|
||
}
|
||
|
||
const screenshots = await enhancedScreenshotService.captureWithLogin(screenshotConfig)
|
||
|
||
if (!screenshots || screenshots.length === 0) {
|
||
throw new Error('Failed to capture screenshots for price-based analysis')
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'screenshot', 'completed', `Captured ${screenshots.length} screenshots`)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'active')
|
||
|
||
// Simplified analysis call - just use the first screenshot
|
||
const analysisResult = await aiAnalysisService.analyzeScreenshot(screenshots[0])
|
||
|
||
if (!analysisResult) {
|
||
throw new Error('AI analysis returned null result')
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'analysis', 'completed', `Analysis: ${analysisResult.recommendation}`)
|
||
progressTracker.updateStep(sessionId, 'evaluation', 'active')
|
||
|
||
// Store the triggered analysis in trading journal
|
||
await prisma.tradingJournal.create({
|
||
data: {
|
||
userId: this.config.userId,
|
||
screenshotUrl: screenshots[0] || '',
|
||
aiAnalysis: analysisResult.reasoning || 'No analysis available',
|
||
confidence: analysisResult.confidence || 0,
|
||
recommendation: analysisResult.recommendation || 'HOLD',
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
sessionId,
|
||
notes: `Price-triggered analysis: ${trigger} - Current: $${data.currentPrice}, Target: $${data.targetPrice}`,
|
||
marketSentiment: analysisResult.marketSentiment || 'Unknown',
|
||
tradingMode: this.config.mode,
|
||
isAutomated: true,
|
||
priceAtAnalysis: data.currentPrice,
|
||
marketCondition: trigger,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Log important insights for potential position adjustments
|
||
if (analysisResult.recommendation === 'SELL' && trigger === 'SL_APPROACH') {
|
||
console.log('⚠️ AI recommends SELL while approaching Stop Loss - consider early exit')
|
||
} else if (analysisResult.recommendation === 'BUY' && trigger === 'TP_APPROACH') {
|
||
console.log('🎯 AI recommends BUY while approaching Take Profit - consider extending position')
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'evaluation', 'completed')
|
||
progressTracker.updateStep(sessionId, 'complete', 'completed',
|
||
`${analysisResult.recommendation} (${analysisResult.confidence}% confidence)`)
|
||
|
||
console.log(`✅ Price-based analysis completed (${trigger}): ${analysisResult.recommendation} with ${analysisResult.confidence}% confidence`)
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Price-based analysis failed (${trigger}):`, error)
|
||
|
||
progressTracker.updateStep(sessionId, 'complete', 'error',
|
||
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
}
|
||
}
|
||
}
|
||
|
||
export const automationService = new AutomationService()
|