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