526 lines
15 KiB
TypeScript
526 lines
15 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'
|
|
|
|
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`)
|
|
|
|
// Create 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: Make trading decision
|
|
const tradeDecision = await this.makeTradeDecision(analysisResult)
|
|
if (!tradeDecision) {
|
|
console.log('📊 No trading opportunity found')
|
|
return
|
|
}
|
|
|
|
// Step 5: 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('📸 Taking screenshot and analyzing...')
|
|
|
|
const screenshotConfig = {
|
|
symbol: this.config!.symbol,
|
|
timeframe: this.config!.timeframe,
|
|
layouts: ['ai', 'diy']
|
|
}
|
|
|
|
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig)
|
|
|
|
if (!result.analysis || result.screenshots.length === 0) {
|
|
console.log('❌ No analysis or screenshots captured')
|
|
return null
|
|
}
|
|
|
|
console.log(`✅ Analysis completed: ${result.analysis.recommendation} with ${result.analysis.confidence}% confidence`)
|
|
return result
|
|
|
|
} catch (error) {
|
|
console.error('Error performing analysis:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
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 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 {
|
|
const currentPrice = analysis.currentPrice || 0
|
|
const stopLossPercent = this.config!.stopLossPercent / 100
|
|
|
|
if (analysis.direction === 'LONG') {
|
|
return currentPrice * (1 - stopLossPercent)
|
|
} else {
|
|
return currentPrice * (1 + stopLossPercent)
|
|
}
|
|
}
|
|
|
|
private calculateTakeProfit(analysis: any): number {
|
|
const currentPrice = analysis.currentPrice || 0
|
|
const takeProfitPercent = this.config!.takeProfitPercent / 100
|
|
|
|
if (analysis.direction === 'LONG') {
|
|
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
|
|
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 {
|
|
if (!this.config) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
isActive: this.isRunning,
|
|
mode: this.config.mode,
|
|
symbol: this.config.symbol,
|
|
timeframe: this.config.timeframe,
|
|
totalTrades: this.stats.totalTrades,
|
|
successfulTrades: this.stats.successfulTrades,
|
|
winRate: this.stats.winRate,
|
|
totalPnL: this.stats.totalPnL,
|
|
errorCount: this.stats.errorCount,
|
|
lastError: this.stats.lastError || undefined,
|
|
lastAnalysis: new Date(),
|
|
lastTrade: new Date()
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get automation status:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
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()
|