Files
trading_bot_v3/lib/automation-service-simple.ts
mindesbunister 2bbaa072d6 Fix automation v2 timeframe selection and SELL signal execution
- Fixed timeframe selection to use user-selected timeframes instead of hardcoded values
- Added selectedTimeframes property to AutomationConfig interface
- Added maxDailyTrades property to AutomationConfig interface
- Removed Jupiter DEX blocking logic that prevented SELL signals from opening SHORT positions
- Fixed SELL signals to properly execute SHORT positions on Drift Protocol
- Updated automation test route with missing properties (selectedTimeframes, maxDailyTrades, dexProvider)
- Added debug logging to automation v2 page for better troubleshooting
- Added test click button for UI interaction debugging
- Resolved TypeScript compilation errors in automation service
- Ensured automation v2 interface is fully functional and responsive

These changes enable:
- Multi-timeframe analysis with user-selected timeframes
- Proper SHORT position execution on SELL signals
- Improved UI reliability and debugging capabilities
- Full Drift Protocol perpetual futures trading support
2025-07-23 13:39:33 +02:00

1237 lines
45 KiB
TypeScript
Raw 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 { jupiterDEXService } from './jupiter-dex-service'
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
import { TradingViewCredentials } from './tradingview-automation'
import { progressTracker, ProgressStatus } from './progress-tracker'
import aggressiveCleanup from './aggressive-cleanup'
import { analysisCompletionFlag } from './analysis-completion-flag'
import priceMonitorService from './price-monitor'
const prisma = new PrismaClient()
export interface AutomationConfig {
userId: string
mode: 'SIMULATION' | 'LIVE'
symbol: string
timeframe: string
selectedTimeframes: string[] // Multi-timeframe support
tradingAmount: number
maxLeverage: number
stopLossPercent: number
takeProfitPercent: number
maxDailyTrades: number
riskPercentage: number
}
export interface AutomationStatus {
isActive: boolean
mode: 'SIMULATION' | 'LIVE'
symbol: string
timeframe: string
totalTrades: number
successfulTrades: number
winRate: number
totalPnL: number
lastAnalysis?: Date
lastTrade?: Date
nextScheduled?: Date
errorCount: number
lastError?: string
}
export class AutomationService {
private isRunning = false
private config: AutomationConfig | null = null
private intervalId: NodeJS.Timeout | null = null
private stats = {
totalTrades: 0,
successfulTrades: 0,
winRate: 0,
totalPnL: 0,
errorCount: 0,
lastError: null as string | null
}
async startAutomation(config: AutomationConfig): Promise<boolean> {
try {
if (this.isRunning) {
throw new Error('Automation is already running')
}
this.config = config
this.isRunning = true
console.log(`🤖 Starting automation for ${config.symbol} ${config.timeframe} in ${config.mode} mode`)
console.log(`📊 Using timeframes: ${config.selectedTimeframes?.join(", ") || "default fallback"}`)
console.log(`📊 Timeframes array:`, config.selectedTimeframes)
console.log(`🔧 Full config:`, JSON.stringify(config, null, 2))
// 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,
selectedTimeframes: config.selectedTimeframes,
maxLeverage: config.maxLeverage,
stopLossPercent: config.stopLossPercent,
takeProfitPercent: config.takeProfitPercent,
maxDailyTrades: config.maxDailyTrades,
riskPercentage: config.riskPercentage
},
startBalance: config.tradingAmount,
currentBalance: config.tradingAmount,
createdAt: new Date(),
updatedAt: new Date()
}
})
// Start automation cycle
this.startAutomationCycle()
// Start price monitoring
await priceMonitorService.startMonitoring()
// Set up price monitor event listeners for automatic analysis triggering
priceMonitorService.on('tp_approach', async (data) => {
if (data.symbol === this.config?.symbol) {
console.log(`🎯 TP approach detected for ${data.symbol}, triggering analysis...`)
await this.triggerPriceBasedAnalysis('TP_APPROACH', data)
}
})
priceMonitorService.on('sl_approach', async (data) => {
if (data.symbol === this.config?.symbol) {
console.log(`⚠️ SL approach detected for ${data.symbol}, triggering analysis...`)
await this.triggerPriceBasedAnalysis('SL_APPROACH', data)
}
})
priceMonitorService.on('critical_level', async (data) => {
if (data.symbol === this.config?.symbol) {
console.log(`🚨 Critical level reached for ${data.symbol}, triggering urgent analysis...`)
await this.triggerPriceBasedAnalysis('CRITICAL', data)
}
})
return true
} catch (error) {
console.error('Failed to start automation:', error)
this.stats.errorCount++
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
return false
}
}
private startAutomationCycle(): void {
if (!this.config) return
// Get interval in milliseconds based on timeframe
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
console.log(`🔄 Starting automation cycle every ${intervalMs/1000} seconds`)
this.intervalId = setInterval(async () => {
if (this.isRunning && this.config) {
await this.runAutomationCycle()
}
}, intervalMs)
// Run first cycle immediately
this.runAutomationCycle()
}
private getIntervalFromTimeframe(timeframe: string): number {
const intervals: { [key: string]: number } = {
'1m': 60 * 1000,
'3m': 3 * 60 * 1000,
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'30m': 30 * 60 * 1000,
'1h': 60 * 60 * 1000,
'2h': 2 * 60 * 60 * 1000,
'4h': 4 * 60 * 60 * 1000,
'1d': 24 * 60 * 60 * 1000
}
return intervals[timeframe] || intervals['1h'] // Default to 1 hour
}
private async runAutomationCycle(): Promise<void> {
if (!this.config) return
try {
console.log(`🔍 Running automation cycle for ${this.config.symbol} ${this.config.timeframe}`)
// Step 1: Check daily trade limit
const todayTrades = await this.getTodayTradeCount(this.config.userId)
if (todayTrades >= this.config.maxDailyTrades) {
console.log(`📊 Daily trade limit reached (${todayTrades}/${this.config.maxDailyTrades})`)
// Run cleanup even when trade limit is reached
await this.runPostCycleCleanup('trade_limit_reached')
return
}
// Step 2: Take screenshot and analyze
const analysisResult = await this.performAnalysis()
if (!analysisResult) {
console.log('❌ Analysis failed, skipping cycle')
// Run cleanup when analysis fails
await this.runPostCycleCleanup('analysis_failed')
return
}
// Step 3: Store analysis for learning
await this.storeAnalysisForLearning(analysisResult)
// Step 4: Update session with latest analysis
await this.updateSessionWithAnalysis(analysisResult)
// Step 5: Make trading decision
const tradeDecision = await this.makeTradeDecision(analysisResult)
if (!tradeDecision) {
console.log('📊 No trading opportunity found')
// Run cleanup when no trading opportunity
await this.runPostCycleCleanup('no_opportunity')
return
}
// Step 6: Execute trade
await this.executeTrade(tradeDecision)
// Run cleanup after successful trade execution
await this.runPostCycleCleanup('trade_executed')
} catch (error) {
console.error('Error in automation cycle:', error)
this.stats.errorCount++
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
// Run cleanup on error
await this.runPostCycleCleanup('error')
}
}
private async runPostCycleCleanup(reason: string): Promise<void> {
console.log(`🧹 Running post-cycle cleanup (reason: ${reason})`)
// Longer delay to ensure all analysis processes AND trading decision have finished
await new Promise(resolve => setTimeout(resolve, 10000)) // 10 seconds
try {
// Use the new post-analysis cleanup that respects completion flags
await aggressiveCleanup.runPostAnalysisCleanup()
console.log(`✅ Post-cycle cleanup completed for: ${reason}`)
} catch (error) {
console.error('Error in post-cycle cleanup:', error)
}
}
private async performAnalysis(): Promise<{
screenshots: string[]
analysis: AnalysisResult | null
} | null> {
// Generate unique session ID for this analysis
const sessionId = `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Mark the start of analysis cycle to prevent cleanup interruption
analysisCompletionFlag.startAnalysisCycle(sessionId)
try {
console.log(`📸 Starting multi-timeframe analysis with dual layouts... (Session: ${sessionId})`)
// Create progress tracking session
const progressSteps = [
{ id: 'init', title: 'Initialize', description: 'Starting multi-timeframe analysis', status: 'pending' as const },
{ id: 'capture', title: 'Capture', description: 'Capturing screenshots for all timeframes', status: 'pending' as const },
{ id: 'analysis', title: 'Analysis', description: 'Running AI analysis on screenshots', status: 'pending' as const },
{ id: 'complete', title: 'Complete', description: 'Analysis complete', status: 'pending' as const }
]
progressTracker.createSession(sessionId, progressSteps)
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting multi-timeframe analysis...')
// Multi-timeframe analysis: 15m, 1h, 2h, 4h
const timeframes = this.config!.selectedTimeframes && this.config!.selectedTimeframes.length > 0 ? this.config!.selectedTimeframes : ["15", "60", "120", "240"]
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 (1h) as base
const primaryResult = validResults.find(r => r.timeframe === '1h') || validResults[0]
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
// Calculate weighted confidence based on timeframe alignment
const alignment = this.calculateTimeframeAlignment(validResults)
const baseAnalysis = primaryResult.analysis!
// Adjust confidence based on timeframe alignment
const adjustedConfidence = Math.round(baseAnalysis.confidence * alignment.score)
// Create combined analysis with multi-timeframe reasoning
const combinedAnalysis: AnalysisResult = {
...baseAnalysis,
confidence: adjustedConfidence,
reasoning: `Multi-timeframe Dual-Layout Analysis (${results.map(r => r.timeframe).join(', ')}): ${baseAnalysis.reasoning}
📊 Each timeframe analyzed with BOTH AI layout (RSI, MACD, EMAs) and DIY layout (Stochastic RSI, VWAP, OBV)
⏱️ Timeframe Alignment: ${alignment.description}
<EFBFBD> Signal Strength: ${alignment.strength}
🎯 Confidence Adjustment: ${baseAnalysis.confidence}% → ${adjustedConfidence}% (${alignment.score >= 1 ? 'Enhanced' : 'Reduced'} due to timeframe ${alignment.score >= 1 ? 'alignment' : 'divergence'})
🔬 Analysis Details:
${validResults.map(r => `${r.timeframe}: ${r.analysis?.recommendation} (${r.analysis?.confidence}% confidence)`).join('\n')}`,
keyLevels: this.consolidateKeyLevels(validResults),
marketSentiment: this.consolidateMarketSentiment(validResults)
}
return {
screenshots,
analysis: combinedAnalysis
}
}
private calculateTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
score: number
description: string
strength: string
} {
const recommendations = results.map(r => r.analysis?.recommendation).filter(Boolean)
const buySignals = recommendations.filter(r => r === 'BUY').length
const sellSignals = recommendations.filter(r => r === 'SELL').length
const holdSignals = recommendations.filter(r => r === 'HOLD').length
const total = recommendations.length
const maxSignals = Math.max(buySignals, sellSignals, holdSignals)
const alignmentRatio = maxSignals / total
let score = 1.0
let description = ''
let strength = ''
if (alignmentRatio >= 0.75) {
score = 1.2 // Boost confidence
description = `Strong alignment (${maxSignals}/${total} timeframes agree)`
strength = 'Strong'
} else if (alignmentRatio >= 0.5) {
score = 1.0 // Neutral
description = `Moderate alignment (${maxSignals}/${total} timeframes agree)`
strength = 'Moderate'
} else {
score = 0.8 // Reduce confidence
description = `Weak alignment (${maxSignals}/${total} timeframes agree)`
strength = 'Weak'
}
return { score, description, strength }
}
private consolidateKeyLevels(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): any {
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
if (allLevels.length === 0) return {}
// Use the 1h timeframe levels as primary, or first available
const primaryLevels = results.find(r => r.timeframe === '1h')?.analysis?.keyLevels || allLevels[0]
return {
...primaryLevels,
note: `Consolidated from ${allLevels.length} timeframes`
}
}
private consolidateMarketSentiment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): 'BULLISH' | 'BEARISH' | 'NEUTRAL' {
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
if (sentiments.length === 0) return 'NEUTRAL'
// Use the 1h timeframe sentiment as primary, or first available
const primarySentiment = results.find(r => r.timeframe === '1h')?.analysis?.marketSentiment || sentiments[0]
return primarySentiment || 'NEUTRAL'
}
private analyzeTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): string {
const recommendations = results.map(r => ({
timeframe: r.timeframe,
recommendation: r.analysis?.recommendation,
confidence: r.analysis?.confidence || 0
}))
const summary = recommendations.map(r => `${r.timeframe}: ${r.recommendation} (${r.confidence}%)`).join(', ')
return summary
}
private async storeAnalysisForLearning(result: {
screenshots: string[]
analysis: AnalysisResult | null
}): Promise<void> {
try {
if (!result.analysis) return
await prisma.aILearningData.create({
data: {
userId: this.config!.userId,
symbol: this.config!.symbol,
timeframe: this.config!.timeframe,
screenshot: result.screenshots[0] || '',
analysisData: JSON.stringify(result.analysis),
marketConditions: JSON.stringify({
marketSentiment: result.analysis.marketSentiment,
keyLevels: result.analysis.keyLevels,
timestamp: new Date().toISOString()
}),
confidenceScore: result.analysis.confidence,
createdAt: new Date()
}
})
} catch (error) {
console.error('Error storing analysis for learning:', error)
}
}
private async updateSessionWithAnalysis(result: {
screenshots: string[]
analysis: AnalysisResult | null
}): Promise<void> {
try {
if (!result.analysis) return
// Store the analysis decision in a field that works for now
const analysisDecision = `${result.analysis.recommendation} with ${result.analysis.confidence}% confidence - ${result.analysis.summary}`
// Update the current session with the latest analysis
await prisma.automationSession.updateMany({
where: {
userId: this.config!.userId,
symbol: this.config!.symbol,
timeframe: this.config!.timeframe,
status: 'ACTIVE'
},
data: {
lastAnalysis: new Date(),
lastError: analysisDecision // Temporarily store analysis here
}
})
// Also log the analysis for debugging
console.log('📊 Analysis stored in database:', {
recommendation: result.analysis.recommendation,
confidence: result.analysis.confidence,
summary: result.analysis.summary
})
} catch (error) {
console.error('Failed to update session with analysis:', error)
}
}
private async makeTradeDecision(result: {
screenshots: string[]
analysis: AnalysisResult | null
}): Promise<any | null> {
try {
const analysis = result.analysis
if (!analysis) return null
// Only trade if confidence is high enough
if (analysis.confidence < 70) {
console.log(`📊 Confidence too low: ${analysis.confidence}%`)
return null
}
// Only trade if direction is clear
if (analysis.recommendation === 'HOLD') {
console.log('📊 No clear direction signal')
return null
}
// Log the trading signal
if (analysis.recommendation === "SELL") {
console.log("📉 SELL signal detected - Opening SHORT position")
} else if (analysis.recommendation === "BUY") {
console.log("📈 BUY signal detected - Opening LONG position")
}
// Calculate position size based on risk percentage
const positionSize = await this.calculatePositionSize(analysis)
return {
direction: analysis.recommendation,
confidence: analysis.confidence,
positionSize,
stopLoss: this.calculateStopLoss(analysis),
takeProfit: this.calculateTakeProfit(analysis),
marketSentiment: analysis.marketSentiment,
currentPrice: analysis.entry?.price || 190 // Store current price for calculations
}
} catch (error) {
console.error('Error making trade decision:', error)
return null
}
}
private async calculatePositionSize(analysis: any): Promise<number> {
const baseAmount = this.config!.tradingAmount // This is the USD amount to invest
const riskAdjustment = this.config!.riskPercentage / 100
const confidenceAdjustment = analysis.confidence / 100
// ✅ ENHANCED: Handle both BUY and SELL position sizing
if (analysis.recommendation === 'SELL') {
// For SELL orders, calculate how much SOL to sell based on current holdings
return await this.calculateSellAmount(analysis)
}
// For BUY orders, calculate USD amount to invest
const usdAmount = baseAmount * riskAdjustment * confidenceAdjustment
// Get current price to convert USD to token amount
let currentPrice = analysis.entry?.price || analysis.currentPrice
if (!currentPrice) {
try {
const { default: PriceFetcher } = await import('./price-fetcher')
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
console.log(`📊 Using current ${this.config?.symbol || 'SOLUSD'} price for position size: $${currentPrice}`)
} catch (error) {
console.error('Error fetching price for position size, using fallback:', error)
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
}
}
// Calculate token amount: USD investment / token price
const tokenAmount = usdAmount / currentPrice
console.log(`💰 BUY Position calculation: $${usdAmount} ÷ $${currentPrice} = ${tokenAmount.toFixed(4)} tokens`)
return tokenAmount
}
// ✅ NEW: Calculate SOL amount to sell for SELL orders
private async calculateSellAmount(analysis: any): Promise<number> {
try {
// Get current SOL holdings from recent open trades
const openTrades = await prisma.trade.findMany({
where: {
userId: this.config!.userId,
symbol: this.config!.symbol,
status: 'OPEN',
side: 'BUY' // Only BUY trades represent SOL holdings
},
orderBy: { createdAt: 'desc' }
})
let totalSOLHoldings = 0
for (const trade of openTrades) {
totalSOLHoldings += trade.amount
}
// Risk-adjusted sell amount (don't sell everything at once)
const riskAdjustment = this.config!.riskPercentage / 100
const confidenceAdjustment = analysis.confidence / 100
const sellAmount = totalSOLHoldings * riskAdjustment * confidenceAdjustment
console.log(`💰 SELL Position calculation: ${totalSOLHoldings.toFixed(4)} SOL holdings × ${(riskAdjustment * confidenceAdjustment * 100).toFixed(1)}% = ${sellAmount.toFixed(4)} SOL to sell`)
return Math.max(sellAmount, 0.001) // Minimum 0.001 SOL
} catch (error) {
console.error('❌ Error calculating sell amount:', error)
return 0.01 // Fallback: sell 0.01 SOL
}
}
private calculateStopLoss(analysis: any): number {
// Use AI analysis stopLoss if available, otherwise calculate from entry price
if (analysis.stopLoss?.price) {
return analysis.stopLoss.price
}
const currentPrice = analysis.entry?.price || 189 // Current SOL price
const stopLossPercent = this.config!.stopLossPercent / 100
// ✅ ENHANCED: Proper stop loss for both BUY and SELL
if (analysis.recommendation === 'BUY') {
// BUY: Stop loss below entry (price goes down)
return currentPrice * (1 - stopLossPercent)
} else if (analysis.recommendation === 'SELL') {
// SELL: Stop loss above entry (price goes up)
return currentPrice * (1 + stopLossPercent)
} else {
return currentPrice * (1 - stopLossPercent)
}
}
private calculateTakeProfit(analysis: any): number {
// Use AI analysis takeProfit if available, otherwise calculate from entry price
if (analysis.takeProfits?.tp1?.price) {
return analysis.takeProfits.tp1.price
}
const currentPrice = analysis.entry?.price || 150 // Default SOL price
const takeProfitPercent = this.config!.takeProfitPercent / 100
// ✅ ENHANCED: Proper take profit for both BUY and SELL
if (analysis.recommendation === 'BUY') {
// BUY: Take profit above entry (price goes up)
return currentPrice * (1 + takeProfitPercent)
} else if (analysis.recommendation === 'SELL') {
// SELL: Take profit below entry (price goes down)
return currentPrice * (1 - takeProfitPercent)
} else {
return currentPrice * (1 + takeProfitPercent)
}
}
private async executeTrade(decision: any): Promise<void> {
try {
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
let tradeResult: any
if (this.config!.mode === 'SIMULATION') {
// Execute simulation trade
tradeResult = await this.executeSimulationTrade(decision)
} else {
// Execute live trade via Jupiter
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 = 'Jupiter DEX 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.runPostAnalysisCleanup()
} 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> {
try {
console.log(`🚀 Executing DRIFT trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol} with ${this.config!.maxLeverage}x leverage`)
const response = await fetch("http://localhost:3000/api/automation/trade", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
dexProvider: "DRIFT",
action: decision.direction.toLowerCase() === "buy" ? "open_long" : "open_short",
symbol: this.config!.symbol.replace("USD", ""), // Convert SOLUSD to SOL
amount: this.config!.tradingAmount,
side: decision.direction,
leverage: this.config!.maxLeverage,
stopLoss: decision.stopLoss,
takeProfit: decision.takeProfit,
mode: "LIVE"
})
})
if (!response.ok) {
throw new Error(`Drift trade request failed: ${response.statusText}`)
}
const result = await response.json()
if (result.success) {
return {
transactionId: result.result?.transactionId || result.txId,
executionPrice: result.result?.executionPrice || decision.currentPrice,
amount: result.result?.amount || decision.positionSize,
direction: decision.direction,
status: "COMPLETED",
timestamp: new Date(),
fees: result.result?.fees || 0,
slippage: result.result?.slippage || 0,
leverage: this.config!.maxLeverage,
dexProvider: "DRIFT",
tradingAmount: this.config!.tradingAmount
}
} else {
throw new Error(result.error || "Drift trade execution failed")
}
} catch (error) {
console.error("Live trade execution error:", error)
throw error
}
}
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 Jupiter
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 Jupiter-specific fields for live trades
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
realTradingAmount: this.config!.tradingAmount,
inputAmount: result.inputAmount,
slippage: result.slippage
})
}
})
console.log('✅ Trade stored in database successfully')
} catch (error) {
console.error('❌ Error storing trade:', error)
}
}
private updateStats(result: any): void {
this.stats.totalTrades++
if (result.status === 'COMPLETED') {
this.stats.successfulTrades++
this.stats.winRate = (this.stats.successfulTrades / this.stats.totalTrades) * 100
// Update PnL (simplified calculation)
const pnl = result.amount * 0.01 * (Math.random() > 0.5 ? 1 : -1) // Random PnL for demo
this.stats.totalPnL += pnl
}
}
private async getTodayTradeCount(userId: string): Promise<number> {
const today = new Date()
today.setHours(0, 0, 0, 0)
const count = await prisma.trade.count({
where: {
userId,
isAutomated: true,
createdAt: {
gte: today
}
}
})
return count
}
async stopAutomation(): Promise<boolean> {
try {
this.isRunning = false
// Clear the interval if it exists
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
// Stop price monitoring
try {
await priceMonitorService.stopMonitoring()
console.log('📊 Price monitoring stopped')
} catch (error) {
console.error('Failed to stop price monitoring:', error)
}
// Update database session status to STOPPED
if (this.config) {
await prisma.automationSession.updateMany({
where: {
userId: this.config.userId,
symbol: this.config.symbol,
timeframe: this.config.timeframe,
status: 'ACTIVE'
},
data: {
status: 'STOPPED',
updatedAt: new Date()
}
})
}
this.config = null
console.log('🛑 Automation stopped')
return true
} catch (error) {
console.error('Failed to stop automation:', error)
return false
}
}
async pauseAutomation(): Promise<boolean> {
try {
if (!this.isRunning) {
return false
}
this.isRunning = false
console.log('⏸️ Automation paused')
return true
} catch (error) {
console.error('Failed to pause automation:', error)
return false
}
}
async resumeAutomation(): Promise<boolean> {
try {
if (!this.config) {
return false
}
this.isRunning = true
console.log('▶️ Automation resumed')
return true
} catch (error) {
console.error('Failed to resume automation:', error)
return false
}
}
async getStatus(): Promise<AutomationStatus | null> {
try {
// Get the latest active automation session from database first
const session = await prisma.automationSession.findFirst({
where: { status: 'ACTIVE' },
orderBy: { createdAt: 'desc' }
})
if (!session) {
return null
}
// If we have a session but automation is not running in memory,
// it means the server was restarted but the session is still active
const isActiveInMemory = this.isRunning && this.config !== null
// Auto-restart automation if session exists but not running in memory
if (!isActiveInMemory) {
console.log('🔄 Found active session but automation not running, attempting auto-restart...')
await this.autoRestartFromSession(session)
}
return {
isActive: this.isRunning && this.config !== null,
mode: session.mode as 'SIMULATION' | 'LIVE',
symbol: session.symbol,
timeframe: session.timeframe,
totalTrades: session.totalTrades,
successfulTrades: session.successfulTrades,
winRate: session.winRate,
totalPnL: session.totalPnL,
errorCount: session.errorCount,
lastError: session.lastError || undefined,
lastAnalysis: session.lastAnalysis || undefined,
lastTrade: session.lastTrade || undefined,
nextScheduled: session.nextScheduled || undefined
}
} catch (error) {
console.error('Failed to get automation status:', error)
return null
}
}
private async autoRestartFromSession(session: any): Promise<void> {
try {
const settings = session.settings || {}
const config: AutomationConfig = {
userId: session.userId,
mode: session.mode,
symbol: session.symbol,
timeframe: session.timeframe,
selectedTimeframes: settings.selectedTimeframes || ["60", "240"], // Default fallback
tradingAmount: settings.tradingAmount || 100,
maxLeverage: settings.maxLeverage || 3,
stopLossPercent: settings.stopLossPercent || 2,
takeProfitPercent: settings.takeProfitPercent || 6,
maxDailyTrades: settings.maxDailyTrades || 5,
riskPercentage: settings.riskPercentage || 2
}
await this.startAutomation(config)
console.log('✅ Automation auto-restarted successfully')
} catch (error) {
console.error('Failed to auto-restart automation:', error)
}
}
async getLearningInsights(userId: string): Promise<{
totalAnalyses: number
avgAccuracy: number
bestTimeframe: string
worstTimeframe: string
commonFailures: string[]
recommendations: string[]
}> {
try {
// For now, return mock data
return {
totalAnalyses: 150,
avgAccuracy: 0.72,
bestTimeframe: '1h',
worstTimeframe: '15m',
commonFailures: [
'Low confidence predictions',
'Missed support/resistance levels',
'Timeframe misalignment'
],
recommendations: [
'Focus on 1h timeframe for better accuracy',
'Wait for higher confidence signals (>75%)',
'Use multiple timeframe confirmation'
]
}
} catch (error) {
console.error('Failed to get learning insights:', error)
return {
totalAnalyses: 0,
avgAccuracy: 0,
bestTimeframe: 'Unknown',
worstTimeframe: 'Unknown',
commonFailures: [],
recommendations: []
}
}
}
/**
* Trigger analysis based on price movement alerts
*/
private async triggerPriceBasedAnalysis(
trigger: 'TP_APPROACH' | 'SL_APPROACH' | 'CRITICAL',
data: any
): Promise<void> {
if (!this.config || !this.isRunning) {
console.log('❌ Cannot trigger price-based analysis: automation not running')
return
}
const sessionId = `price-trigger-${Date.now()}`
try {
console.log(`🔥 Price-based analysis triggered by ${trigger} for ${data.symbol}`)
console.log(`📊 Current price: $${data.currentPrice}, Target: $${data.targetPrice}`)
// Create progress tracker for this analysis
const steps = [
{ id: 'trigger', title: 'Triggered by price movement', description: 'Analysis initiated by price alert', status: 'pending' as ProgressStatus },
{ id: 'screenshot', title: 'Capturing screenshots', description: 'Taking fresh market screenshots', status: 'pending' as ProgressStatus },
{ id: 'analysis', title: 'Running AI analysis', description: 'Analyzing current market conditions', status: 'pending' as ProgressStatus },
{ id: 'evaluation', title: 'Evaluating position', description: 'Checking position adjustments', status: 'pending' as ProgressStatus },
{ id: 'complete', title: 'Analysis complete', description: 'Price-based analysis finished', status: 'pending' as ProgressStatus }
]
progressTracker.createSession(sessionId, steps)
progressTracker.updateStep(sessionId, 'trigger', 'active', `${trigger}: ${data.symbol} at $${data.currentPrice}`)
// Run enhanced screenshot capture with current symbol/timeframe
progressTracker.updateStep(sessionId, 'screenshot', 'active')
const screenshotConfig = {
symbol: this.config.symbol,
timeframe: this.config.timeframe,
layouts: ['ai', 'diy'],
sessionId
}
const screenshots = await enhancedScreenshotService.captureWithLogin(screenshotConfig)
if (!screenshots || screenshots.length === 0) {
throw new Error('Failed to capture screenshots for price-based analysis')
}
progressTracker.updateStep(sessionId, 'screenshot', 'completed', `Captured ${screenshots.length} screenshots`)
progressTracker.updateStep(sessionId, 'analysis', 'active')
// Simplified analysis call - just use the first screenshot
const analysisResult = await aiAnalysisService.analyzeScreenshot(screenshots[0])
if (!analysisResult) {
throw new Error('AI analysis returned null result')
}
progressTracker.updateStep(sessionId, 'analysis', 'completed', `Analysis: ${analysisResult.recommendation}`)
progressTracker.updateStep(sessionId, 'evaluation', 'active')
// Store the triggered analysis in trading journal
await prisma.tradingJournal.create({
data: {
userId: this.config.userId,
screenshotUrl: screenshots[0] || '',
aiAnalysis: analysisResult.reasoning || 'No analysis available',
confidence: analysisResult.confidence || 0,
recommendation: analysisResult.recommendation || 'HOLD',
symbol: this.config.symbol,
timeframe: this.config.timeframe,
sessionId,
notes: `Price-triggered analysis: ${trigger} - Current: $${data.currentPrice}, Target: $${data.targetPrice}`,
marketSentiment: analysisResult.marketSentiment || 'Unknown',
tradingMode: this.config.mode,
isAutomated: true,
priceAtAnalysis: data.currentPrice,
marketCondition: trigger,
createdAt: new Date()
}
})
// Log important insights for potential position adjustments
if (analysisResult.recommendation === 'SELL' && trigger === 'SL_APPROACH') {
console.log('⚠️ AI recommends SELL while approaching Stop Loss - consider early exit')
} else if (analysisResult.recommendation === 'BUY' && trigger === 'TP_APPROACH') {
console.log('🎯 AI recommends BUY while approaching Take Profit - consider extending position')
}
progressTracker.updateStep(sessionId, 'evaluation', 'completed')
progressTracker.updateStep(sessionId, 'complete', 'completed',
`${analysisResult.recommendation} (${analysisResult.confidence}% confidence)`)
console.log(`✅ Price-based analysis completed (${trigger}): ${analysisResult.recommendation} with ${analysisResult.confidence}% confidence`)
} catch (error) {
console.error(`❌ Price-based analysis failed (${trigger}):`, error)
progressTracker.updateStep(sessionId, 'complete', 'error',
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
this.stats.errorCount++
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
}
}
}
export const automationService = new AutomationService()