Files
trading_bot_v3/lib/automation-service-simple.ts.broken
mindesbunister 1e4f305657 fix: emergency automation fix - stop runaway trading loops
- Replace automation service with emergency rate-limited version
- Add 5-minute minimum interval between automation starts
- Implement forced Chromium process cleanup on stop
- Backup broken automation service as .broken file
- Emergency service prevents multiple simultaneous automations
- Fixed 1400+ Chromium process accumulation issue
- Tested and confirmed: rate limiting works, processes stay at 0
2025-07-24 20:33:20 +02:00

2044 lines
76 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
protected 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 {
console.log(`🔧 DEBUG: startAutomation called - isRunning: ${this.isRunning}, config exists: ${!!this.config}`)
if (this.isRunning) {
console.log(`⚠️ DEBUG: Automation already running - rejecting restart attempt`)
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,
selectedTimeframes: config.selectedTimeframes,
riskPercentage: config.riskPercentage
},
startBalance: config.tradingAmount,
currentBalance: config.tradingAmount,
createdAt: new Date(),
updatedAt: new Date()
}
})
// Start automation cycle
// Start automation cycle (price-based if positions exist, time-based if not)
await 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 async startAutomationCycle(): Promise<void> {
if (!this.config) return
// Check if we have open positions - if so, only use price-based triggers
const hasPositions = await this.hasOpenPositions()
if (hasPositions) {
console.log(`📊 Open positions detected for ${this.config.symbol} - switching to price-proximity mode only`)
console.log(`🎯 Automation will only trigger on SL/TP approach or critical levels`)
// Don't start time-based cycles when positions exist
// Price monitor events (sl_approach, tp_approach, critical_level) are already set up
return
}
// No positions - start normal time-based automation cycle
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) {
// Double-check positions before each cycle
const stillHasPositions = await this.hasOpenPositions()
if (stillHasPositions) {
console.log(`📊 Positions opened during automation - stopping time-based cycles`)
this.stopTimeCycles()
return
}
await this.runAutomationCycle()
}
}, intervalMs)
// Run first cycle immediately
this.runAutomationCycle()
}
private stopTimeCycles(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
console.log('⏸️ Time-based automation cycles stopped - now in price-proximity mode only')
}
}
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: string) => ['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> {
// Check if automation should still be running
if (!this.isRunning || !this.config) {
console.log('🛑 Automation cycle stopped - isRunning:', this.isRunning, 'config:', !!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 open positions first - DON'T analyze if positions exist unless DCA is needed
const hasPositions = await this.hasOpenPositions()
if (hasPositions) {
console.log(`📊 Open position detected for ${this.config.symbol}, checking for DCA only`)
// Only 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')
} else {
console.log('📊 Position monitoring only - no new analysis needed')
await this.runPostCycleCleanup('position_monitoring_only')
}
return
}
// Step 2: Check daily trade limit - DISABLED (no limits needed)
// 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 with error handling
console.log('📊 Performing analysis...')
const analysisResult = await this.performAnalysis()
if (!analysisResult) {
console.log('❌ Analysis failed, skipping cycle')
console.log(`⏰ Next analysis in ${this.getIntervalFromTimeframe(this.config.timeframe)/1000} seconds`)
// 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)
// Check if all analyses failed (browser automation issues)
const validResults = multiTimeframeResults.filter(result => result.analysis !== null)
if (validResults.length === 0) {
console.log('❌ All timeframe analyses failed - likely browser automation failure')
console.log(`⏰ Browser automation issues detected - next analysis in ${this.getIntervalFromTimeframe(this.config!.timeframe)/1000} seconds`)
progressTracker.updateStep(sessionId, 'capture', 'error', 'Browser automation failed - will retry on next cycle')
progressTracker.deleteSession(sessionId)
// Mark analysis as complete to allow cleanup
analysisCompletionFlag.markAnalysisComplete(sessionId)
return null
}
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${validResults.length} timeframe analyses`)
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Processing multi-timeframe results...')
// Process and combine multi-timeframe results using valid results only
const combinedResult = this.combineMultiTimeframeAnalysis(validResults)
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 actual Drift positions instead of database records
const response = await fetch('http://localhost:3000/api/drift/positions')
if (!response.ok) {
console.error('Failed to fetch Drift positions:', response.statusText)
return false
}
const data = await response.json()
const positions = data.positions || []
// Check if we have any positions for our symbol
const symbolPositions = positions.filter((pos: any) => {
const marketSymbol = pos.marketSymbol || pos.market?.symbol || ''
return marketSymbol.includes(this.config!.symbol.replace('USD', ''))
})
console.log(`🔍 Current ${this.config!.symbol} positions: ${symbolPositions.length}`)
if (symbolPositions.length > 0) {
symbolPositions.forEach((pos: any) => {
console.log(`<60> Position: ${pos.marketSymbol} ${pos.side} ${pos.baseAssetAmount} @ $${pos.entryPrice}`)
})
}
return symbolPositions.length > 0
} catch (error) {
console.error('❌ Error checking current Drift 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 learning data
learningData: 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
}
/**
* Determine if current strategy is scalping based on selected timeframes
*/
private isScalpingStrategy(): boolean {
if (!this.config) return false
if (this.config.selectedTimeframes) {
const timeframes = this.config.selectedTimeframes
const isScalping = timeframes.includes('5') || timeframes.includes('3') ||
(timeframes.length > 1 && timeframes.every((tf: string) => ['1', '3', '5', '15', '30'].includes(tf)))
return isScalping
}
// Fallback to single timeframe check
return ['1m', '3m', '5m'].includes(this.config.timeframe)
}
/**
* Check if there are any open positions for current symbol
*/
setTempConfig(config: any): void {
this.config = config as AutomationConfig;
}
clearTempConfig(): void {
this.config = null;
}
async hasOpenPositions(): Promise<boolean> {
if (!this.config) return false
try {
// Check actual Drift positions instead of database records
const response = await fetch('http://localhost:3000/api/drift/positions')
if (!response.ok) {
console.error('Failed to fetch Drift positions:', response.statusText)
return false
}
const data = await response.json()
const positions = data.positions || []
// Check if there are any positions for our symbol
const symbolPositions = positions.filter((pos: any) => {
const marketSymbol = pos.marketSymbol || pos.market?.symbol || ''
return marketSymbol.includes(this.config!.symbol.replace('USD', ''))
})
console.log(`🔍 Found ${symbolPositions.length} open Drift positions for ${this.config.symbol}`)
return symbolPositions.length > 0
} catch (error) {
console.error('Error checking Drift positions:', error)
return false
}
}
/**
* Placeholder methods for new actions (to be implemented)
*/
private async adjustStopLoss(newSLPrice: number): Promise<void> {
console.log(`🎯 Adjusting stop loss to $${newSLPrice.toFixed(4)} (placeholder implementation)`)
// TODO: Implement actual SL adjustment via Drift SDK
}
private async exitPosition(reason: string): Promise<void> {
console.log(`🚪 Exiting position due to: ${reason} (placeholder implementation)`)
// TODO: Implement actual position exit via Drift SDK
}
async stopAutomation(): Promise<boolean> {
try {
console.log('🛑 Stopping automation service...')
this.isRunning = false
// Clear the interval if it exists
if (this.intervalId) {
console.log('🛑 Clearing automation interval')
clearInterval(this.intervalId)
this.intervalId = null
}
// Store config reference before clearing it
const configRef = this.config
// Stop price monitoring with force stop if needed
try {
await priceMonitorService.stopMonitoring()
console.log('📊 Price monitoring stopped')
// Double-check and force stop if still running
setTimeout(() => {
if (priceMonitorService.isMonitoring()) {
console.log('⚠️ Price monitor still running, forcing stop...')
priceMonitorService.stopMonitoring()
}
}, 1000)
} catch (error) {
console.error('Failed to stop price monitoring:', error)
// Force stop via API as fallback
try {
await fetch('http://localhost:3000/api/price-monitor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'stop_monitoring' })
})
console.log('📊 Price monitoring force-stopped via API')
} catch (apiError) {
console.error('Failed to force stop price monitoring:', apiError)
}
}
// Update database session status to STOPPED
if (configRef) {
await prisma.automationSession.updateMany({
where: {
userId: configRef.userId,
symbol: configRef.symbol,
timeframe: configRef.timeframe,
status: 'ACTIVE'
},
data: {
status: 'STOPPED',
updatedAt: new Date()
}
})
console.log('🛑 Database session status updated to STOPPED')
}
// Reset config AFTER using it for database update
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, checking if restart is appropriate...')
// Don't auto-restart if there are open positions unless only DCA is needed
const tempConfig = { userId: session.userId, symbol: session.symbol }
this.config = tempConfig as AutomationConfig // Temporarily set config for position check
const hasPositions = await this.hasOpenPositions()
this.config = null // Clear temp config
if (hasPositions) {
console.log('📊 Open positions detected - preventing auto-restart to avoid unwanted analysis')
console.log('💡 Use manual start to override this safety check if needed')
return {
isActive: false,
mode: session.mode as 'SIMULATION' | 'LIVE',
symbol: session.symbol,
timeframe: session.timeframe,
totalTrades: session.totalTrades,
successfulTrades: session.successfulTrades,
winRate: session.winRate,
totalPnL: session.totalPnL,
lastAnalysis: session.lastAnalysis || undefined,
lastTrade: session.lastTrade || undefined,
nextScheduled: session.nextScheduled || undefined,
errorCount: session.errorCount,
lastError: session.lastError || undefined,
nextAnalysisIn: 0,
analysisInterval: 0
}
} else {
console.log('✅ No open positions - safe to auto-restart automation')
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()
}
})
// Enhanced action logic for intelligent scalping optimization
if (trigger === 'SL_APPROACH') {
console.log('🔍 Stop Loss approaching - analyzing intelligent scalping action')
const slAction = await this.analyzeSLApproachAction(analysisResult, data)
if (slAction.action === 'DCA_REVERSAL' && slAction.shouldExecute) {
console.log('🔄 Executing DCA reversal to average down position')
await this.executeDCA(slAction.dcaResult)
} else if (slAction.action === 'EARLY_EXIT' && slAction.shouldExecute) {
console.log('🚪 Executing early exit before stop loss hit')
// TODO: Implement early exit logic
} else if (slAction.action === 'ADJUST_SL' && slAction.shouldExecute) {
console.log('📊 Adjusting stop loss based on market reversal signals')
// TODO: Implement SL adjustment logic
} else {
console.log(`💡 SL Approach Action: ${slAction.action} (not executing: ${slAction.reasoning})`)
}
}
// 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)
}
}
/**
* Intelligent analysis when stop loss is approaching for scalping strategies
*/
private async analyzeSLApproachAction(
analysisResult: any,
priceData: any
): Promise<{
action: 'DCA_REVERSAL' | 'EARLY_EXIT' | 'ADJUST_SL' | 'HOLD',
shouldExecute: boolean,
reasoning: string,
dcaResult?: any
}> {
try {
if (!this.config) {
return { action: 'HOLD', shouldExecute: false, reasoning: 'No configuration available' }
}
// Only apply intelligent SL logic for scalping strategies
if (!this.isScalpingStrategy()) {
return {
action: 'HOLD',
shouldExecute: false,
reasoning: 'Not a scalping timeframe - using standard SL approach'
}
}
// Check if we have open positions to work with
const hasPositions = await this.hasOpenPositions()
if (!hasPositions) {
return {
action: 'HOLD',
shouldExecute: false,
reasoning: 'No open positions to manage'
}
}
// Analyze market reversal signals based on AI recommendation and confidence
const confidence = analysisResult.confidence || 0
const recommendation = analysisResult.recommendation || 'HOLD'
// Strong BUY signal while approaching SL suggests potential reversal
if (recommendation === 'BUY' && confidence >= 75) {
console.log('🔄 Strong BUY signal detected while approaching SL - checking DCA opportunity')
// Check DCA opportunity for potential reversal
const dcaOpportunity = await this.checkForDCAOpportunity()
if (dcaOpportunity.shouldDCA) {
return {
action: 'DCA_REVERSAL',
shouldExecute: true,
reasoning: `AI shows ${confidence}% confidence BUY signal - DCA to average down`,
dcaResult: dcaOpportunity
}
} else {
return {
action: 'ADJUST_SL',
shouldExecute: true,
reasoning: `AI shows ${confidence}% confidence BUY signal - adjust SL to give more room`
}
}
}
// Strong SELL signal confirms downtrend - early exit
else if (recommendation === 'SELL' && confidence >= 80) {
return {
action: 'EARLY_EXIT',
shouldExecute: true,
reasoning: `AI shows ${confidence}% confidence SELL signal - exit before SL hit`
}
}
// Medium confidence signals - more conservative approach
else if (confidence >= 60) {
return {
action: 'ADJUST_SL',
shouldExecute: recommendation === 'BUY',
reasoning: `Medium confidence ${recommendation} - ${recommendation === 'BUY' ? 'adjust SL' : 'maintain position'}`
}
}
// Low confidence or HOLD - maintain current strategy
else {
return {
action: 'HOLD',
shouldExecute: false,
reasoning: `Low confidence (${confidence}%) or HOLD signal - let SL trigger naturally`
}
}
} catch (error) {
console.error('Error analyzing SL approach action:', error)
return {
action: 'HOLD',
shouldExecute: false,
reasoning: 'Error in SL approach analysis'
}
}
}
}
export const automationService = new AutomationService()