Files
trading_bot_v3/lib/automation-service.ts
mindesbunister 4d319e3102 feat: Remove manual TP/SL inputs - Enable full AI-powered risk management
- Removed stop loss and take profit input fields from automation-v2 page
- Updated AutomationConfig interfaces to remove manual TP/SL parameters
- Implemented dynamic AI risk calculation methods:
  * calculateAIStopLoss() - Volatility and confidence-based SL calculation
  * calculateAITakeProfit() - Risk/reward optimized TP calculation
- Added AI Risk Management information panel explaining automated calculation
- Enhanced risk management logic to use AI-generated values first, then fallback to dynamic calculation
- Supports ultra-tight scalping percentages (0.3% to 2% SL range)
- AI adapts risk based on market volatility, confidence levels, and learned patterns
- Proven effective with real trades: 0.8% SL / 1.5% TP achieving 1.50% profit

This enables fully autonomous AI risk management without manual user intervention,
allowing the AI to optimize stop loss and take profit levels based on technical
analysis, market conditions, and continuous learning from real trade outcomes.
2025-07-24 10:31:46 +02:00

816 lines
25 KiB
TypeScript

import { PrismaClient } from '@prisma/client'
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
import { jupiterDEXService } from './jupiter-dex-service'
import { TradingViewCredentials } from './tradingview-automation'
const prisma = new PrismaClient()
export interface AutomationConfig {
userId: string
mode: 'SIMULATION' | 'LIVE'
symbol: string
timeframe: string
selectedTimeframes: string[] // Multi-timeframe support
tradingAmount: number
maxLeverage: number
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
maxDailyTrades: number
riskPercentage: number
dexProvider: 'JUPITER' | 'DRIFT'
}
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 activeSession: any = null
private intervalId: NodeJS.Timeout | null = null
private isRunning = false
private credentials: TradingViewCredentials | null = null
constructor() {
this.initialize()
}
private async initialize() {
// Load credentials from environment or database
this.credentials = {
email: process.env.TRADINGVIEW_EMAIL || '',
password: process.env.TRADINGVIEW_PASSWORD || ''
}
}
async startAutomation(config: AutomationConfig): Promise<boolean> {
try {
if (this.isRunning) {
throw new Error('Automation is already running')
}
// Validate configuration
if (!config.userId || !config.symbol || !config.timeframe) {
throw new Error('Invalid automation configuration')
}
// Create or update automation session
const existingSession = await prisma.automationSession.findFirst({
where: {
userId: config.userId,
symbol: config.symbol,
timeframe: config.timeframe
}
})
let session
if (existingSession) {
session = await prisma.automationSession.update({
where: { id: existingSession.id },
data: {
status: 'ACTIVE',
mode: config.mode,
settings: config as any,
updatedAt: new Date()
}
})
} else {
session = await prisma.automationSession.create({
data: {
userId: config.userId,
status: 'ACTIVE',
mode: config.mode,
symbol: config.symbol,
timeframe: config.timeframe,
settings: config as any
}
})
}
this.activeSession = session
this.isRunning = true
// Start the automation loop
this.startAutomationLoop(config)
console.log(`🤖 Automation started for ${config.symbol} ${config.timeframe} in ${config.mode} mode`)
return true
} catch (error) {
console.error('Failed to start automation:', error)
return false
}
}
async stopAutomation(): Promise<boolean> {
try {
if (!this.isRunning) {
return true
}
// Clear interval
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
// Update session status
if (this.activeSession) {
await prisma.automationSession.update({
where: { id: this.activeSession.id },
data: {
status: 'STOPPED',
updatedAt: new Date()
}
})
}
this.isRunning = false
this.activeSession = 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 || !this.activeSession) {
return false
}
// Clear interval but keep session
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
// Update session status
await prisma.automationSession.update({
where: { id: this.activeSession.id },
data: {
status: 'PAUSED',
updatedAt: new Date()
}
})
console.log('⏸️ Automation paused')
return true
} catch (error) {
console.error('Failed to pause automation:', error)
return false
}
}
async resumeAutomation(): Promise<boolean> {
try {
if (!this.activeSession) {
return false
}
// Update session status
await prisma.automationSession.update({
where: { id: this.activeSession.id },
data: {
status: 'ACTIVE',
updatedAt: new Date()
}
})
// Restart automation loop
const config = this.activeSession.settings as AutomationConfig
this.startAutomationLoop(config)
console.log('▶️ Automation resumed')
return true
} catch (error) {
console.error('Failed to resume automation:', error)
return false
}
}
private startAutomationLoop(config: AutomationConfig) {
// Calculate interval based on timeframe
const intervalMs = this.getIntervalFromTimeframe(config.timeframe)
console.log(`🔄 Starting automation loop every ${intervalMs/1000/60} minutes`)
this.intervalId = setInterval(async () => {
try {
await this.executeAutomationCycle(config)
} catch (error) {
console.error('Automation cycle error:', error)
await this.handleAutomationError(error)
}
}, intervalMs)
// Execute first cycle immediately
setTimeout(async () => {
try {
await this.executeAutomationCycle(config)
} catch (error) {
console.error('Initial automation cycle error:', error)
await this.handleAutomationError(error)
}
}, 5000) // 5 second delay for initialization
}
private async executeAutomationCycle(config: AutomationConfig) {
console.log(`🔄 Executing automation cycle for ${config.symbol} ${config.timeframe}`)
// Check for open positions first (instead of daily trade limit)
const hasOpenPosition = await this.checkForOpenPositions(config)
if (hasOpenPosition) {
console.log(`📊 Open position detected for ${config.symbol}, monitoring only`)
return
}
// Generate session ID for progress tracking
const sessionId = `auto_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`
// Step 1: Capture screenshot and analyze
const screenshotConfig = {
symbol: config.symbol,
timeframe: config.timeframe,
layouts: ['ai', 'diy'],
sessionId,
analyze: true
}
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(screenshotConfig)
if (!result.analysis || result.screenshots.length === 0) {
console.log('❌ Failed to capture or analyze chart')
return
}
// Step 2: Store analysis in database for learning (only if analysis exists)
if (result.analysis) {
await this.storeAnalysisForLearning(config, { ...result, analysis: result.analysis }, sessionId)
}
// Step 3: Check if we should execute trade
const shouldTrade = await this.shouldExecuteTrade(result.analysis, config)
if (!shouldTrade) {
console.log('📊 Analysis does not meet trading criteria')
return
}
// Step 4: Execute trade based on analysis
await this.executeTrade(config, result.analysis, result.screenshots[0])
// Step 5: Update session statistics
await this.updateSessionStats(config.userId)
}
private async storeAnalysisForLearning(
config: AutomationConfig,
result: { screenshots: string[], analysis: AnalysisResult },
sessionId: string
) {
try {
// Store in trading journal
await prisma.tradingJournal.create({
data: {
userId: config.userId,
screenshotUrl: result.screenshots.join(','),
aiAnalysis: JSON.stringify(result.analysis),
marketSentiment: result.analysis.marketSentiment,
keyLevels: result.analysis.keyLevels,
recommendation: result.analysis.recommendation,
confidence: result.analysis.confidence,
symbol: config.symbol,
timeframe: config.timeframe,
tradingMode: config.mode,
sessionId: sessionId,
priceAtAnalysis: result.analysis.entry?.price
}
})
// Store in AI learning data
await prisma.aILearningData.create({
data: {
userId: config.userId,
sessionId: sessionId,
analysisData: result.analysis as any,
marketConditions: {
timeframe: config.timeframe,
symbol: config.symbol,
timestamp: new Date().toISOString()
},
confidenceScore: result.analysis.confidence,
timeframe: config.timeframe,
symbol: config.symbol,
screenshot: result.screenshots[0],
predictedPrice: result.analysis.entry?.price
}
})
console.log('📚 Analysis stored for learning')
} catch (error) {
console.error('Failed to store analysis for learning:', error)
}
}
private async shouldExecuteTrade(analysis: AnalysisResult, config: AutomationConfig): Promise<boolean> {
// Check minimum confidence threshold
if (analysis.confidence < 70) {
console.log(`📊 Confidence too low: ${analysis.confidence}%`)
return false
}
// Check if recommendation is actionable
if (analysis.recommendation === 'HOLD') {
console.log('📊 Recommendation is HOLD')
return false
}
// Check if we have required trading levels
if (!analysis.entry || !analysis.stopLoss) {
console.log('📊 Missing entry or stop loss levels')
return false
}
// Check risk/reward ratio
if (analysis.riskToReward) {
const rr = this.parseRiskReward(analysis.riskToReward)
if (rr < 2) {
console.log(`📊 Risk/reward ratio too low: ${rr}`)
return false
}
}
// Check recent performance for dynamic adjustments
const recentPerformance = await this.getRecentPerformance(config.userId)
if (recentPerformance.winRate < 0.4 && recentPerformance.totalTrades > 10) {
console.log('📊 Recent performance too poor, requiring higher confidence')
return analysis.confidence > 80
}
return true
}
private async checkForOpenPositions(config: AutomationConfig): Promise<boolean> {
try {
console.log(`🔍 Checking for open positions for ${config.symbol}`)
// For Jupiter DEX, we don't have persistent positions like in Drift
// This method would need to be implemented based on your specific needs
// For now, return false to allow trading
if (config.dexProvider === 'DRIFT') {
// Check Drift positions via API
const response = await fetch('http://localhost:3000/api/drift/positions')
if (!response.ok) {
console.warn('⚠️ Could not fetch Drift positions, assuming no open positions')
return false
}
const data = await response.json()
if (!data.success || !data.positions) {
return false
}
// Check if there's an open position for the current symbol
const symbolBase = config.symbol.replace('USD', '') // SOLUSD -> SOL
const openPosition = data.positions.find((pos: any) =>
pos.symbol.includes(symbolBase) && pos.size > 0.001
)
if (openPosition) {
console.log(`📊 Found open ${openPosition.side} position: ${openPosition.symbol} ${openPosition.size}`)
return true
}
}
return false
} catch (error) {
console.error('❌ Error checking positions:', error)
// On error, assume no positions to allow trading
return false
}
}
private async executeTrade(config: AutomationConfig, analysis: AnalysisResult, screenshotUrl: string) {
try {
console.log(`🚀 Executing ${config.mode} trade: ${analysis.recommendation} ${config.symbol}`)
const side = analysis.recommendation === 'BUY' ? 'BUY' : 'SELL'
const amount = await this.calculateTradeAmount(config, analysis)
const leverage = Math.min(config.maxLeverage, 3) // Cap at 3x for safety
let tradeResult: any = null
if (config.mode === 'SIMULATION') {
// Simulate trade
tradeResult = await this.simulateTrade({
symbol: config.symbol,
side,
amount,
price: analysis.entry?.price || 0,
stopLoss: analysis.stopLoss?.price,
takeProfit: analysis.takeProfits?.tp1?.price,
leverage
})
} else {
// Execute real trade via unified trading endpoint
tradeResult = await this.executeUnifiedTrade({
symbol: config.symbol,
side,
amount,
stopLoss: analysis.stopLoss?.price,
takeProfit: analysis.takeProfits?.tp1?.price,
leverage,
dexProvider: config.dexProvider
})
}
// Store trade in database
await prisma.trade.create({
data: {
userId: config.userId,
symbol: config.symbol,
side,
amount,
price: analysis.entry?.price || 0,
status: tradeResult?.success ? 'FILLED' : 'FAILED',
isAutomated: true,
entryPrice: analysis.entry?.price,
stopLoss: analysis.stopLoss?.price,
takeProfit: analysis.takeProfits?.tp1?.price,
leverage,
timeframe: config.timeframe,
tradingMode: config.mode,
confidence: analysis.confidence,
marketSentiment: analysis.marketSentiment,
screenshotUrl,
aiAnalysis: JSON.stringify(analysis),
driftTxId: tradeResult?.txId,
executedAt: new Date()
}
})
console.log(`✅ Trade executed: ${tradeResult?.success ? 'SUCCESS' : 'FAILED'}`)
} catch (error) {
console.error('Trade execution error:', error)
// Store failed trade
await prisma.trade.create({
data: {
userId: config.userId,
symbol: config.symbol,
side: analysis.recommendation === 'BUY' ? 'BUY' : 'SELL',
amount: config.tradingAmount,
price: analysis.entry?.price || 0,
status: 'FAILED',
isAutomated: true,
timeframe: config.timeframe,
tradingMode: config.mode,
confidence: analysis.confidence,
marketSentiment: analysis.marketSentiment,
screenshotUrl,
aiAnalysis: JSON.stringify(analysis)
}
})
}
}
private async executeUnifiedTrade(params: {
symbol: string
side: string
amount: number
stopLoss?: number
takeProfit?: number
leverage?: number
dexProvider: 'JUPITER' | 'DRIFT'
}): Promise<{ success: boolean; txId?: string }> {
try {
console.log(`🚀 Executing ${params.dexProvider} trade: ${params.side} ${params.amount} ${params.symbol}`)
const response = await fetch('http://localhost:3000/api/automation/trade', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
symbol: params.symbol,
side: params.side,
amount: params.amount,
leverage: params.leverage,
stopLoss: params.stopLoss,
takeProfit: params.takeProfit,
dexProvider: params.dexProvider,
mode: 'LIVE'
})
})
if (!response.ok) {
throw new Error(`Trade request failed: ${response.statusText}`)
}
const result = await response.json()
return {
success: result.success,
txId: result.txId || result.transactionId
}
} catch (error) {
console.error('Unified trade execution error:', error)
return { success: false }
}
}
private async simulateTrade(params: {
symbol: string
side: string
amount: number
price: number
stopLoss?: number
takeProfit?: number
leverage?: number
}): Promise<{ success: boolean; txId?: string }> {
// Simulate realistic execution with small random variation
const priceVariation = 0.001 * (Math.random() - 0.5) // ±0.1%
const executedPrice = params.price * (1 + priceVariation)
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 500))
return {
success: true,
txId: `sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`
}
}
private async calculateTradeAmount(config: AutomationConfig, analysis: AnalysisResult): Promise<number> {
try {
// Fetch actual account balance from Drift
console.log('💰 Fetching account balance for position sizing...')
const balanceResponse = await fetch(`http://localhost:3000/api/drift/balance`)
if (!balanceResponse.ok) {
console.log('⚠️ Failed to fetch balance, using fallback calculation')
// Fallback to config amount
let amount = Math.min(config.tradingAmount, 35) // Cap at $35 max
const riskAdjustment = config.riskPercentage / 100
return Math.max(amount * riskAdjustment, 5)
}
const balanceData = await balanceResponse.json()
const availableBalance = parseFloat(balanceData.availableBalance || '0')
console.log(`💰 Available balance: $${availableBalance}`)
if (availableBalance <= 0) {
throw new Error('No available balance')
}
// Calculate position size based on risk percentage of available balance
const riskAmount = availableBalance * (config.riskPercentage / 100)
// Adjust based on confidence (reduce risk for low confidence signals)
const confidenceMultiplier = Math.min(analysis.confidence / 100, 1)
let amount = riskAmount * confidenceMultiplier
// Apply leverage to get position size
amount *= Math.min(config.maxLeverage, 10)
// Ensure minimum trade amount but cap at available balance
amount = Math.max(amount, 5) // Minimum $5 position
amount = Math.min(amount, availableBalance * 0.8) // Never use more than 80% of balance
console.log(`📊 Position sizing calculation:`)
console.log(` - Available balance: $${availableBalance}`)
console.log(` - Risk percentage: ${config.riskPercentage}%`)
console.log(` - Risk amount: $${riskAmount.toFixed(2)}`)
console.log(` - Confidence multiplier: ${confidenceMultiplier}`)
console.log(` - Leverage: ${Math.min(config.maxLeverage, 10)}x`)
console.log(` - Final position size: $${amount.toFixed(2)}`)
return Math.round(amount * 100) / 100 // Round to 2 decimal places
} catch (error) {
console.log(`⚠️ Error calculating trade amount: ${error instanceof Error ? error.message : String(error)}`)
// Safe fallback - use small fixed amount
return 5
}
}
private parseRiskReward(rrString: string): number {
// Parse "1:2.5" format
const parts = rrString.split(':')
if (parts.length === 2) {
return parseFloat(parts[1]) / parseFloat(parts[0])
}
return 0
}
private getIntervalFromTimeframe(timeframe: string): number {
const intervals: { [key: string]: number } = {
'1m': 1 * 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,
'6h': 6 * 60 * 60 * 1000,
'12h': 12 * 60 * 60 * 1000,
'1d': 24 * 60 * 60 * 1000
}
return intervals[timeframe] || intervals['1h'] // Default to 1 hour
}
private async getRecentPerformance(userId: string): Promise<{
winRate: number
totalTrades: number
avgRR: number
}> {
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const trades = await prisma.trade.findMany({
where: {
userId,
isAutomated: true,
createdAt: {
gte: thirtyDaysAgo
},
status: 'FILLED'
}
})
const totalTrades = trades.length
const winningTrades = trades.filter(t => (t.pnlPercent || 0) > 0).length
const winRate = totalTrades > 0 ? winningTrades / totalTrades : 0
const avgRR = trades.reduce((sum, t) => sum + (t.actualRR || 0), 0) / Math.max(totalTrades, 1)
return { winRate, totalTrades, avgRR }
}
private async updateSessionStats(userId: string) {
try {
if (!this.activeSession) return
const stats = await this.getRecentPerformance(userId)
await prisma.automationSession.update({
where: { id: this.activeSession.id },
data: {
totalTrades: stats.totalTrades,
successfulTrades: Math.round(stats.totalTrades * stats.winRate),
winRate: stats.winRate,
avgRiskReward: stats.avgRR,
lastAnalysis: new Date()
}
})
} catch (error) {
console.error('Failed to update session stats:', error)
}
}
private async handleAutomationError(error: any) {
try {
if (this.activeSession) {
await prisma.automationSession.update({
where: { id: this.activeSession.id },
data: {
errorCount: { increment: 1 },
lastError: error.message || 'Unknown error',
updatedAt: new Date()
}
})
// Stop automation if too many errors
if (this.activeSession.errorCount >= 5) {
console.log('❌ Too many errors, stopping automation')
await this.stopAutomation()
}
}
} catch (dbError) {
console.error('Failed to handle automation error:', dbError)
}
}
async getStatus(): Promise<AutomationStatus | null> {
try {
if (!this.activeSession) {
return null
}
const session = await prisma.automationSession.findUnique({
where: { id: this.activeSession.id }
})
if (!session) {
return null
}
return {
isActive: this.isRunning,
mode: session.mode as 'SIMULATION' | 'LIVE',
symbol: session.symbol,
timeframe: session.timeframe,
totalTrades: session.totalTrades,
successfulTrades: session.successfulTrades,
winRate: session.winRate,
totalPnL: session.totalPnL,
lastAnalysis: session.lastAnalysis || undefined,
lastTrade: session.lastTrade || undefined,
nextScheduled: session.nextScheduled || undefined,
errorCount: session.errorCount,
lastError: session.lastError || undefined
}
} 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 {
const learningData = await prisma.aILearningData.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 100
})
const totalAnalyses = learningData.length
const avgAccuracy = learningData.reduce((sum, d) => sum + (d.accuracyScore || 0), 0) / Math.max(totalAnalyses, 1)
// Group by timeframe to find best/worst
const timeframeStats = learningData.reduce((acc, d) => {
if (!acc[d.timeframe]) {
acc[d.timeframe] = { count: 0, accuracy: 0 }
}
acc[d.timeframe].count += 1
acc[d.timeframe].accuracy += d.accuracyScore || 0
return acc
}, {} as { [key: string]: { count: number, accuracy: number } })
const timeframes = Object.entries(timeframeStats).map(([tf, stats]) => ({
timeframe: tf,
avgAccuracy: stats.accuracy / stats.count
}))
const bestTimeframe = timeframes.sort((a, b) => b.avgAccuracy - a.avgAccuracy)[0]?.timeframe || 'Unknown'
const worstTimeframe = timeframes.sort((a, b) => a.avgAccuracy - b.avgAccuracy)[0]?.timeframe || 'Unknown'
return {
totalAnalyses,
avgAccuracy,
bestTimeframe,
worstTimeframe,
commonFailures: [
'Low confidence predictions',
'Missed support/resistance levels',
'Timeframe misalignment'
],
recommendations: [
'Focus on higher timeframes for better accuracy',
'Wait for higher confidence signals',
'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()