Files
trading_bot_v3/lib/automation-service-simple.ts.backup
mindesbunister 1505bc04cd fix: add null checks to prevent 'Cannot read properties of null' error on automation page
- Added proper null checks for status object before accessing selectedTimeframes
- Fixed timeframes display to handle null status gracefully
- Fixed analysis interval calculation with optional chaining
- Resolved 500 internal server error on /automation-v2 page
2025-07-24 15:04:25 +02:00

1751 lines
64 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
import prisma from '../lib/prisma'
import AILeverageCalculator from './ai-leverage-calculator'
import AIDCAManager from './ai-dca-manager'
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
nextAnalysisIn?: number // Seconds until next analysis
analysisInterval?: number // Analysis interval in seconds
currentCycle?: number // Current automation cycle
}
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 {
// Check if this is a scalping strategy (multiple short timeframes)
if (this.config?.selectedTimeframes) {
const timeframes = this.config.selectedTimeframes
const isScalping = timeframes.includes('5') || timeframes.includes('3') ||
(timeframes.length > 1 && timeframes.every(tf => ['1', '3', '5', '15', '30'].includes(tf)))
if (isScalping) {
console.log('🎯 Scalping strategy detected - using frequent analysis (2-3 minutes)')
return 2 * 60 * 1000 // 2 minutes for scalping
}
// Day trading strategy (short-medium timeframes)
const isDayTrading = timeframes.includes('60') || timeframes.includes('120') ||
timeframes.some(tf => ['30', '60', '120'].includes(tf))
if (isDayTrading) {
console.log('⚡ Day trading strategy detected - using moderate analysis (5-10 minutes)')
return 5 * 60 * 1000 // 5 minutes for day trading
}
// Swing trading (longer timeframes)
const isSwingTrading = timeframes.includes('240') || timeframes.includes('D') ||
timeframes.some(tf => ['240', '480', 'D', '1d'].includes(tf))
if (isSwingTrading) {
console.log('🎯 Swing trading strategy detected - using standard analysis (15-30 minutes)')
return 15 * 60 * 1000 // 15 minutes for swing trading
}
}
// Fallback to timeframe-based intervals
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}`)
// Update next scheduled time in database for timer display
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
const nextScheduled = new Date(Date.now() + intervalMs)
try {
await prisma.automationSession.updateMany({
where: {
userId: this.config.userId,
status: 'ACTIVE'
},
data: {
nextScheduled: nextScheduled,
lastAnalysis: new Date()
}
})
console.log(`⏰ Next analysis scheduled for: ${nextScheduled.toLocaleTimeString()}`)
} catch (dbError) {
console.error('Failed to update next scheduled time:', dbError)
}
// Step 1: Check for DCA opportunities on existing positions
const dcaOpportunity = await this.checkForDCAOpportunity()
if (dcaOpportunity.shouldDCA) {
console.log('🔄 DCA opportunity found, executing position scaling')
await this.executeDCA(dcaOpportunity)
await this.runPostCycleCleanup('dca_executed')
return
}
// Step 2: 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 3: 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 AI-driven position size with optimal leverage
const positionResult = await this.calculatePositionSize(analysis)
return {
direction: analysis.recommendation,
confidence: analysis.confidence,
positionSize: positionResult.tokenAmount,
leverageUsed: positionResult.leverageUsed,
marginRequired: positionResult.marginRequired,
liquidationPrice: positionResult.liquidationPrice,
riskAssessment: positionResult.riskAssessment,
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<{
tokenAmount: number
leverageUsed: number
marginRequired: number
liquidationPrice: number
riskAssessment: string
}> {
console.log('🧠 AI Position Sizing with Dynamic Leverage Calculation...')
// ✅ ENHANCED: Handle SELL positions with AI leverage for shorting
if (analysis.recommendation === 'SELL') {
return await this.calculateSellPositionWithLeverage(analysis)
}
// Get account balance
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
const balanceData = await balanceResponse.json()
if (!balanceData.success) {
throw new Error('Could not fetch account balance for position sizing')
}
const accountValue = balanceData.accountValue || balanceData.totalCollateral
const availableBalance = balanceData.availableBalance
console.log(`💰 Account Status: Value=$${accountValue.toFixed(2)}, Available=$${availableBalance.toFixed(2)}`)
// Get current price for entry
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: $${currentPrice}`)
} catch (error) {
console.error('Error fetching price for position sizing, using fallback:', error)
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
}
}
// Calculate stop loss price from analysis
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
const direction = analysis.recommendation === 'BUY' ? 'long' : 'short'
let stopLossPrice: number
if (direction === 'long') {
stopLossPrice = currentPrice * (1 - stopLossPercent)
} else {
stopLossPrice = currentPrice * (1 + stopLossPercent)
}
console.log(`🎯 Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}, Direction=${direction}`)
// Use AI Leverage Calculator for optimal leverage
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
accountValue,
availableBalance,
entryPrice: currentPrice,
stopLossPrice,
side: direction,
maxLeverageAllowed: this.config!.maxLeverage || 20, // Platform max leverage
safetyBuffer: 0.10 // 10% safety buffer between liquidation and stop loss
})
// Calculate final position size
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
const tokenAmount = leveragedAmount / currentPrice
console.log(`<60> AI Position Result:`, {
baseAmount: `$${baseAmount.toFixed(2)}`,
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
tokenAmount: tokenAmount.toFixed(4),
riskLevel: leverageResult.riskAssessment,
reasoning: leverageResult.reasoning
})
return {
tokenAmount,
leverageUsed: leverageResult.recommendedLeverage,
marginRequired: leverageResult.marginRequired,
liquidationPrice: leverageResult.liquidationPrice,
riskAssessment: leverageResult.riskAssessment
}
}
// ✅ 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
}
}
// ✅ NEW: Calculate leveraged short position for SELL orders
private async calculateSellPositionWithLeverage(analysis: any): Promise<{
tokenAmount: number
leverageUsed: number
marginRequired: number
liquidationPrice: number
riskAssessment: string
}> {
try {
console.log('📉 Calculating SELL position with AI leverage...')
// Get account balance for leverage calculation
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
const balanceData = await balanceResponse.json()
const accountValue = balanceData.accountValue || balanceData.totalCollateral
const availableBalance = balanceData.availableBalance
// Get current price
let currentPrice = analysis.entry?.price || analysis.currentPrice
if (!currentPrice) {
const { default: PriceFetcher } = await import('./price-fetcher')
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
}
// Calculate stop loss for short position (above entry price)
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
const stopLossPrice = currentPrice * (1 + stopLossPercent)
console.log(`🎯 SHORT Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}`)
// Use AI leverage for short position
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
accountValue,
availableBalance,
entryPrice: currentPrice,
stopLossPrice,
side: 'short',
maxLeverageAllowed: this.config!.maxLeverage || 20,
safetyBuffer: 0.10
})
// Calculate leveraged short amount
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
const tokenAmount = leveragedAmount / currentPrice
console.log(`📉 SELL Position with AI Leverage:`, {
baseAmount: `$${baseAmount.toFixed(2)}`,
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
tokenAmount: tokenAmount.toFixed(4),
riskLevel: leverageResult.riskAssessment,
reasoning: leverageResult.reasoning
})
return {
tokenAmount,
leverageUsed: leverageResult.recommendedLeverage,
marginRequired: leverageResult.marginRequired,
liquidationPrice: leverageResult.liquidationPrice,
riskAssessment: leverageResult.riskAssessment
}
} catch (error) {
console.error('Error calculating SELL position with leverage:', error)
return {
tokenAmount: 0.01, // Fallback small amount
leverageUsed: 1,
marginRequired: 0,
liquidationPrice: 0,
riskAssessment: 'HIGH'
}
}
}
private calculateStopLoss(analysis: any): number {
// ✅ AI-FIRST: Use AI analysis stopLoss if available
if (analysis.stopLoss?.price) {
const currentPrice = analysis.entry?.price || 189
const stopLossPrice = analysis.stopLoss.price
// Convert absolute price to percentage
if (analysis.recommendation === 'BUY') {
return ((currentPrice - stopLossPrice) / currentPrice) * 100
} else if (analysis.recommendation === 'SELL') {
return ((stopLossPrice - currentPrice) / currentPrice) * 100
}
}
// If AI provides explicit stop loss percentage, use it
if (analysis.stopLossPercent) {
return analysis.stopLossPercent
}
// Fallback: Dynamic stop loss based on market volatility (AI-calculated)
// AI determines volatility-based stop loss (0.5% to 2% range)
return this.calculateAIStopLoss(analysis)
}
private calculateTakeProfit(analysis: any): number {
// ✅ AI-FIRST: Use AI analysis takeProfit if available
if (analysis.takeProfits?.tp1?.price) {
const currentPrice = analysis.entry?.price || 150
const takeProfitPrice = analysis.takeProfits.tp1.price
// Convert absolute price to percentage
if (analysis.recommendation === 'BUY') {
return ((takeProfitPrice - currentPrice) / currentPrice) * 100
} else if (analysis.recommendation === 'SELL') {
return ((currentPrice - takeProfitPrice) / currentPrice) * 100
}
}
// If AI provides explicit take profit percentage, use it
if (analysis.takeProfitPercent) {
return analysis.takeProfitPercent
}
// Fallback: Dynamic take profit based on AI risk/reward optimization
return this.calculateAITakeProfit(analysis)
}
// 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 with AI-calculated leverage
console.log(`🌊 Executing Drift trade: ${decision.direction} ${this.config!.symbol}`)
console.log(`🧠 AI Leverage: ${decision.leverageUsed.toFixed(1)}x (Risk: ${decision.riskAssessment})`)
console.log(`💀 Liquidation Price: $${decision.liquidationPrice.toFixed(4)}`)
// 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: decision.leverageUsed || this.config!.maxLeverage || 2, // Use AI-calculated leverage
stopLoss: true,
takeProfit: true,
stopLossPercent: stopLossPercent,
takeProfitPercent: takeProfitPercent,
mode: this.config!.mode || 'SIMULATION',
// Include AI leverage details for logging
aiLeverageDetails: {
calculatedLeverage: decision.leverageUsed,
liquidationPrice: decision.liquidationPrice,
riskAssessment: decision.riskAssessment,
marginRequired: decision.marginRequired
}
})
})
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: decision.leverageUsed || tradeResult.leverageUsed || this.config!.maxLeverage,
liquidationPrice: decision.liquidationPrice,
riskAssessment: decision.riskAssessment,
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 AI leverage information
leverage: result.leverage || decision.leverageUsed,
// Add Drift-specific fields for live trades
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
realTradingAmount: this.config!.tradingAmount,
driftTxId: result.transactionId
}),
// Add AI leverage details in metadata
metadata: JSON.stringify({
aiLeverage: {
calculatedLeverage: decision.leverageUsed,
liquidationPrice: decision.liquidationPrice,
riskAssessment: decision.riskAssessment,
marginRequired: decision.marginRequired,
balanceStrategy: result.accountValue < 1000 ? 'AGGRESSIVE_100%' : 'CONSERVATIVE_50%'
}
})
}
})
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)
}
// Calculate next analysis timing
const analysisInterval = Math.floor(this.getIntervalFromTimeframe(session.timeframe) / 1000) // Convert to seconds
let nextAnalysisIn = 0
if (this.isRunning && session.nextScheduled) {
const nextScheduledTime = new Date(session.nextScheduled).getTime()
const currentTime = Date.now()
nextAnalysisIn = Math.max(0, Math.floor((nextScheduledTime - currentTime) / 1000))
}
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,
nextAnalysisIn: nextAnalysisIn,
analysisInterval: analysisInterval,
currentCycle: session.totalTrades || 0
}
} 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'
}
}
/**
* Check for DCA opportunities on existing open positions
*/
private async checkForDCAOpportunity(): Promise<any> {
try {
if (!this.config) return { shouldDCA: false }
// Get current open positions
const openPositions = await prisma.trade.findMany({
where: {
userId: this.config.userId,
status: 'open',
symbol: this.config.symbol
},
orderBy: { createdAt: 'desc' },
take: 1
})
if (openPositions.length === 0) {
return { shouldDCA: false, reasoning: 'No open positions to DCA' }
}
const currentPosition = openPositions[0]
// Get current market price
let currentPrice: number
try {
const { default: PriceFetcher } = await import('./price-fetcher')
currentPrice = await PriceFetcher.getCurrentPrice(this.config.symbol)
} catch (error) {
console.error('Error fetching current price for DCA analysis:', error)
return { shouldDCA: false, reasoning: 'Cannot fetch current price' }
}
// Get account status for DCA calculation (simplified version)
const accountStatus = {
accountValue: 1000, // Could integrate with actual account status
availableBalance: 500,
leverage: currentPosition.leverage || 1,
liquidationPrice: 0
}
// Analyze DCA opportunity using AI DCA Manager
const dcaParams = {
currentPosition: {
side: currentPosition.side as 'long' | 'short',
size: currentPosition.amount || 0,
entryPrice: currentPosition.entryPrice || currentPosition.price,
currentPrice,
unrealizedPnl: currentPosition.profit || 0,
stopLoss: currentPosition.stopLoss || 0,
takeProfit: currentPosition.takeProfit || 0
},
accountStatus,
marketData: {
price: currentPrice,
priceChange24h: 0, // Could fetch from price API if needed
volume: 0,
support: (currentPosition.entryPrice || currentPosition.price) * 0.95, // Estimate
resistance: (currentPosition.entryPrice || currentPosition.price) * 1.05 // Estimate
},
maxLeverageAllowed: this.config.maxLeverage || 20
}
const dcaResult = AIDCAManager.analyzeDCAOpportunity(dcaParams)
console.log('🔍 DCA Analysis Result:', {
shouldDCA: dcaResult.shouldDCA,
confidence: dcaResult.confidence,
reasoning: dcaResult.reasoning,
dcaAmount: dcaResult.dcaAmount?.toFixed(4),
riskLevel: dcaResult.riskAssessment
})
return dcaResult
} catch (error) {
console.error('Error checking DCA opportunity:', error)
return { shouldDCA: false, reasoning: 'DCA analysis failed' }
}
}
/**
* Execute DCA by scaling into existing position
*/
private async executeDCA(dcaResult: any): Promise<void> {
try {
if (!this.config || !dcaResult.shouldDCA) return
console.log('🔄 Executing DCA scaling:', {
amount: dcaResult.dcaAmount?.toFixed(4),
newAverage: dcaResult.newAveragePrice?.toFixed(4),
newLeverage: dcaResult.newLeverage?.toFixed(1) + 'x',
confidence: dcaResult.confidence + '%'
})
// Get current open position
const openPosition = await prisma.trade.findFirst({
where: {
userId: this.config.userId,
status: 'open',
symbol: this.config.symbol
},
orderBy: { createdAt: 'desc' }
})
if (!openPosition) {
console.error('❌ No open position found for DCA')
return
}
// Execute DCA trade via Drift Protocol (simplified for now)
if (this.config.mode === 'LIVE') {
console.log('📈 Live DCA would execute via Drift Protocol (not implemented yet)')
// TODO: Implement live DCA execution
}
// Update position with new averages (both LIVE and SIMULATION)
await this.updatePositionAfterDCA(openPosition.id, dcaResult)
// Create DCA record for tracking
await this.createDCARecord(openPosition.id, dcaResult)
console.log('✅ DCA executed successfully')
} catch (error) {
console.error('Error executing DCA:', error)
}
}
/**
* Update position after DCA execution
*/
private async updatePositionAfterDCA(positionId: string, dcaResult: any): Promise<void> {
try {
// Calculate new position metrics
const newSize = dcaResult.dcaAmount * (dcaResult.newLeverage || 1)
await prisma.trade.update({
where: { id: positionId },
data: {
amount: { increment: newSize },
entryPrice: dcaResult.newAveragePrice,
stopLoss: dcaResult.newStopLoss,
takeProfit: dcaResult.newTakeProfit,
leverage: dcaResult.newLeverage,
aiAnalysis: `DCA: ${dcaResult.reasoning}`,
updatedAt: new Date()
}
})
console.log('📊 Position updated after DCA:', {
newAverage: dcaResult.newAveragePrice?.toFixed(4),
newSL: dcaResult.newStopLoss?.toFixed(4),
newTP: dcaResult.newTakeProfit?.toFixed(4),
newLeverage: dcaResult.newLeverage?.toFixed(1) + 'x'
})
} catch (error) {
console.error('Error updating position after DCA:', error)
}
}
/**
* Create DCA record for tracking and analysis
*/
private async createDCARecord(positionId: string, dcaResult: any): Promise<void> {
try {
await prisma.dCARecord.create({
data: {
tradeId: positionId,
dcaAmount: dcaResult.dcaAmount,
dcaPrice: dcaResult.newAveragePrice, // Current market price for DCA entry
newAveragePrice: dcaResult.newAveragePrice,
newStopLoss: dcaResult.newStopLoss,
newTakeProfit: dcaResult.newTakeProfit,
newLeverage: dcaResult.newLeverage,
confidence: dcaResult.confidence,
reasoning: dcaResult.reasoning,
riskAssessment: dcaResult.riskAssessment,
createdAt: new Date()
}
})
console.log('📝 DCA record created for tracking')
} catch (error) {
console.error('Error creating DCA record:', error)
}
}
}
export const automationService = new AutomationService()