Stop API improvements: - Added comprehensive debug logging for stop process - Changed session status from INACTIVE to STOPPED for clarity - Better error tracking and result reporting Automation service improvements: - Added isRunning check at start of runAutomationCycle to prevent zombie cycles - Enhanced stop method with better logging and state reset - Proper config cleanup after database update to prevent residual processes - More robust interval clearing and state management These changes should fix the issue where automation appears stopped but continues running in background.
1931 lines
71 KiB
TypeScript
1931 lines
71 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
||
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
||
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
||
import { TradingViewCredentials } from './tradingview-automation'
|
||
import { progressTracker, ProgressStatus } from './progress-tracker'
|
||
import aggressiveCleanup from './aggressive-cleanup'
|
||
import { analysisCompletionFlag } from './analysis-completion-flag'
|
||
import priceMonitorService from './price-monitor'
|
||
|
||
import prisma from '../lib/prisma'
|
||
import AILeverageCalculator from './ai-leverage-calculator'
|
||
import AIDCAManager from './ai-dca-manager'
|
||
|
||
export interface AutomationConfig {
|
||
userId: string
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
selectedTimeframes?: string[] // Multi-timeframe support from UI
|
||
tradingAmount: number
|
||
maxLeverage: number
|
||
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||
maxDailyTrades: number
|
||
riskPercentage: number
|
||
dexProvider?: string // DEX provider (DRIFT or JUPITER)
|
||
}
|
||
|
||
export interface AutomationStatus {
|
||
isActive: boolean
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
totalTrades: number
|
||
successfulTrades: number
|
||
winRate: number
|
||
totalPnL: number
|
||
lastAnalysis?: Date
|
||
lastTrade?: Date
|
||
nextScheduled?: Date
|
||
errorCount: number
|
||
lastError?: string
|
||
nextAnalysisIn?: number // Seconds until next analysis
|
||
analysisInterval?: number // Analysis interval in seconds
|
||
currentCycle?: number // Current automation cycle
|
||
}
|
||
|
||
export class AutomationService {
|
||
private isRunning = false
|
||
private config: AutomationConfig | null = null
|
||
private intervalId: NodeJS.Timeout | null = null
|
||
private stats = {
|
||
totalTrades: 0,
|
||
successfulTrades: 0,
|
||
winRate: 0,
|
||
totalPnL: 0,
|
||
errorCount: 0,
|
||
lastError: null as string | null
|
||
}
|
||
|
||
async startAutomation(config: AutomationConfig): Promise<boolean> {
|
||
try {
|
||
if (this.isRunning) {
|
||
throw new Error('Automation is already running')
|
||
}
|
||
|
||
this.config = config
|
||
this.isRunning = true
|
||
|
||
console.log(`🤖 Starting automation for ${config.symbol} ${config.timeframe} in ${config.mode} mode`)
|
||
|
||
// Ensure user exists in database
|
||
await prisma.user.upsert({
|
||
where: { id: config.userId },
|
||
update: {},
|
||
create: {
|
||
id: config.userId,
|
||
email: `${config.userId}@example.com`,
|
||
name: config.userId,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Delete any existing automation session for this user/symbol/timeframe
|
||
await prisma.automationSession.deleteMany({
|
||
where: {
|
||
userId: config.userId,
|
||
symbol: config.symbol,
|
||
timeframe: config.timeframe
|
||
}
|
||
})
|
||
|
||
// Create new automation session in database
|
||
await prisma.automationSession.create({
|
||
data: {
|
||
userId: config.userId,
|
||
status: 'ACTIVE',
|
||
mode: config.mode,
|
||
symbol: config.symbol,
|
||
timeframe: config.timeframe,
|
||
settings: {
|
||
tradingAmount: config.tradingAmount,
|
||
maxLeverage: config.maxLeverage,
|
||
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||
maxDailyTrades: config.maxDailyTrades,
|
||
selectedTimeframes: config.selectedTimeframes,
|
||
riskPercentage: config.riskPercentage
|
||
},
|
||
startBalance: config.tradingAmount,
|
||
currentBalance: config.tradingAmount,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Start automation cycle
|
||
this.startAutomationCycle()
|
||
|
||
// Start price monitoring
|
||
await priceMonitorService.startMonitoring()
|
||
|
||
// Set up price monitor event listeners for automatic analysis triggering
|
||
priceMonitorService.on('tp_approach', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`🎯 TP approach detected for ${data.symbol}, triggering analysis...`)
|
||
await this.triggerPriceBasedAnalysis('TP_APPROACH', data)
|
||
}
|
||
})
|
||
|
||
priceMonitorService.on('sl_approach', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`⚠️ SL approach detected for ${data.symbol}, triggering analysis...`)
|
||
await this.triggerPriceBasedAnalysis('SL_APPROACH', data)
|
||
}
|
||
})
|
||
|
||
priceMonitorService.on('critical_level', async (data) => {
|
||
if (data.symbol === this.config?.symbol) {
|
||
console.log(`🚨 Critical level reached for ${data.symbol}, triggering urgent analysis...`)
|
||
await this.triggerPriceBasedAnalysis('CRITICAL', data)
|
||
}
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to start automation:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
return false
|
||
}
|
||
}
|
||
|
||
private startAutomationCycle(): void {
|
||
if (!this.config) return
|
||
|
||
// Get interval in milliseconds based on timeframe
|
||
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
|
||
|
||
console.log(`🔄 Starting automation cycle every ${intervalMs/1000} seconds`)
|
||
|
||
this.intervalId = setInterval(async () => {
|
||
if (this.isRunning && this.config) {
|
||
await this.runAutomationCycle()
|
||
}
|
||
}, intervalMs)
|
||
|
||
// Run first cycle immediately
|
||
this.runAutomationCycle()
|
||
}
|
||
|
||
private getIntervalFromTimeframe(timeframe: string): number {
|
||
// Check if this is a scalping strategy (multiple short timeframes)
|
||
if (this.config?.selectedTimeframes) {
|
||
const timeframes = this.config.selectedTimeframes
|
||
const isScalping = timeframes.includes('5') || timeframes.includes('3') || (timeframes.length > 1 && timeframes.every((tf: 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 DCA opportunities on existing positions
|
||
const dcaOpportunity = await this.checkForDCAOpportunity()
|
||
if (dcaOpportunity.shouldDCA) {
|
||
console.log('🔄 DCA opportunity found, executing position scaling')
|
||
await this.executeDCA(dcaOpportunity)
|
||
await this.runPostCycleCleanup('dca_executed')
|
||
return
|
||
}
|
||
|
||
// Step 2: Check daily trade limit - 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
|
||
const analysisResult = await this.performAnalysis()
|
||
if (!analysisResult) {
|
||
console.log('❌ Analysis failed, skipping cycle')
|
||
// Run cleanup when analysis fails
|
||
await this.runPostCycleCleanup('analysis_failed')
|
||
return
|
||
}
|
||
|
||
// Step 3: Store analysis for learning
|
||
await this.storeAnalysisForLearning(analysisResult)
|
||
|
||
// Step 4: Update session with latest analysis
|
||
await this.updateSessionWithAnalysis(analysisResult)
|
||
|
||
// Step 5: Make trading decision
|
||
const tradeDecision = await this.makeTradeDecision(analysisResult)
|
||
if (!tradeDecision) {
|
||
console.log('📊 No trading opportunity found')
|
||
// Run cleanup when no trading opportunity
|
||
await this.runPostCycleCleanup('no_opportunity')
|
||
return
|
||
}
|
||
|
||
// Step 6: Execute trade
|
||
await this.executeTrade(tradeDecision)
|
||
|
||
// Run cleanup after successful trade execution
|
||
await this.runPostCycleCleanup('trade_executed')
|
||
|
||
} catch (error) {
|
||
console.error('Error in automation cycle:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
|
||
// Run cleanup on error
|
||
await this.runPostCycleCleanup('error')
|
||
}
|
||
}
|
||
|
||
private async runPostCycleCleanup(reason: string): Promise<void> {
|
||
console.log(`🧹 Running post-cycle cleanup (reason: ${reason})`)
|
||
|
||
// Longer delay to ensure all analysis processes AND trading decision have finished
|
||
await new Promise(resolve => setTimeout(resolve, 10000)) // 10 seconds
|
||
|
||
try {
|
||
// Use the new post-analysis cleanup that respects completion flags
|
||
await aggressiveCleanup.runPostAnalysisCleanup()
|
||
console.log(`✅ Post-cycle cleanup completed for: ${reason}`)
|
||
} catch (error) {
|
||
console.error('Error in post-cycle cleanup:', error)
|
||
}
|
||
}
|
||
|
||
private async performAnalysis(): Promise<{
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} | null> {
|
||
// Generate unique session ID for this analysis
|
||
const sessionId = `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||
|
||
// Mark the start of analysis cycle to prevent cleanup interruption
|
||
analysisCompletionFlag.startAnalysisCycle(sessionId)
|
||
|
||
try {
|
||
console.log(`📸 Starting multi-timeframe analysis with dual layouts... (Session: ${sessionId})`)
|
||
|
||
// Create progress tracking session
|
||
const progressSteps = [
|
||
{ id: 'init', title: 'Initialize', description: 'Starting multi-timeframe analysis', status: 'pending' as const },
|
||
{ id: 'capture', title: 'Capture', description: 'Capturing screenshots for all timeframes', status: 'pending' as const },
|
||
{ id: 'analysis', title: 'Analysis', description: 'Running AI analysis on screenshots', status: 'pending' as const },
|
||
{ id: 'complete', title: 'Complete', description: 'Analysis complete', status: 'pending' as const }
|
||
]
|
||
|
||
progressTracker.createSession(sessionId, progressSteps)
|
||
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting multi-timeframe analysis...')
|
||
|
||
// Use selected timeframes from UI, fallback to default if not provided
|
||
const timeframes = this.config!.selectedTimeframes || ['1h']
|
||
const symbol = this.config!.symbol
|
||
|
||
console.log(`🔍 Analyzing ${symbol} across timeframes: ${timeframes.join(', ')} with AI + DIY layouts`)
|
||
|
||
progressTracker.updateStep(sessionId, 'init', 'completed', `Starting analysis for ${timeframes.length} timeframes`)
|
||
progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...')
|
||
|
||
// Analyze each timeframe with both AI and DIY layouts
|
||
const multiTimeframeResults = await this.analyzeMultiTimeframeWithDualLayouts(symbol, timeframes, sessionId)
|
||
|
||
if (multiTimeframeResults.length === 0) {
|
||
console.log('❌ No multi-timeframe analysis results')
|
||
progressTracker.updateStep(sessionId, 'capture', 'error', 'No analysis results captured')
|
||
progressTracker.deleteSession(sessionId)
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
return null
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${multiTimeframeResults.length} timeframe analyses`)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Processing multi-timeframe results...')
|
||
|
||
// Process and combine multi-timeframe results
|
||
const combinedResult = this.combineMultiTimeframeAnalysis(multiTimeframeResults)
|
||
|
||
if (!combinedResult.analysis) {
|
||
console.log('❌ Failed to combine multi-timeframe analysis')
|
||
progressTracker.updateStep(sessionId, 'analysis', 'error', 'Failed to combine analysis results')
|
||
progressTracker.deleteSession(sessionId)
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
return null
|
||
}
|
||
|
||
console.log(`✅ Multi-timeframe analysis completed: ${combinedResult.analysis.recommendation} with ${combinedResult.analysis.confidence}% confidence`)
|
||
console.log(`📊 Timeframe alignment: ${this.analyzeTimeframeAlignment(multiTimeframeResults)}`)
|
||
|
||
progressTracker.updateStep(sessionId, 'analysis', 'completed', `Analysis complete: ${combinedResult.analysis.recommendation}`)
|
||
progressTracker.updateStep(sessionId, 'complete', 'completed', 'Multi-timeframe analysis finished')
|
||
|
||
// Clean up session after successful completion
|
||
setTimeout(() => {
|
||
progressTracker.deleteSession(sessionId)
|
||
}, 2000)
|
||
|
||
// Mark analysis as complete to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
|
||
return combinedResult
|
||
|
||
} catch (error) {
|
||
console.error('Error performing multi-timeframe analysis:', error)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'error', error instanceof Error ? error.message : 'Unknown error')
|
||
setTimeout(() => {
|
||
progressTracker.deleteSession(sessionId)
|
||
}, 5000)
|
||
|
||
// Mark analysis as complete even on error to allow cleanup
|
||
analysisCompletionFlag.markAnalysisComplete(sessionId)
|
||
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async analyzeMultiTimeframeWithDualLayouts(
|
||
symbol: string,
|
||
timeframes: string[],
|
||
sessionId: string
|
||
): Promise<Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>> {
|
||
const results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }> = []
|
||
|
||
for (let i = 0; i < timeframes.length; i++) {
|
||
const timeframe = timeframes[i]
|
||
try {
|
||
console.log(`📊 Analyzing ${symbol} ${timeframe} with AI + DIY layouts... (${i + 1}/${timeframes.length})`)
|
||
|
||
// Update progress for timeframe
|
||
progressTracker.updateTimeframeProgress(sessionId, i + 1, timeframes.length, timeframe)
|
||
|
||
// Use the dual-layout configuration for each timeframe
|
||
const screenshotConfig = {
|
||
symbol: symbol,
|
||
timeframe: timeframe,
|
||
layouts: ['ai', 'diy'],
|
||
sessionId: sessionId
|
||
}
|
||
|
||
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig)
|
||
|
||
if (result.analysis) {
|
||
console.log(`✅ ${timeframe} analysis: ${result.analysis.recommendation} (${result.analysis.confidence}% confidence)`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: result.analysis
|
||
})
|
||
} else {
|
||
console.log(`❌ ${timeframe} analysis failed`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
|
||
// Small delay between captures to avoid overwhelming the system
|
||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||
|
||
} catch (error) {
|
||
console.error(`Failed to analyze ${symbol} ${timeframe}:`, error)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
}
|
||
|
||
return results
|
||
}
|
||
|
||
private combineMultiTimeframeAnalysis(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} {
|
||
const validResults = results.filter(r => r.analysis !== null)
|
||
|
||
if (validResults.length === 0) {
|
||
return { screenshots: [], analysis: null }
|
||
}
|
||
|
||
// Get the primary timeframe (first selected or default) as base
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primaryResult = validResults.find(r => r.timeframe === primaryTimeframe) || validResults[0]
|
||
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
|
||
|
||
// Calculate weighted confidence based on timeframe alignment
|
||
const alignment = this.calculateTimeframeAlignment(validResults)
|
||
const baseAnalysis = primaryResult.analysis!
|
||
|
||
// Adjust confidence based on timeframe alignment
|
||
const adjustedConfidence = Math.round(baseAnalysis.confidence * alignment.score)
|
||
|
||
// Create combined analysis with multi-timeframe reasoning
|
||
const combinedAnalysis: AnalysisResult = {
|
||
...baseAnalysis,
|
||
confidence: adjustedConfidence,
|
||
reasoning: `Multi-timeframe Dual-Layout Analysis (${results.map(r => r.timeframe).join(', ')}): ${baseAnalysis.reasoning}
|
||
|
||
📊 Each timeframe analyzed with BOTH AI layout (RSI, MACD, EMAs) and DIY layout (Stochastic RSI, VWAP, OBV)
|
||
⏱️ Timeframe Alignment: ${alignment.description}
|
||
<EFBFBD> Signal Strength: ${alignment.strength}
|
||
🎯 Confidence Adjustment: ${baseAnalysis.confidence}% → ${adjustedConfidence}% (${alignment.score >= 1 ? 'Enhanced' : 'Reduced'} due to timeframe ${alignment.score >= 1 ? 'alignment' : 'divergence'})
|
||
|
||
🔬 Analysis Details:
|
||
${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.analysis?.confidence}% confidence)`).join('\n')}`,
|
||
|
||
keyLevels: this.consolidateKeyLevels(validResults),
|
||
marketSentiment: this.consolidateMarketSentiment(validResults)
|
||
}
|
||
|
||
return {
|
||
screenshots,
|
||
analysis: combinedAnalysis
|
||
}
|
||
}
|
||
|
||
private calculateTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
score: number
|
||
description: string
|
||
strength: string
|
||
} {
|
||
const recommendations = results.map(r => r.analysis?.recommendation).filter(Boolean)
|
||
const buySignals = recommendations.filter(r => r === 'BUY').length
|
||
const sellSignals = recommendations.filter(r => r === 'SELL').length
|
||
const holdSignals = recommendations.filter(r => r === 'HOLD').length
|
||
|
||
const total = recommendations.length
|
||
const maxSignals = Math.max(buySignals, sellSignals, holdSignals)
|
||
const alignmentRatio = maxSignals / total
|
||
|
||
let score = 1.0
|
||
let description = ''
|
||
let strength = ''
|
||
|
||
if (alignmentRatio >= 0.75) {
|
||
score = 1.2 // Boost confidence
|
||
description = `Strong alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Strong'
|
||
} else if (alignmentRatio >= 0.5) {
|
||
score = 1.0 // Neutral
|
||
description = `Moderate alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Moderate'
|
||
} else {
|
||
score = 0.8 // Reduce confidence
|
||
description = `Weak alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Weak'
|
||
}
|
||
|
||
return { score, description, strength }
|
||
}
|
||
|
||
private consolidateKeyLevels(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): any {
|
||
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
|
||
if (allLevels.length === 0) return {}
|
||
|
||
// Use the primary timeframe levels (first selected) as primary, or first available
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primaryLevels = results.find(r => r.timeframe === primaryTimeframe)?.analysis?.keyLevels || allLevels[0]
|
||
|
||
return {
|
||
...primaryLevels,
|
||
note: `Consolidated from ${allLevels.length} timeframes`
|
||
}
|
||
}
|
||
|
||
private consolidateMarketSentiment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): 'BULLISH' | 'BEARISH' | 'NEUTRAL' {
|
||
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
|
||
if (sentiments.length === 0) return 'NEUTRAL'
|
||
|
||
// Use the primary timeframe sentiment (first selected) as primary, or first available
|
||
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
const primarySentiment = results.find(r => r.timeframe === primaryTimeframe)?.analysis?.marketSentiment || sentiments[0]
|
||
|
||
return primarySentiment || 'NEUTRAL'
|
||
}
|
||
|
||
private analyzeTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): string {
|
||
const recommendations = results.map(r => ({
|
||
timeframe: r.timeframe,
|
||
recommendation: r.analysis?.recommendation,
|
||
confidence: r.analysis?.confidence || 0
|
||
}))
|
||
|
||
const summary = recommendations.map(r => `${r.timeframe}: ${r.recommendation} (${r.confidence}%)`).join(', ')
|
||
return summary
|
||
}
|
||
|
||
private async storeAnalysisForLearning(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
await prisma.aILearningData.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
screenshot: result.screenshots[0] || '',
|
||
analysisData: JSON.stringify(result.analysis),
|
||
marketConditions: JSON.stringify({
|
||
marketSentiment: result.analysis.marketSentiment,
|
||
keyLevels: result.analysis.keyLevels,
|
||
timestamp: new Date().toISOString()
|
||
}),
|
||
confidenceScore: result.analysis.confidence,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('Error storing analysis for learning:', error)
|
||
}
|
||
}
|
||
|
||
private async updateSessionWithAnalysis(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
// Store the analysis decision in a field that works for now
|
||
const analysisDecision = `${result.analysis.recommendation} with ${result.analysis.confidence}% confidence - ${result.analysis.summary}`
|
||
|
||
// Update the current session with the latest analysis
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
lastAnalysis: new Date(),
|
||
lastError: analysisDecision // Temporarily store analysis here
|
||
}
|
||
})
|
||
|
||
// Also log the analysis for debugging
|
||
console.log('📊 Analysis stored in database:', {
|
||
recommendation: result.analysis.recommendation,
|
||
confidence: result.analysis.confidence,
|
||
summary: result.analysis.summary
|
||
})
|
||
} catch (error) {
|
||
console.error('Failed to update session with analysis:', error)
|
||
}
|
||
}
|
||
|
||
private async makeTradeDecision(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<any | null> {
|
||
try {
|
||
const analysis = result.analysis
|
||
if (!analysis) return null
|
||
|
||
// Only trade if confidence is high enough
|
||
if (analysis.confidence < 70) {
|
||
console.log(`📊 Confidence too low: ${analysis.confidence}%`)
|
||
return null
|
||
}
|
||
|
||
// Only trade if direction is clear
|
||
if (analysis.recommendation === 'HOLD') {
|
||
console.log('📊 No clear direction signal')
|
||
return null
|
||
}
|
||
|
||
// ✅ ENHANCED: Support both BUY and SELL signals
|
||
if (analysis.recommendation === 'SELL') {
|
||
// Check if we have SOL position to sell
|
||
const hasPosition = await this.checkCurrentPosition()
|
||
if (!hasPosition) {
|
||
console.log('📊 SELL signal but no SOL position to sell - skipping')
|
||
return null
|
||
}
|
||
console.log('📉 SELL signal detected with existing SOL position')
|
||
} else if (analysis.recommendation === 'BUY') {
|
||
console.log('📈 BUY signal detected')
|
||
}
|
||
|
||
// Calculate AI-driven position size with optimal leverage
|
||
const positionResult = await this.calculatePositionSize(analysis)
|
||
|
||
return {
|
||
direction: analysis.recommendation,
|
||
confidence: analysis.confidence,
|
||
positionSize: positionResult.tokenAmount,
|
||
leverageUsed: positionResult.leverageUsed,
|
||
marginRequired: positionResult.marginRequired,
|
||
liquidationPrice: positionResult.liquidationPrice,
|
||
riskAssessment: positionResult.riskAssessment,
|
||
stopLoss: this.calculateStopLoss(analysis),
|
||
takeProfit: this.calculateTakeProfit(analysis),
|
||
marketSentiment: analysis.marketSentiment,
|
||
currentPrice: analysis.entry?.price || 190 // Store current price for calculations
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error making trade decision:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// ✅ NEW: Check if we have SOL position available to sell
|
||
private async checkCurrentPosition(): Promise<boolean> {
|
||
try {
|
||
// Check recent trades to see current position
|
||
const recentTrades = await prisma.trade.findMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
status: 'OPEN'
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 5
|
||
})
|
||
|
||
// Count open positions
|
||
let netPosition = 0
|
||
for (const trade of recentTrades) {
|
||
if (trade.side === 'BUY') {
|
||
netPosition += trade.amount
|
||
} else if (trade.side === 'SELL') {
|
||
netPosition -= trade.amount
|
||
}
|
||
}
|
||
|
||
console.log(`🔍 Current SOL position: ${netPosition.toFixed(4)} SOL`)
|
||
return netPosition > 0.001 // Have at least 0.001 SOL to sell
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error checking current position:', error)
|
||
// If we can't check, default to allowing the trade (fail-safe)
|
||
return true
|
||
}
|
||
}
|
||
|
||
private async calculatePositionSize(analysis: any): Promise<{
|
||
tokenAmount: number
|
||
leverageUsed: number
|
||
marginRequired: number
|
||
liquidationPrice: number
|
||
riskAssessment: string
|
||
}> {
|
||
console.log('🧠 AI Position Sizing with Dynamic Leverage Calculation...')
|
||
|
||
// ✅ ENHANCED: Handle SELL positions with AI leverage for shorting
|
||
if (analysis.recommendation === 'SELL') {
|
||
return await this.calculateSellPositionWithLeverage(analysis)
|
||
}
|
||
|
||
// Get account balance
|
||
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
|
||
const balanceData = await balanceResponse.json()
|
||
|
||
if (!balanceData.success) {
|
||
throw new Error('Could not fetch account balance for position sizing')
|
||
}
|
||
|
||
const accountValue = balanceData.accountValue || balanceData.totalCollateral
|
||
const availableBalance = balanceData.availableBalance
|
||
|
||
console.log(`💰 Account Status: Value=$${accountValue.toFixed(2)}, Available=$${availableBalance.toFixed(2)}`)
|
||
|
||
// Get current price for entry
|
||
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
||
|
||
if (!currentPrice) {
|
||
try {
|
||
const { default: PriceFetcher } = await import('./price-fetcher')
|
||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||
console.log(`📊 Using current ${this.config?.symbol || 'SOLUSD'} price: $${currentPrice}`)
|
||
} catch (error) {
|
||
console.error('Error fetching price for position sizing, using fallback:', error)
|
||
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
||
}
|
||
}
|
||
|
||
// Calculate stop loss price from analysis
|
||
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
|
||
const direction = analysis.recommendation === 'BUY' ? 'long' : 'short'
|
||
|
||
let stopLossPrice: number
|
||
if (direction === 'long') {
|
||
stopLossPrice = currentPrice * (1 - stopLossPercent)
|
||
} else {
|
||
stopLossPrice = currentPrice * (1 + stopLossPercent)
|
||
}
|
||
|
||
console.log(`🎯 Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}, Direction=${direction}`)
|
||
|
||
// Use AI Leverage Calculator for optimal leverage
|
||
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||
accountValue,
|
||
availableBalance,
|
||
entryPrice: currentPrice,
|
||
stopLossPrice,
|
||
side: direction,
|
||
maxLeverageAllowed: this.config!.maxLeverage || 20, // Platform max leverage
|
||
safetyBuffer: 0.10 // 10% safety buffer between liquidation and stop loss
|
||
})
|
||
|
||
// Calculate final position size
|
||
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
|
||
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
|
||
const tokenAmount = leveragedAmount / currentPrice
|
||
|
||
console.log(`<EFBFBD> AI Position Result:`, {
|
||
baseAmount: `$${baseAmount.toFixed(2)}`,
|
||
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
|
||
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
|
||
tokenAmount: tokenAmount.toFixed(4),
|
||
riskLevel: leverageResult.riskAssessment,
|
||
reasoning: leverageResult.reasoning
|
||
})
|
||
|
||
return {
|
||
tokenAmount,
|
||
leverageUsed: leverageResult.recommendedLeverage,
|
||
marginRequired: leverageResult.marginRequired,
|
||
liquidationPrice: leverageResult.liquidationPrice,
|
||
riskAssessment: leverageResult.riskAssessment
|
||
}
|
||
}
|
||
|
||
// ✅ NEW: Calculate SOL amount to sell for SELL orders
|
||
private async calculateSellAmount(analysis: any): Promise<number> {
|
||
try {
|
||
// Get current SOL holdings from recent open trades
|
||
const openTrades = await prisma.trade.findMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
status: 'OPEN',
|
||
side: 'BUY' // Only BUY trades represent SOL holdings
|
||
},
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
let totalSOLHoldings = 0
|
||
for (const trade of openTrades) {
|
||
totalSOLHoldings += trade.amount
|
||
}
|
||
|
||
// Risk-adjusted sell amount (don't sell everything at once)
|
||
const riskAdjustment = this.config!.riskPercentage / 100
|
||
const confidenceAdjustment = analysis.confidence / 100
|
||
const sellAmount = totalSOLHoldings * riskAdjustment * confidenceAdjustment
|
||
|
||
console.log(`💰 SELL Position calculation: ${totalSOLHoldings.toFixed(4)} SOL holdings × ${(riskAdjustment * confidenceAdjustment * 100).toFixed(1)}% = ${sellAmount.toFixed(4)} SOL to sell`)
|
||
|
||
return Math.max(sellAmount, 0.001) // Minimum 0.001 SOL
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error calculating sell amount:', error)
|
||
return 0.01 // Fallback: sell 0.01 SOL
|
||
}
|
||
}
|
||
|
||
// ✅ NEW: Calculate leveraged short position for SELL orders
|
||
private async calculateSellPositionWithLeverage(analysis: any): Promise<{
|
||
tokenAmount: number
|
||
leverageUsed: number
|
||
marginRequired: number
|
||
liquidationPrice: number
|
||
riskAssessment: string
|
||
}> {
|
||
try {
|
||
console.log('📉 Calculating SELL position with AI leverage...')
|
||
|
||
// Get account balance for leverage calculation
|
||
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
|
||
const balanceData = await balanceResponse.json()
|
||
|
||
const accountValue = balanceData.accountValue || balanceData.totalCollateral
|
||
const availableBalance = balanceData.availableBalance
|
||
|
||
// Get current price
|
||
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
||
if (!currentPrice) {
|
||
const { default: PriceFetcher } = await import('./price-fetcher')
|
||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||
}
|
||
|
||
// Calculate stop loss for short position (above entry price)
|
||
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
|
||
const stopLossPrice = currentPrice * (1 + stopLossPercent)
|
||
|
||
console.log(`🎯 SHORT Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}`)
|
||
|
||
// Use AI leverage for short position
|
||
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||
accountValue,
|
||
availableBalance,
|
||
entryPrice: currentPrice,
|
||
stopLossPrice,
|
||
side: 'short',
|
||
maxLeverageAllowed: this.config!.maxLeverage || 20,
|
||
safetyBuffer: 0.10
|
||
})
|
||
|
||
// Calculate leveraged short amount
|
||
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
|
||
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
|
||
const tokenAmount = leveragedAmount / currentPrice
|
||
|
||
console.log(`📉 SELL Position with AI Leverage:`, {
|
||
baseAmount: `$${baseAmount.toFixed(2)}`,
|
||
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
|
||
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
|
||
tokenAmount: tokenAmount.toFixed(4),
|
||
riskLevel: leverageResult.riskAssessment,
|
||
reasoning: leverageResult.reasoning
|
||
})
|
||
|
||
return {
|
||
tokenAmount,
|
||
leverageUsed: leverageResult.recommendedLeverage,
|
||
marginRequired: leverageResult.marginRequired,
|
||
liquidationPrice: leverageResult.liquidationPrice,
|
||
riskAssessment: leverageResult.riskAssessment
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error calculating SELL position with leverage:', error)
|
||
return {
|
||
tokenAmount: 0.01, // Fallback small amount
|
||
leverageUsed: 1,
|
||
marginRequired: 0,
|
||
liquidationPrice: 0,
|
||
riskAssessment: 'HIGH'
|
||
}
|
||
}
|
||
}
|
||
|
||
private calculateStopLoss(analysis: any): number {
|
||
// ✅ AI-FIRST: Use AI analysis stopLoss if available
|
||
if (analysis.stopLoss?.price) {
|
||
const currentPrice = analysis.entry?.price || 189
|
||
const stopLossPrice = analysis.stopLoss.price
|
||
|
||
// Convert absolute price to percentage
|
||
if (analysis.recommendation === 'BUY') {
|
||
return ((currentPrice - stopLossPrice) / currentPrice) * 100
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return ((stopLossPrice - currentPrice) / currentPrice) * 100
|
||
}
|
||
}
|
||
|
||
// If AI provides explicit stop loss percentage, use it
|
||
if (analysis.stopLossPercent) {
|
||
return analysis.stopLossPercent
|
||
}
|
||
|
||
// Fallback: Dynamic stop loss based on market volatility (AI-calculated)
|
||
// AI determines volatility-based stop loss (0.5% to 2% range)
|
||
return this.calculateAIStopLoss(analysis)
|
||
}
|
||
|
||
private calculateTakeProfit(analysis: any): number {
|
||
// ✅ AI-FIRST: Use AI analysis takeProfit if available
|
||
if (analysis.takeProfits?.tp1?.price) {
|
||
const currentPrice = analysis.entry?.price || 150
|
||
const takeProfitPrice = analysis.takeProfits.tp1.price
|
||
|
||
// Convert absolute price to percentage
|
||
if (analysis.recommendation === 'BUY') {
|
||
return ((takeProfitPrice - currentPrice) / currentPrice) * 100
|
||
} else if (analysis.recommendation === 'SELL') {
|
||
return ((currentPrice - takeProfitPrice) / currentPrice) * 100
|
||
}
|
||
}
|
||
|
||
// If AI provides explicit take profit percentage, use it
|
||
if (analysis.takeProfitPercent) {
|
||
return analysis.takeProfitPercent
|
||
}
|
||
|
||
// Fallback: Dynamic take profit based on AI risk/reward optimization
|
||
return this.calculateAITakeProfit(analysis)
|
||
}
|
||
|
||
// AI-calculated dynamic stop loss based on volatility and market conditions
|
||
private calculateAIStopLoss(analysis: any): number {
|
||
// Extract confidence and market sentiment for adaptive stop loss
|
||
const confidence = analysis.confidence || 70
|
||
const volatility = analysis.marketConditions?.volatility || 'MEDIUM'
|
||
|
||
// Base stop loss percentages (proven to work from our testing)
|
||
let baseStopLoss = 0.8 // 0.8% base (proven effective)
|
||
|
||
// Adjust based on volatility
|
||
if (volatility === 'HIGH') {
|
||
baseStopLoss = 1.2 // Wider stop loss for high volatility
|
||
} else if (volatility === 'LOW') {
|
||
baseStopLoss = 0.5 // Tighter stop loss for low volatility
|
||
}
|
||
|
||
// Adjust based on confidence (higher confidence = tighter stop loss)
|
||
if (confidence > 85) {
|
||
baseStopLoss *= 0.8 // 20% tighter for high confidence
|
||
} else if (confidence < 70) {
|
||
baseStopLoss *= 1.3 // 30% wider for low confidence
|
||
}
|
||
|
||
return Math.max(0.3, Math.min(2.0, baseStopLoss)) // Cap between 0.3% and 2%
|
||
}
|
||
|
||
// AI-calculated dynamic take profit based on market conditions and risk/reward
|
||
private calculateAITakeProfit(analysis: any): number {
|
||
const stopLossPercent = this.calculateAIStopLoss(analysis)
|
||
const confidence = analysis.confidence || 70
|
||
|
||
// Target minimum 1.5:1 risk/reward ratio, scaled by confidence
|
||
let baseRiskReward = 1.5
|
||
|
||
if (confidence > 85) {
|
||
baseRiskReward = 2.0 // Higher reward target for high confidence
|
||
} else if (confidence < 70) {
|
||
baseRiskReward = 1.2 // Lower reward target for low confidence
|
||
}
|
||
|
||
const takeProfitPercent = stopLossPercent * baseRiskReward
|
||
return Math.max(0.5, Math.min(5.0, takeProfitPercent)) // Cap between 0.5% and 5%
|
||
}
|
||
|
||
private async executeTrade(decision: any): Promise<void> {
|
||
try {
|
||
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
|
||
|
||
let tradeResult: any
|
||
|
||
if (this.config!.mode === 'SIMULATION') {
|
||
// Execute simulation trade
|
||
tradeResult = await this.executeSimulationTrade(decision)
|
||
} else {
|
||
// Execute live trade via Drift Protocol
|
||
console.log(`💰 LIVE TRADE: $${this.config!.tradingAmount} trading amount configured`)
|
||
tradeResult = await this.executeLiveTrade(decision)
|
||
|
||
// If live trade failed, fall back to simulation for data consistency
|
||
if (!tradeResult || !tradeResult.success) {
|
||
console.log('⚠️ Live trade failed, falling back to simulation for record keeping')
|
||
tradeResult = await this.executeSimulationTrade(decision)
|
||
tradeResult.status = 'FAILED'
|
||
tradeResult.error = 'Drift Protocol execution failed'
|
||
}
|
||
}
|
||
|
||
// Store trade in database
|
||
await this.storeTrade(decision, tradeResult)
|
||
|
||
// Update stats
|
||
this.updateStats(tradeResult)
|
||
|
||
console.log(`✅ Trade executed successfully: ${tradeResult.transactionId || 'SIMULATION'}`)
|
||
|
||
// Force cleanup after successful trade execution
|
||
if (tradeResult.status !== 'FAILED') {
|
||
setTimeout(async () => {
|
||
try {
|
||
await aggressiveCleanup.forceCleanupAfterTrade()
|
||
} catch (error) {
|
||
console.error('Error in post-trade cleanup:', error)
|
||
}
|
||
}, 2000) // 2 second delay
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error executing trade:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Trade execution failed'
|
||
}
|
||
}
|
||
|
||
private async executeSimulationTrade(decision: any): Promise<any> {
|
||
// Simulate trade execution with realistic parameters
|
||
let currentPrice = decision.currentPrice
|
||
|
||
// If no current price provided, fetch real price
|
||
if (!currentPrice) {
|
||
try {
|
||
const { default: PriceFetcher } = await import('./price-fetcher')
|
||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||
console.log(`📊 Fetched real ${this.config?.symbol || 'SOLUSD'} price: $${currentPrice}`)
|
||
} catch (error) {
|
||
console.error('Error fetching real price, using fallback:', error)
|
||
// Use a more realistic fallback based on symbol
|
||
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
||
}
|
||
}
|
||
|
||
const slippage = Math.random() * 0.005 // 0-0.5% slippage
|
||
const executionPrice = currentPrice * (1 + (Math.random() > 0.5 ? slippage : -slippage))
|
||
|
||
return {
|
||
transactionId: `SIM_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||
executionPrice,
|
||
amount: decision.positionSize,
|
||
direction: decision.direction,
|
||
status: 'OPEN', // Trades start as OPEN, not COMPLETED
|
||
timestamp: new Date(),
|
||
fees: decision.positionSize * 0.001, // 0.1% fee
|
||
slippage: slippage * 100
|
||
}
|
||
}
|
||
|
||
private async executeLiveTrade(decision: any): Promise<any> {
|
||
// Execute real trade via Drift Protocol with AI-calculated leverage
|
||
console.log(`🌊 Executing Drift trade: ${decision.direction} ${this.config!.symbol}`)
|
||
console.log(`🧠 AI Leverage: ${decision.leverageUsed.toFixed(1)}x (Risk: ${decision.riskAssessment})`)
|
||
console.log(`💀 Liquidation Price: $${decision.liquidationPrice.toFixed(4)}`)
|
||
|
||
// Calculate AI-generated stop loss and take profit from analysis
|
||
const stopLossPercent = decision.stopLoss || this.calculateAIStopLoss(decision)
|
||
const takeProfitPercent = decision.takeProfit || this.calculateAITakeProfit(decision)
|
||
|
||
console.log(`🎯 AI Risk Management: SL=${stopLossPercent}%, TP=${takeProfitPercent}%`)
|
||
|
||
// Call the unified trading API endpoint that routes to Drift
|
||
const tradeResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/automation/trade`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
dexProvider: this.config!.dexProvider || 'DRIFT',
|
||
action: 'place_order',
|
||
symbol: this.config!.symbol,
|
||
amount: this.config!.tradingAmount,
|
||
side: decision.direction.toLowerCase(),
|
||
leverage: decision.leverageUsed || this.config!.maxLeverage || 2, // Use AI-calculated leverage
|
||
stopLoss: true,
|
||
takeProfit: true,
|
||
stopLossPercent: stopLossPercent,
|
||
takeProfitPercent: takeProfitPercent,
|
||
mode: this.config!.mode || 'SIMULATION',
|
||
// Include AI leverage details for logging
|
||
aiLeverageDetails: {
|
||
calculatedLeverage: decision.leverageUsed,
|
||
liquidationPrice: decision.liquidationPrice,
|
||
riskAssessment: decision.riskAssessment,
|
||
marginRequired: decision.marginRequired
|
||
}
|
||
})
|
||
})
|
||
|
||
const tradeResult = await tradeResponse.json()
|
||
|
||
// Convert Drift result to standard trade result format
|
||
if (tradeResult.success) {
|
||
return {
|
||
transactionId: tradeResult.result?.transactionId || tradeResult.result?.txId,
|
||
executionPrice: tradeResult.result?.executionPrice,
|
||
amount: tradeResult.result?.amount,
|
||
direction: decision.direction,
|
||
status: 'COMPLETED',
|
||
timestamp: new Date(),
|
||
leverage: decision.leverageUsed || tradeResult.leverageUsed || this.config!.maxLeverage,
|
||
liquidationPrice: decision.liquidationPrice,
|
||
riskAssessment: decision.riskAssessment,
|
||
stopLoss: stopLossPercent,
|
||
takeProfit: takeProfitPercent,
|
||
tradingAmount: this.config!.tradingAmount,
|
||
dexProvider: 'DRIFT'
|
||
}
|
||
} else {
|
||
throw new Error(tradeResult.error || 'Drift trade execution failed')
|
||
}
|
||
}
|
||
|
||
private async storeTrade(decision: any, result: any): Promise<void> {
|
||
try {
|
||
// Ensure we have a valid price for database storage
|
||
const executionPrice = result.executionPrice || decision.currentPrice || decision.entryPrice
|
||
|
||
if (!executionPrice) {
|
||
console.error('❌ No valid price available for trade storage. Result:', result)
|
||
console.error('❌ Decision data:', { currentPrice: decision.currentPrice, entryPrice: decision.entryPrice })
|
||
return
|
||
}
|
||
|
||
// For live trades, use the actual amounts from Drift
|
||
const tradeAmount = result.tradingAmount ? this.config!.tradingAmount : decision.positionSize
|
||
const actualAmount = result.amount || decision.positionSize
|
||
|
||
console.log(`💾 Storing trade: ${decision.direction} ${actualAmount} ${this.config!.symbol} at $${executionPrice}`)
|
||
|
||
await prisma.trade.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
side: decision.direction,
|
||
amount: actualAmount,
|
||
price: executionPrice,
|
||
status: result.status || 'COMPLETED',
|
||
driftTxId: result.transactionId || result.txId,
|
||
fees: result.fees || 0,
|
||
stopLoss: decision.stopLoss,
|
||
takeProfit: decision.takeProfit,
|
||
isAutomated: true,
|
||
tradingMode: this.config!.mode,
|
||
confidence: decision.confidence,
|
||
marketSentiment: decision.marketSentiment,
|
||
createdAt: new Date(),
|
||
// Add AI leverage information
|
||
leverage: result.leverage || decision.leverageUsed,
|
||
// Add Drift-specific fields for live trades
|
||
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
|
||
realTradingAmount: this.config!.tradingAmount,
|
||
driftTxId: result.transactionId
|
||
}),
|
||
// Add AI leverage details in metadata
|
||
metadata: JSON.stringify({
|
||
aiLeverage: {
|
||
calculatedLeverage: decision.leverageUsed,
|
||
liquidationPrice: decision.liquidationPrice,
|
||
riskAssessment: decision.riskAssessment,
|
||
marginRequired: decision.marginRequired,
|
||
balanceStrategy: result.accountValue < 1000 ? 'AGGRESSIVE_100%' : 'CONSERVATIVE_50%'
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
console.log('✅ Trade stored in database successfully')
|
||
} catch (error) {
|
||
console.error('❌ Error storing trade:', error)
|
||
}
|
||
}
|
||
|
||
private updateStats(result: any): void {
|
||
this.stats.totalTrades++
|
||
|
||
if (result.status === 'COMPLETED') {
|
||
this.stats.successfulTrades++
|
||
this.stats.winRate = (this.stats.successfulTrades / this.stats.totalTrades) * 100
|
||
|
||
// Update PnL (simplified calculation)
|
||
const pnl = result.amount * 0.01 * (Math.random() > 0.5 ? 1 : -1) // Random PnL for demo
|
||
this.stats.totalPnL += pnl
|
||
}
|
||
}
|
||
|
||
private async getTodayTradeCount(userId: string): Promise<number> {
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
const count = await prisma.trade.count({
|
||
where: {
|
||
userId,
|
||
isAutomated: true,
|
||
createdAt: {
|
||
gte: today
|
||
}
|
||
}
|
||
})
|
||
|
||
return count
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
*/
|
||
private async hasOpenPositions(): Promise<boolean> {
|
||
if (!this.config) return false
|
||
|
||
try {
|
||
const openPositions = await prisma.trade.findMany({
|
||
where: {
|
||
userId: this.config.userId,
|
||
status: 'open',
|
||
symbol: this.config.symbol
|
||
}
|
||
})
|
||
|
||
return openPositions.length > 0
|
||
} catch (error) {
|
||
console.error('Error checking open 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
|
||
}
|
||
|
||
// Reset config to prevent any residual processes
|
||
this.config = null
|
||
|
||
// Stop price monitoring
|
||
try {
|
||
await priceMonitorService.stopMonitoring()
|
||
console.log('📊 Price monitoring stopped')
|
||
} catch (error) {
|
||
console.error('Failed to stop price monitoring:', error)
|
||
}
|
||
|
||
// Update database session status to STOPPED
|
||
if (this.config) {
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config.userId,
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
status: 'STOPPED',
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
}
|
||
|
||
this.config = null
|
||
|
||
console.log('🛑 Automation stopped')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to stop automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async pauseAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.isRunning) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = false
|
||
console.log('⏸️ Automation paused')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to pause automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async resumeAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.config) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = true
|
||
console.log('▶️ Automation resumed')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to resume automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getStatus(): Promise<AutomationStatus | null> {
|
||
try {
|
||
// Get the latest active automation session from database first
|
||
const session = await prisma.automationSession.findFirst({
|
||
where: { status: 'ACTIVE' },
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
if (!session) {
|
||
return null
|
||
}
|
||
|
||
// If we have a session but automation is not running in memory,
|
||
// it means the server was restarted but the session is still active
|
||
const isActiveInMemory = this.isRunning && this.config !== null
|
||
|
||
// Auto-restart automation if session exists but not running in memory
|
||
if (!isActiveInMemory) {
|
||
console.log('🔄 Found active session but automation not running, attempting auto-restart...')
|
||
await this.autoRestartFromSession(session)
|
||
}
|
||
|
||
// Calculate next analysis timing
|
||
const analysisInterval = Math.floor(this.getIntervalFromTimeframe(session.timeframe) / 1000) // Convert to seconds
|
||
let nextAnalysisIn = 0
|
||
|
||
if (this.isRunning && session.nextScheduled) {
|
||
const nextScheduledTime = new Date(session.nextScheduled).getTime()
|
||
const currentTime = Date.now()
|
||
nextAnalysisIn = Math.max(0, Math.floor((nextScheduledTime - currentTime) / 1000))
|
||
}
|
||
|
||
return {
|
||
isActive: this.isRunning && this.config !== null,
|
||
mode: session.mode as 'SIMULATION' | 'LIVE',
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
totalTrades: session.totalTrades,
|
||
successfulTrades: session.successfulTrades,
|
||
winRate: session.winRate,
|
||
totalPnL: session.totalPnL,
|
||
errorCount: session.errorCount,
|
||
lastError: session.lastError || undefined,
|
||
lastAnalysis: session.lastAnalysis || undefined,
|
||
lastTrade: session.lastTrade || undefined,
|
||
nextScheduled: session.nextScheduled || undefined,
|
||
nextAnalysisIn: nextAnalysisIn,
|
||
analysisInterval: analysisInterval,
|
||
currentCycle: session.totalTrades || 0
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get automation status:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async autoRestartFromSession(session: any): Promise<void> {
|
||
try {
|
||
const settings = session.settings || {}
|
||
const config: AutomationConfig = {
|
||
userId: session.userId,
|
||
mode: session.mode,
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
tradingAmount: settings.tradingAmount || 100,
|
||
maxLeverage: settings.maxLeverage || 3,
|
||
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||
maxDailyTrades: settings.maxDailyTrades || 5,
|
||
riskPercentage: settings.riskPercentage || 2
|
||
}
|
||
|
||
await this.startAutomation(config)
|
||
console.log('✅ Automation auto-restarted successfully')
|
||
} catch (error) {
|
||
console.error('Failed to auto-restart automation:', error)
|
||
}
|
||
}
|
||
|
||
async getLearningInsights(userId: string): Promise<{
|
||
totalAnalyses: number
|
||
avgAccuracy: number
|
||
bestTimeframe: string
|
||
worstTimeframe: string
|
||
commonFailures: string[]
|
||
recommendations: string[]
|
||
}> {
|
||
try {
|
||
// For now, return mock data with dynamic timeframe
|
||
const selectedTimeframes = this.config?.selectedTimeframes || ['1h']
|
||
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||
|
||
return {
|
||
totalAnalyses: 150,
|
||
avgAccuracy: 0.72,
|
||
bestTimeframe: primaryTimeframe,
|
||
worstTimeframe: '15m',
|
||
commonFailures: [
|
||
'Low confidence predictions',
|
||
'Missed support/resistance levels',
|
||
'Timeframe misalignment'
|
||
],
|
||
recommendations: [
|
||
`Focus on ${primaryTimeframe} timeframe for better accuracy`,
|
||
'Wait for higher confidence signals (>75%)',
|
||
'Use multiple timeframe confirmation'
|
||
]
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get learning insights:', error)
|
||
return {
|
||
totalAnalyses: 0,
|
||
avgAccuracy: 0,
|
||
bestTimeframe: 'Unknown',
|
||
worstTimeframe: 'Unknown',
|
||
commonFailures: [],
|
||
recommendations: []
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Trigger analysis based on price movement alerts
|
||
*/
|
||
private async triggerPriceBasedAnalysis(
|
||
trigger: 'TP_APPROACH' | 'SL_APPROACH' | 'CRITICAL',
|
||
data: any
|
||
): Promise<void> {
|
||
if (!this.config || !this.isRunning) {
|
||
console.log('❌ Cannot trigger price-based analysis: automation not running')
|
||
return
|
||
}
|
||
|
||
const sessionId = `price-trigger-${Date.now()}`
|
||
|
||
try {
|
||
console.log(`🔥 Price-based analysis triggered by ${trigger} for ${data.symbol}`)
|
||
console.log(`📊 Current price: $${data.currentPrice}, Target: $${data.targetPrice}`)
|
||
|
||
// Create progress tracker for this analysis
|
||
const steps = [
|
||
{ id: 'trigger', title: 'Triggered by price movement', description: 'Analysis initiated by price alert', status: 'pending' as ProgressStatus },
|
||
{ id: 'screenshot', title: 'Capturing screenshots', description: 'Taking fresh market screenshots', status: 'pending' as ProgressStatus },
|
||
{ id: 'analysis', title: 'Running AI analysis', description: 'Analyzing current market conditions', status: 'pending' as ProgressStatus },
|
||
{ id: 'evaluation', title: 'Evaluating position', description: 'Checking position adjustments', status: 'pending' as ProgressStatus },
|
||
{ id: 'complete', title: 'Analysis complete', description: 'Price-based analysis finished', status: 'pending' as ProgressStatus }
|
||
]
|
||
|
||
progressTracker.createSession(sessionId, steps)
|
||
|
||
progressTracker.updateStep(sessionId, 'trigger', 'active', `${trigger}: ${data.symbol} at $${data.currentPrice}`)
|
||
|
||
// Run enhanced screenshot capture with current symbol/timeframe
|
||
progressTracker.updateStep(sessionId, 'screenshot', 'active')
|
||
|
||
const screenshotConfig = {
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
layouts: ['ai', 'diy'],
|
||
sessionId
|
||
}
|
||
|
||
const screenshots = await enhancedScreenshotService.captureWithLogin(screenshotConfig)
|
||
|
||
if (!screenshots || screenshots.length === 0) {
|
||
throw new Error('Failed to capture screenshots for price-based analysis')
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'screenshot', 'completed', `Captured ${screenshots.length} screenshots`)
|
||
progressTracker.updateStep(sessionId, 'analysis', 'active')
|
||
|
||
// Simplified analysis call - just use the first screenshot
|
||
const analysisResult = await aiAnalysisService.analyzeScreenshot(screenshots[0])
|
||
|
||
if (!analysisResult) {
|
||
throw new Error('AI analysis returned null result')
|
||
}
|
||
|
||
progressTracker.updateStep(sessionId, 'analysis', 'completed', `Analysis: ${analysisResult.recommendation}`)
|
||
progressTracker.updateStep(sessionId, 'evaluation', 'active')
|
||
|
||
// Store the triggered analysis in trading journal
|
||
await prisma.tradingJournal.create({
|
||
data: {
|
||
userId: this.config.userId,
|
||
screenshotUrl: screenshots[0] || '',
|
||
aiAnalysis: analysisResult.reasoning || 'No analysis available',
|
||
confidence: analysisResult.confidence || 0,
|
||
recommendation: analysisResult.recommendation || 'HOLD',
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
sessionId,
|
||
notes: `Price-triggered analysis: ${trigger} - Current: $${data.currentPrice}, Target: $${data.targetPrice}`,
|
||
marketSentiment: analysisResult.marketSentiment || 'Unknown',
|
||
tradingMode: this.config.mode,
|
||
isAutomated: true,
|
||
priceAtAnalysis: data.currentPrice,
|
||
marketCondition: trigger,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
|
||
// 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()
|