- Enhanced frontend trade display with comprehensive analysis details * Added trigger analysis showing original trade signals and confidence * Added current metrics for active trades (P&L, time in trade, price changes) * Added exit analysis for completed trades (accuracy, actual vs expected R/R) * Added detailed trade context explaining analysis-trade relationships - Fixed automation session persistence after server restarts * Modified getStatus() to check database first instead of in-memory state * Added auto-restart functionality when active session exists but automation stopped * Improved session tracking and state management - Enhanced API response structure * Added triggerAnalysis, currentMetrics, exitMetrics to trade objects * Added analysisContext explaining signal changes (BUY → HOLD scenarios) * Added comprehensive trade quality assessment and performance tracking Features: Detailed analysis-trade correlation display Real-time P&L tracking for active trades Analysis accuracy assessment for completed trades Automation session persistence across server restarts Enhanced trade information with meaningful context
836 lines
27 KiB
TypeScript
836 lines
27 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
||
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
||
import { jupiterDEXService } from './jupiter-dex-service'
|
||
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
||
import { TradingViewCredentials } from './tradingview-automation'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
export interface AutomationConfig {
|
||
userId: string
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
tradingAmount: number
|
||
maxLeverage: number
|
||
stopLossPercent: number
|
||
takeProfitPercent: number
|
||
maxDailyTrades: number
|
||
riskPercentage: number
|
||
}
|
||
|
||
export interface AutomationStatus {
|
||
isActive: boolean
|
||
mode: 'SIMULATION' | 'LIVE'
|
||
symbol: string
|
||
timeframe: string
|
||
totalTrades: number
|
||
successfulTrades: number
|
||
winRate: number
|
||
totalPnL: number
|
||
lastAnalysis?: Date
|
||
lastTrade?: Date
|
||
nextScheduled?: Date
|
||
errorCount: number
|
||
lastError?: string
|
||
}
|
||
|
||
export class AutomationService {
|
||
private isRunning = false
|
||
private config: AutomationConfig | null = null
|
||
private intervalId: NodeJS.Timeout | null = null
|
||
private stats = {
|
||
totalTrades: 0,
|
||
successfulTrades: 0,
|
||
winRate: 0,
|
||
totalPnL: 0,
|
||
errorCount: 0,
|
||
lastError: null as string | null
|
||
}
|
||
|
||
async startAutomation(config: AutomationConfig): Promise<boolean> {
|
||
try {
|
||
if (this.isRunning) {
|
||
throw new Error('Automation is already running')
|
||
}
|
||
|
||
this.config = config
|
||
this.isRunning = true
|
||
|
||
console.log(`🤖 Starting automation for ${config.symbol} ${config.timeframe} in ${config.mode} mode`)
|
||
|
||
// 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: config.stopLossPercent,
|
||
takeProfitPercent: config.takeProfitPercent,
|
||
maxDailyTrades: config.maxDailyTrades,
|
||
riskPercentage: config.riskPercentage
|
||
},
|
||
startBalance: config.tradingAmount,
|
||
currentBalance: config.tradingAmount,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
|
||
// Start automation cycle
|
||
this.startAutomationCycle()
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to start automation:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
return false
|
||
}
|
||
}
|
||
|
||
private startAutomationCycle(): void {
|
||
if (!this.config) return
|
||
|
||
// Get interval in milliseconds based on timeframe
|
||
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
|
||
|
||
console.log(`🔄 Starting automation cycle every ${intervalMs/1000} seconds`)
|
||
|
||
this.intervalId = setInterval(async () => {
|
||
if (this.isRunning && this.config) {
|
||
await this.runAutomationCycle()
|
||
}
|
||
}, intervalMs)
|
||
|
||
// Run first cycle immediately
|
||
this.runAutomationCycle()
|
||
}
|
||
|
||
private getIntervalFromTimeframe(timeframe: string): number {
|
||
const intervals: { [key: string]: number } = {
|
||
'1m': 60 * 1000,
|
||
'3m': 3 * 60 * 1000,
|
||
'5m': 5 * 60 * 1000,
|
||
'15m': 15 * 60 * 1000,
|
||
'30m': 30 * 60 * 1000,
|
||
'1h': 60 * 60 * 1000,
|
||
'2h': 2 * 60 * 60 * 1000,
|
||
'4h': 4 * 60 * 60 * 1000,
|
||
'1d': 24 * 60 * 60 * 1000
|
||
}
|
||
|
||
return intervals[timeframe] || intervals['1h'] // Default to 1 hour
|
||
}
|
||
|
||
private async runAutomationCycle(): Promise<void> {
|
||
if (!this.config) return
|
||
|
||
try {
|
||
console.log(`🔍 Running automation cycle for ${this.config.symbol} ${this.config.timeframe}`)
|
||
|
||
// Step 1: Check daily trade limit
|
||
const todayTrades = await this.getTodayTradeCount(this.config.userId)
|
||
if (todayTrades >= this.config.maxDailyTrades) {
|
||
console.log(`📊 Daily trade limit reached (${todayTrades}/${this.config.maxDailyTrades})`)
|
||
return
|
||
}
|
||
|
||
// Step 2: Take screenshot and analyze
|
||
const analysisResult = await this.performAnalysis()
|
||
if (!analysisResult) {
|
||
console.log('❌ Analysis failed, skipping cycle')
|
||
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')
|
||
return
|
||
}
|
||
|
||
// Step 6: Execute trade
|
||
await this.executeTrade(tradeDecision)
|
||
|
||
} catch (error) {
|
||
console.error('Error in automation cycle:', error)
|
||
this.stats.errorCount++
|
||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||
}
|
||
}
|
||
|
||
private async performAnalysis(): Promise<{
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} | null> {
|
||
try {
|
||
console.log('📸 Starting multi-timeframe analysis with dual layouts...')
|
||
|
||
// Multi-timeframe analysis: 15m, 1h, 2h, 4h
|
||
const timeframes = ['15', '1h', '2h', '4h']
|
||
const symbol = this.config!.symbol
|
||
|
||
console.log(`🔍 Analyzing ${symbol} across timeframes: ${timeframes.join(', ')} with AI + DIY layouts`)
|
||
|
||
// Analyze each timeframe with both AI and DIY layouts
|
||
const multiTimeframeResults = await this.analyzeMultiTimeframeWithDualLayouts(symbol, timeframes)
|
||
|
||
if (multiTimeframeResults.length === 0) {
|
||
console.log('❌ No multi-timeframe analysis results')
|
||
return null
|
||
}
|
||
|
||
// Process and combine multi-timeframe results
|
||
const combinedResult = this.combineMultiTimeframeAnalysis(multiTimeframeResults)
|
||
|
||
if (!combinedResult.analysis) {
|
||
console.log('❌ Failed to combine multi-timeframe analysis')
|
||
return null
|
||
}
|
||
|
||
console.log(`✅ Multi-timeframe analysis completed: ${combinedResult.analysis.recommendation} with ${combinedResult.analysis.confidence}% confidence`)
|
||
console.log(`📊 Timeframe alignment: ${this.analyzeTimeframeAlignment(multiTimeframeResults)}`)
|
||
|
||
return combinedResult
|
||
|
||
} catch (error) {
|
||
console.error('Error performing multi-timeframe analysis:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async analyzeMultiTimeframeWithDualLayouts(
|
||
symbol: string,
|
||
timeframes: string[]
|
||
): Promise<Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>> {
|
||
const results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }> = []
|
||
|
||
for (const timeframe of timeframes) {
|
||
try {
|
||
console.log(`📊 Analyzing ${symbol} ${timeframe} with AI + DIY layouts...`)
|
||
|
||
// Use the dual-layout configuration for each timeframe
|
||
const screenshotConfig = {
|
||
symbol: symbol,
|
||
timeframe: timeframe,
|
||
layouts: ['ai', 'diy']
|
||
}
|
||
|
||
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig)
|
||
|
||
if (result.analysis) {
|
||
console.log(`✅ ${timeframe} analysis: ${result.analysis.recommendation} (${result.analysis.confidence}% confidence)`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: result.analysis
|
||
})
|
||
} else {
|
||
console.log(`❌ ${timeframe} analysis failed`)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
|
||
// Small delay between captures to avoid overwhelming the system
|
||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||
|
||
} catch (error) {
|
||
console.error(`Failed to analyze ${symbol} ${timeframe}:`, error)
|
||
results.push({
|
||
symbol,
|
||
timeframe,
|
||
analysis: null
|
||
})
|
||
}
|
||
}
|
||
|
||
return results
|
||
}
|
||
|
||
private combineMultiTimeframeAnalysis(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
} {
|
||
const validResults = results.filter(r => r.analysis !== null)
|
||
|
||
if (validResults.length === 0) {
|
||
return { screenshots: [], analysis: null }
|
||
}
|
||
|
||
// Get the primary timeframe (1h) as base
|
||
const primaryResult = validResults.find(r => r.timeframe === '1h') || validResults[0]
|
||
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
|
||
|
||
// Calculate weighted confidence based on timeframe alignment
|
||
const alignment = this.calculateTimeframeAlignment(validResults)
|
||
const baseAnalysis = primaryResult.analysis!
|
||
|
||
// Adjust confidence based on timeframe alignment
|
||
const adjustedConfidence = Math.round(baseAnalysis.confidence * alignment.score)
|
||
|
||
// Create combined analysis with multi-timeframe reasoning
|
||
const combinedAnalysis: AnalysisResult = {
|
||
...baseAnalysis,
|
||
confidence: adjustedConfidence,
|
||
reasoning: `Multi-timeframe Dual-Layout Analysis (${results.map(r => r.timeframe).join(', ')}): ${baseAnalysis.reasoning}
|
||
|
||
📊 Each timeframe analyzed with BOTH AI layout (RSI, MACD, EMAs) and DIY layout (Stochastic RSI, VWAP, OBV)
|
||
⏱️ Timeframe Alignment: ${alignment.description}
|
||
<EFBFBD> Signal Strength: ${alignment.strength}
|
||
🎯 Confidence Adjustment: ${baseAnalysis.confidence}% → ${adjustedConfidence}% (${alignment.score >= 1 ? 'Enhanced' : 'Reduced'} due to timeframe ${alignment.score >= 1 ? 'alignment' : 'divergence'})
|
||
|
||
🔬 Analysis Details:
|
||
${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.analysis?.confidence}% confidence)`).join('\n')}`,
|
||
|
||
keyLevels: this.consolidateKeyLevels(validResults),
|
||
marketSentiment: this.consolidateMarketSentiment(validResults)
|
||
}
|
||
|
||
return {
|
||
screenshots,
|
||
analysis: combinedAnalysis
|
||
}
|
||
}
|
||
|
||
private calculateTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): {
|
||
score: number
|
||
description: string
|
||
strength: string
|
||
} {
|
||
const recommendations = results.map(r => r.analysis?.recommendation).filter(Boolean)
|
||
const buySignals = recommendations.filter(r => r === 'BUY').length
|
||
const sellSignals = recommendations.filter(r => r === 'SELL').length
|
||
const holdSignals = recommendations.filter(r => r === 'HOLD').length
|
||
|
||
const total = recommendations.length
|
||
const maxSignals = Math.max(buySignals, sellSignals, holdSignals)
|
||
const alignmentRatio = maxSignals / total
|
||
|
||
let score = 1.0
|
||
let description = ''
|
||
let strength = ''
|
||
|
||
if (alignmentRatio >= 0.75) {
|
||
score = 1.2 // Boost confidence
|
||
description = `Strong alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Strong'
|
||
} else if (alignmentRatio >= 0.5) {
|
||
score = 1.0 // Neutral
|
||
description = `Moderate alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Moderate'
|
||
} else {
|
||
score = 0.8 // Reduce confidence
|
||
description = `Weak alignment (${maxSignals}/${total} timeframes agree)`
|
||
strength = 'Weak'
|
||
}
|
||
|
||
return { score, description, strength }
|
||
}
|
||
|
||
private consolidateKeyLevels(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): any {
|
||
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
|
||
if (allLevels.length === 0) return {}
|
||
|
||
// Use the 1h timeframe levels as primary, or first available
|
||
const primaryLevels = results.find(r => r.timeframe === '1h')?.analysis?.keyLevels || allLevels[0]
|
||
|
||
return {
|
||
...primaryLevels,
|
||
note: `Consolidated from ${allLevels.length} timeframes`
|
||
}
|
||
}
|
||
|
||
private consolidateMarketSentiment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): 'BULLISH' | 'BEARISH' | 'NEUTRAL' {
|
||
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
|
||
if (sentiments.length === 0) return 'NEUTRAL'
|
||
|
||
// Use the 1h timeframe sentiment as primary, or first available
|
||
const primarySentiment = results.find(r => r.timeframe === '1h')?.analysis?.marketSentiment || sentiments[0]
|
||
|
||
return primarySentiment || 'NEUTRAL'
|
||
}
|
||
|
||
private analyzeTimeframeAlignment(results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }>): string {
|
||
const recommendations = results.map(r => ({
|
||
timeframe: r.timeframe,
|
||
recommendation: r.analysis?.recommendation,
|
||
confidence: r.analysis?.confidence || 0
|
||
}))
|
||
|
||
const summary = recommendations.map(r => `${r.timeframe}: ${r.recommendation} (${r.confidence}%)`).join(', ')
|
||
return summary
|
||
}
|
||
|
||
private async storeAnalysisForLearning(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
await prisma.aILearningData.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
screenshot: result.screenshots[0] || '',
|
||
analysisData: JSON.stringify(result.analysis),
|
||
marketConditions: JSON.stringify({
|
||
marketSentiment: result.analysis.marketSentiment,
|
||
keyLevels: result.analysis.keyLevels,
|
||
timestamp: new Date().toISOString()
|
||
}),
|
||
confidenceScore: result.analysis.confidence,
|
||
createdAt: new Date()
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('Error storing analysis for learning:', error)
|
||
}
|
||
}
|
||
|
||
private async updateSessionWithAnalysis(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<void> {
|
||
try {
|
||
if (!result.analysis) return
|
||
|
||
// Store the analysis decision in a field that works for now
|
||
const analysisDecision = `${result.analysis.recommendation} with ${result.analysis.confidence}% confidence - ${result.analysis.summary}`
|
||
|
||
// Update the current session with the latest analysis
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
timeframe: this.config!.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
lastAnalysis: new Date(),
|
||
lastError: analysisDecision // Temporarily store analysis here
|
||
}
|
||
})
|
||
|
||
// Also log the analysis for debugging
|
||
console.log('📊 Analysis stored in database:', {
|
||
recommendation: result.analysis.recommendation,
|
||
confidence: result.analysis.confidence,
|
||
summary: result.analysis.summary
|
||
})
|
||
} catch (error) {
|
||
console.error('Failed to update session with analysis:', error)
|
||
}
|
||
}
|
||
|
||
private async makeTradeDecision(result: {
|
||
screenshots: string[]
|
||
analysis: AnalysisResult | null
|
||
}): Promise<any | null> {
|
||
try {
|
||
const analysis = result.analysis
|
||
if (!analysis) return null
|
||
|
||
// Only trade if confidence is high enough
|
||
if (analysis.confidence < 70) {
|
||
console.log(`📊 Confidence too low: ${analysis.confidence}%`)
|
||
return null
|
||
}
|
||
|
||
// Only trade if direction is clear
|
||
if (analysis.recommendation === 'HOLD') {
|
||
console.log('📊 No clear direction signal')
|
||
return null
|
||
}
|
||
|
||
// Calculate position size based on risk percentage
|
||
const positionSize = this.calculatePositionSize(analysis)
|
||
|
||
return {
|
||
direction: analysis.recommendation,
|
||
confidence: analysis.confidence,
|
||
positionSize,
|
||
stopLoss: this.calculateStopLoss(analysis),
|
||
takeProfit: this.calculateTakeProfit(analysis),
|
||
marketSentiment: analysis.marketSentiment
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error making trade decision:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private calculatePositionSize(analysis: any): number {
|
||
const baseAmount = this.config!.tradingAmount
|
||
const riskAdjustment = this.config!.riskPercentage / 100
|
||
const confidenceAdjustment = analysis.confidence / 100
|
||
|
||
return baseAmount * riskAdjustment * confidenceAdjustment
|
||
}
|
||
|
||
private calculateStopLoss(analysis: any): number {
|
||
// Use AI analysis stopLoss if available, otherwise calculate from entry price
|
||
if (analysis.stopLoss?.price) {
|
||
return analysis.stopLoss.price
|
||
}
|
||
|
||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
||
const stopLossPercent = this.config!.stopLossPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 - stopLossPercent)
|
||
} else {
|
||
return currentPrice * (1 + stopLossPercent)
|
||
}
|
||
}
|
||
|
||
private calculateTakeProfit(analysis: any): number {
|
||
// Use AI analysis takeProfit if available, otherwise calculate from entry price
|
||
if (analysis.takeProfits?.tp1?.price) {
|
||
return analysis.takeProfits.tp1.price
|
||
}
|
||
|
||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
||
const takeProfitPercent = this.config!.takeProfitPercent / 100
|
||
|
||
if (analysis.recommendation === 'BUY') {
|
||
return currentPrice * (1 + takeProfitPercent)
|
||
} else {
|
||
return currentPrice * (1 - takeProfitPercent)
|
||
}
|
||
}
|
||
|
||
private async executeTrade(decision: any): Promise<void> {
|
||
try {
|
||
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
|
||
|
||
let tradeResult: any
|
||
|
||
if (this.config!.mode === 'SIMULATION') {
|
||
// Execute simulation trade
|
||
tradeResult = await this.executeSimulationTrade(decision)
|
||
} else {
|
||
// Execute live trade via Jupiter
|
||
tradeResult = await this.executeLiveTrade(decision)
|
||
}
|
||
|
||
// Store trade in database
|
||
await this.storeTrade(decision, tradeResult)
|
||
|
||
// Update stats
|
||
this.updateStats(tradeResult)
|
||
|
||
console.log(`✅ Trade executed successfully: ${tradeResult.transactionId || 'SIMULATION'}`)
|
||
|
||
} 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
|
||
const currentPrice = decision.currentPrice || 100 // Mock price
|
||
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: '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 Jupiter DEX
|
||
const inputToken = decision.direction === 'BUY' ? 'USDC' : 'SOL'
|
||
const outputToken = decision.direction === 'BUY' ? 'SOL' : 'USDC'
|
||
|
||
const tokens = {
|
||
SOL: 'So11111111111111111111111111111111111111112',
|
||
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||
}
|
||
|
||
return await jupiterDEXService.executeSwap(
|
||
tokens[inputToken as keyof typeof tokens],
|
||
tokens[outputToken as keyof typeof tokens],
|
||
decision.positionSize,
|
||
50 // 0.5% slippage
|
||
)
|
||
}
|
||
|
||
private async storeTrade(decision: any, result: any): Promise<void> {
|
||
try {
|
||
await prisma.trade.create({
|
||
data: {
|
||
userId: this.config!.userId,
|
||
symbol: this.config!.symbol,
|
||
side: decision.direction,
|
||
amount: decision.positionSize,
|
||
price: result.executionPrice,
|
||
status: result.status,
|
||
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()
|
||
}
|
||
})
|
||
} 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
|
||
}
|
||
|
||
// Update database session status to STOPPED
|
||
if (this.config) {
|
||
await prisma.automationSession.updateMany({
|
||
where: {
|
||
userId: this.config.userId,
|
||
symbol: this.config.symbol,
|
||
timeframe: this.config.timeframe,
|
||
status: 'ACTIVE'
|
||
},
|
||
data: {
|
||
status: 'STOPPED',
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
}
|
||
|
||
this.config = null
|
||
|
||
console.log('🛑 Automation stopped')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to stop automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async pauseAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.isRunning) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = false
|
||
console.log('⏸️ Automation paused')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to pause automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async resumeAutomation(): Promise<boolean> {
|
||
try {
|
||
if (!this.config) {
|
||
return false
|
||
}
|
||
|
||
this.isRunning = true
|
||
console.log('▶️ Automation resumed')
|
||
return true
|
||
} catch (error) {
|
||
console.error('Failed to resume automation:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getStatus(): Promise<AutomationStatus | null> {
|
||
try {
|
||
// Get the latest active automation session from database first
|
||
const session = await prisma.automationSession.findFirst({
|
||
where: { status: 'ACTIVE' },
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
if (!session) {
|
||
return null
|
||
}
|
||
|
||
// If we have a session but automation is not running in memory,
|
||
// it means the server was restarted but the session is still active
|
||
const isActiveInMemory = this.isRunning && this.config !== null
|
||
|
||
// Auto-restart automation if session exists but not running in memory
|
||
if (!isActiveInMemory) {
|
||
console.log('🔄 Found active session but automation not running, attempting auto-restart...')
|
||
await this.autoRestartFromSession(session)
|
||
}
|
||
|
||
return {
|
||
isActive: this.isRunning && this.config !== null,
|
||
mode: session.mode as 'SIMULATION' | 'LIVE',
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
totalTrades: session.totalTrades,
|
||
successfulTrades: session.successfulTrades,
|
||
winRate: session.winRate,
|
||
totalPnL: session.totalPnL,
|
||
errorCount: session.errorCount,
|
||
lastError: session.lastError || undefined,
|
||
lastAnalysis: session.lastAnalysis || undefined,
|
||
lastTrade: session.lastTrade || undefined,
|
||
nextScheduled: session.nextScheduled || undefined
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get automation status:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
private async autoRestartFromSession(session: any): Promise<void> {
|
||
try {
|
||
const settings = session.settings || {}
|
||
const config: AutomationConfig = {
|
||
userId: session.userId,
|
||
mode: session.mode,
|
||
symbol: session.symbol,
|
||
timeframe: session.timeframe,
|
||
tradingAmount: settings.tradingAmount || 100,
|
||
maxLeverage: settings.maxLeverage || 3,
|
||
stopLossPercent: settings.stopLossPercent || 2,
|
||
takeProfitPercent: settings.takeProfitPercent || 6,
|
||
maxDailyTrades: settings.maxDailyTrades || 5,
|
||
riskPercentage: settings.riskPercentage || 2
|
||
}
|
||
|
||
await this.startAutomation(config)
|
||
console.log('✅ Automation auto-restarted successfully')
|
||
} catch (error) {
|
||
console.error('Failed to auto-restart automation:', error)
|
||
}
|
||
}
|
||
|
||
async getLearningInsights(userId: string): Promise<{
|
||
totalAnalyses: number
|
||
avgAccuracy: number
|
||
bestTimeframe: string
|
||
worstTimeframe: string
|
||
commonFailures: string[]
|
||
recommendations: string[]
|
||
}> {
|
||
try {
|
||
// For now, return mock data
|
||
return {
|
||
totalAnalyses: 150,
|
||
avgAccuracy: 0.72,
|
||
bestTimeframe: '1h',
|
||
worstTimeframe: '15m',
|
||
commonFailures: [
|
||
'Low confidence predictions',
|
||
'Missed support/resistance levels',
|
||
'Timeframe misalignment'
|
||
],
|
||
recommendations: [
|
||
'Focus on 1h timeframe for better accuracy',
|
||
'Wait for higher confidence signals (>75%)',
|
||
'Use multiple timeframe confirmation'
|
||
]
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get learning insights:', error)
|
||
return {
|
||
totalAnalyses: 0,
|
||
avgAccuracy: 0,
|
||
bestTimeframe: 'Unknown',
|
||
worstTimeframe: 'Unknown',
|
||
commonFailures: [],
|
||
recommendations: []
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export const automationService = new AutomationService()
|