- Add isRunning check in runCycle to prevent zombie automation cycles - Enhance status reporting with detailed status and next action descriptions - Add clear logging for start/stop operations with isRunning status - Fix disconnect between background intervals and UI status display - Stop button should now work properly when automation is actually running UI will now correctly show when automation is running vs stopped
510 lines
20 KiB
JavaScript
510 lines
20 KiB
JavaScript
// Simple automation service for basic start/stop functionality
|
||
|
||
// Import AI Leverage Calculator for dynamic leverage
|
||
async function importAILeverageCalculator() {
|
||
try {
|
||
const { AILeverageCalculator } = await import('./ai-leverage-calculator.js');
|
||
return AILeverageCalculator;
|
||
} catch (error) {
|
||
console.warn('⚠️ AI Leverage Calculator not available, using default leverage');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
class SimpleAutomation {
|
||
constructor() {
|
||
this.isRunning = false;
|
||
this.config = null;
|
||
this.intervalId = null;
|
||
this.stats = {
|
||
totalCycles: 0,
|
||
totalTrades: 0,
|
||
startTime: null,
|
||
lastActivity: null,
|
||
status: 'Stopped',
|
||
networkErrors: 0,
|
||
consecutiveErrors: 0
|
||
};
|
||
}
|
||
|
||
async start(config) {
|
||
try {
|
||
if (this.isRunning) {
|
||
return { success: false, message: 'Automation already running' };
|
||
}
|
||
|
||
// Validate basic config
|
||
if (!config.selectedTimeframes || config.selectedTimeframes.length === 0) {
|
||
return { success: false, message: 'At least one timeframe required' };
|
||
}
|
||
|
||
this.config = config;
|
||
this.isRunning = true;
|
||
this.stats.startTime = new Date().toISOString();
|
||
this.stats.status = 'Running';
|
||
|
||
console.log('✅ AUTOMATION STATUS: isRunning =', this.isRunning);
|
||
console.log('🎯 LIVE TRADING:', this.config.enableTrading ? 'ENABLED' : 'DISABLED');
|
||
this.stats.totalCycles = 0;
|
||
|
||
// Auto-enable trading when in LIVE mode
|
||
if (config.mode === 'LIVE') {
|
||
this.config.enableTrading = true;
|
||
console.log('🔥 LIVE TRADING ENABLED: Real trades will be executed');
|
||
} else {
|
||
this.config.enableTrading = false;
|
||
console.log('📊 SIMULATION MODE: Trades will be simulated only');
|
||
}
|
||
|
||
// Detect strategy
|
||
const timeframes = config.selectedTimeframes;
|
||
let strategy = 'General';
|
||
|
||
const isScalping = timeframes.includes('5') || timeframes.includes('15') || timeframes.includes('30');
|
||
const isDayTrading = timeframes.includes('60') || timeframes.includes('120');
|
||
const isSwingTrading = timeframes.includes('240') || timeframes.includes('D');
|
||
|
||
if (isScalping) strategy = 'Scalping';
|
||
else if (isDayTrading) strategy = 'Day Trading';
|
||
else if (isSwingTrading) strategy = 'Swing Trading';
|
||
|
||
console.log('SIMPLE AUTOMATION: Starting ' + strategy + ' strategy');
|
||
console.log('TIMEFRAMES: [' + timeframes.join(', ') + ']');
|
||
console.log('MODE: ' + (config.mode || 'SIMULATION'));
|
||
|
||
// Start simple monitoring cycle (10 minutes for safety)
|
||
this.intervalId = setInterval(() => {
|
||
this.runCycle();
|
||
}, 10 * 60 * 1000); // 10 minutes
|
||
|
||
// First cycle after 30 seconds
|
||
setTimeout(() => {
|
||
this.runCycle();
|
||
}, 30000);
|
||
|
||
return {
|
||
success: true,
|
||
message: strategy + ' automation started successfully',
|
||
strategy: strategy,
|
||
timeframes: timeframes
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('Failed to start automation:', error);
|
||
return { success: false, message: 'Failed to start automation: ' + error.message };
|
||
}
|
||
}
|
||
|
||
async stop() {
|
||
try {
|
||
console.log('🛑 STOPPING AUTOMATION...');
|
||
this.isRunning = false;
|
||
this.stats.status = 'Stopped';
|
||
console.log('✅ AUTOMATION STATUS: isRunning =', this.isRunning);
|
||
|
||
if (this.intervalId) {
|
||
clearInterval(this.intervalId);
|
||
this.intervalId = null;
|
||
}
|
||
|
||
console.log('SIMPLE AUTOMATION: Stopped');
|
||
|
||
return { success: true, message: 'Automation stopped successfully' };
|
||
} catch (error) {
|
||
console.error('Failed to stop automation:', error);
|
||
return { success: false, message: 'Failed to stop automation: ' + error.message };
|
||
}
|
||
}
|
||
|
||
async runCycle() {
|
||
try {
|
||
// Check if automation should still be running
|
||
if (!this.isRunning) {
|
||
console.log('⏹️ AUTOMATION STOPPED: Skipping cycle');
|
||
return;
|
||
}
|
||
|
||
this.stats.totalCycles++;
|
||
this.stats.lastActivity = new Date().toISOString();
|
||
|
||
console.log('🔄 AUTOMATION CYCLE ' + this.stats.totalCycles + ': ' + (this.config?.symbol || 'SOLUSD'));
|
||
console.log('⏰ TIME: ' + new Date().toLocaleTimeString());
|
||
console.log('📊 STRATEGY: ' + (this.config.selectedTimeframes?.join(', ') || 'N/A') + ' timeframes');
|
||
|
||
if (this.config) {
|
||
// Perform actual analysis using enhanced screenshot API
|
||
await this.performAnalysis();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error in automation cycle:', error);
|
||
}
|
||
}
|
||
|
||
async performAnalysis() {
|
||
console.log(`📊 TRUE PARALLEL ANALYSIS: Starting simultaneous analysis for ${this.config.selectedTimeframes.length} timeframes...`);
|
||
console.log(`🚀 This will capture ${this.config.selectedTimeframes.length * 2} screenshots in parallel (${this.config.selectedTimeframes.length} timeframes × 2 layouts)`);
|
||
|
||
try {
|
||
// Use internal container port for server-side API calls
|
||
const baseUrl = process.env.INTERNAL_API_URL || 'http://localhost:3000';
|
||
const response = await fetch(`${baseUrl}/api/batch-analysis`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol: this.config.symbol,
|
||
timeframes: this.config.selectedTimeframes, // All timeframes at once!
|
||
layouts: ['ai', 'diy'],
|
||
analyze: true
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.warn(`⚠️ BATCH API ERROR: ${response.status}, falling back to sequential...`);
|
||
return this.performSequentialAnalysis();
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.analysis) {
|
||
// Reset consecutive error counter on success
|
||
this.stats.consecutiveErrors = 0;
|
||
|
||
console.log(`✅ PARALLEL ANALYSIS COMPLETE: ${result.totalScreenshots} screenshots captured`);
|
||
console.log(`📊 COMBINED Recommendation: ${result.analysis.recommendation}`);
|
||
console.log(`🎯 COMBINED Confidence: ${result.analysis.confidence}%`);
|
||
console.log(`⏰ Timeframes analyzed: ${result.timeframes.join(', ')}`);
|
||
|
||
// Check if we should execute a trade based on combined analysis
|
||
if (this.shouldExecuteTrade(result.analysis)) {
|
||
console.log('💰 TRADE SIGNAL: Executing trade...');
|
||
await this.executeTrade(result.analysis);
|
||
} else {
|
||
console.log('📈 SIGNAL: Combined analysis complete, no trade executed');
|
||
}
|
||
|
||
return;
|
||
} else {
|
||
console.warn('⚠️ BATCH ANALYSIS: No valid data, falling back to sequential...');
|
||
return this.performSequentialAnalysis();
|
||
}
|
||
} catch (error) {
|
||
// Track network errors
|
||
this.stats.networkErrors++;
|
||
this.stats.consecutiveErrors++;
|
||
|
||
console.error(`❌ PARALLEL ANALYSIS FAILED (Network Error #${this.stats.networkErrors}):`, error.message);
|
||
|
||
// If too many consecutive errors, slow down
|
||
if (this.stats.consecutiveErrors >= 3) {
|
||
console.warn(`⚠️ HIGH NETWORK ERROR COUNT: ${this.stats.consecutiveErrors} consecutive failures. System will continue but may slow down.`);
|
||
}
|
||
|
||
console.log(`🔄 FALLBACK: Using sequential analysis...`);
|
||
return this.performSequentialAnalysis();
|
||
}
|
||
}
|
||
|
||
// Fallback sequential analysis method
|
||
async performSequentialAnalysis() {
|
||
try {
|
||
console.log('📸 SEQUENTIAL ANALYSIS: Starting...');
|
||
|
||
const allResults = [];
|
||
|
||
// Analyze each timeframe separately
|
||
for (const timeframe of this.config.selectedTimeframes) {
|
||
console.log(`📊 ANALYZING: ${timeframe} timeframe...`);
|
||
|
||
// Use the enhanced screenshot API for each timeframe
|
||
const baseUrl = process.env.INTERNAL_API_URL || 'http://localhost:3000';
|
||
const response = await fetch(`${baseUrl}/api/enhanced-screenshot`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol: this.config.symbol,
|
||
timeframe: timeframe, // Send one timeframe at a time
|
||
layouts: ['ai', 'diy'],
|
||
analyze: true
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.log(`⚠️ TIMEFRAME ${timeframe}: API error ${response.status}`);
|
||
continue;
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.analysis) {
|
||
console.log(`✅ TIMEFRAME ${timeframe}: ${result.analysis.recommendation} (${result.analysis.confidence}%)`);
|
||
allResults.push({
|
||
timeframe: timeframe,
|
||
analysis: result.analysis,
|
||
screenshots: result.screenshots
|
||
});
|
||
} else {
|
||
console.log(`⚠️ TIMEFRAME ${timeframe}: No analysis data`);
|
||
}
|
||
}
|
||
|
||
if (allResults.length > 0) {
|
||
console.log('✅ MULTI-TIMEFRAME ANALYSIS COMPLETE:');
|
||
|
||
// Combine results for overall decision
|
||
const combinedAnalysis = this.combineTimeframeAnalysis(allResults);
|
||
|
||
console.log('📊 COMBINED Recommendation: ' + combinedAnalysis.recommendation);
|
||
console.log('🎯 COMBINED Confidence: ' + combinedAnalysis.confidence + '%');
|
||
console.log('⏰ Timeframes analyzed: ' + allResults.map(r => r.timeframe).join(', '));
|
||
|
||
// Check if we should execute a trade based on combined analysis
|
||
if (this.shouldExecuteTrade(combinedAnalysis)) {
|
||
console.log('💰 TRADE SIGNAL: Executing trade...');
|
||
await this.executeTrade(combinedAnalysis);
|
||
} else {
|
||
console.log('📈 SIGNAL: Combined analysis complete, no trade executed');
|
||
}
|
||
} else {
|
||
console.log('⚠️ ANALYSIS: No valid analysis data from any timeframe');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ ANALYSIS ERROR:', error.message);
|
||
}
|
||
}
|
||
|
||
combineTimeframeAnalysis(results) {
|
||
if (results.length === 1) {
|
||
return results[0].analysis;
|
||
}
|
||
|
||
// Simple combination logic - can be enhanced
|
||
const recommendations = results.map(r => r.analysis.recommendation?.toLowerCase() || 'hold');
|
||
const confidences = results.map(r => r.analysis.confidence || 0);
|
||
|
||
// Count votes for each recommendation
|
||
const votes = {};
|
||
recommendations.forEach(rec => {
|
||
votes[rec] = (votes[rec] || 0) + 1;
|
||
});
|
||
|
||
// Get majority recommendation
|
||
const majorityRec = Object.keys(votes).reduce((a, b) => votes[a] > votes[b] ? a : b);
|
||
|
||
// Average confidence, but boost if multiple timeframes agree
|
||
const avgConfidence = confidences.reduce((sum, conf) => sum + conf, 0) / confidences.length;
|
||
const agreementBonus = votes[majorityRec] > 1 ? 10 : 0; // +10% if multiple agree
|
||
const finalConfidence = Math.min(95, avgConfidence + agreementBonus);
|
||
|
||
console.log(`🔄 CONSENSUS: ${majorityRec.toUpperCase()} from ${votes[majorityRec]}/${results.length} timeframes`);
|
||
|
||
return {
|
||
recommendation: majorityRec,
|
||
confidence: Math.round(finalConfidence),
|
||
reasoning: `Multi-timeframe consensus from ${results.length} timeframes (${results.map(r => r.timeframe).join(', ')})`,
|
||
timeframeResults: results
|
||
};
|
||
}
|
||
|
||
shouldExecuteTrade(analysis) {
|
||
// Always allow trade execution - the useRealDEX flag determines if it's real or simulated
|
||
console.log(`<EFBFBD> TRADE MODE: ${this.config.mode || 'SIMULATION'} - Trading ${this.config.enableTrading ? 'ENABLED' : 'DISABLED'}`);
|
||
|
||
const recommendation = analysis.recommendation?.toLowerCase() || '';
|
||
const confidence = analysis.confidence || 0;
|
||
|
||
// Strategy-specific confidence requirements
|
||
let minConfidence = 75;
|
||
if (this.config.selectedTimeframes?.includes('5') || this.config.selectedTimeframes?.includes('15')) {
|
||
minConfidence = 80; // Higher confidence for scalping
|
||
}
|
||
|
||
const isHighConfidence = confidence >= minConfidence;
|
||
const isClearDirection = recommendation.includes('buy') || recommendation.includes('sell');
|
||
|
||
console.log('🎯 TRADE DECISION: ' + recommendation + ' (' + confidence + '%) - Min: ' + minConfidence + '%');
|
||
|
||
return isHighConfidence && isClearDirection;
|
||
}
|
||
|
||
async executeTrade(analysis) {
|
||
try {
|
||
console.log('💰 EXECUTING TRADE...');
|
||
console.log('📊 Analysis data:', JSON.stringify(analysis, null, 2));
|
||
|
||
// Map analysis recommendation to trading side
|
||
const recommendation = analysis.recommendation?.toLowerCase() || '';
|
||
let side = '';
|
||
|
||
if (recommendation.includes('buy')) {
|
||
side = 'BUY';
|
||
} else if (recommendation.includes('sell')) {
|
||
side = 'SELL';
|
||
} else {
|
||
console.log('❌ TRADE SKIP: Invalid recommendation - ' + recommendation);
|
||
return { success: false, error: 'Invalid recommendation: ' + recommendation };
|
||
}
|
||
|
||
// Extract stop loss and take profit from analysis
|
||
let stopLoss = null;
|
||
let takeProfit = null;
|
||
|
||
// Try to extract from the structured analysis format
|
||
if (analysis.stopLoss && typeof analysis.stopLoss === 'object') {
|
||
stopLoss = analysis.stopLoss.price;
|
||
} else if (analysis.stopLoss && typeof analysis.stopLoss === 'number') {
|
||
stopLoss = analysis.stopLoss;
|
||
}
|
||
|
||
// Extract take profit - prefer tp1 if available
|
||
if (analysis.takeProfits && typeof analysis.takeProfits === 'object') {
|
||
if (analysis.takeProfits.tp1 && analysis.takeProfits.tp1.price) {
|
||
takeProfit = analysis.takeProfits.tp1.price;
|
||
} else if (analysis.takeProfits.tp2 && analysis.takeProfits.tp2.price) {
|
||
takeProfit = analysis.takeProfits.tp2.price;
|
||
}
|
||
} else if (analysis.takeProfit && typeof analysis.takeProfit === 'number') {
|
||
takeProfit = analysis.takeProfit;
|
||
}
|
||
|
||
// Fallback: try to extract from nested levels object
|
||
if (!stopLoss && analysis.levels) {
|
||
stopLoss = analysis.levels.stopLoss || analysis.levels.stop;
|
||
}
|
||
if (!takeProfit && analysis.levels) {
|
||
takeProfit = analysis.levels.takeProfit || analysis.levels.target;
|
||
}
|
||
|
||
// Parse numeric values if they're strings
|
||
if (stopLoss && typeof stopLoss === 'string') {
|
||
stopLoss = parseFloat(stopLoss.replace(/[^0-9.]/g, ''));
|
||
}
|
||
if (takeProfit && typeof takeProfit === 'string') {
|
||
takeProfit = parseFloat(takeProfit.replace(/[^0-9.]/g, ''));
|
||
}
|
||
|
||
console.log(`🎯 Trade levels - SL: ${stopLoss}, TP: ${takeProfit}`);
|
||
|
||
// Calculate optimal leverage using AI Leverage Calculator
|
||
let optimalLeverage = 1; // Default fallback
|
||
console.log('🔧 DEBUG: Starting leverage calculation...');
|
||
try {
|
||
const AILeverageCalculator = await importAILeverageCalculator();
|
||
console.log('🔧 DEBUG: AI Leverage Calculator imported:', !!AILeverageCalculator);
|
||
if (AILeverageCalculator && stopLoss) {
|
||
console.log('🔧 DEBUG: Both calculator and stopLoss available, proceeding...');
|
||
// Get current price from analysis
|
||
const currentPrice = analysis.entry?.price || analysis.currentPrice || 178; // fallback price
|
||
|
||
// Get real account data from Drift API
|
||
let accountValue = 49; // fallback
|
||
let availableBalance = 49; // fallback
|
||
|
||
try {
|
||
const baseUrl = process.env.INTERNAL_API_URL || 'http://localhost:3000';
|
||
console.log('🔧 DEBUG: Fetching balance from:', baseUrl);
|
||
const balanceResponse = await fetch(`${baseUrl}/api/drift/balance`);
|
||
const balanceData = await balanceResponse.json();
|
||
if (balanceData.success) {
|
||
accountValue = balanceData.accountValue;
|
||
availableBalance = balanceData.availableBalance;
|
||
console.log(`💰 Real account data: $${accountValue.toFixed(2)} total, $${availableBalance.toFixed(2)} available`);
|
||
} else {
|
||
console.log('🔧 DEBUG: Balance API returned error:', balanceData);
|
||
}
|
||
} catch (balanceError) {
|
||
console.warn('⚠️ Failed to get real balance, using fallback:', balanceError.message);
|
||
}
|
||
|
||
console.log(`🧮 Calculating optimal leverage: Entry=$${currentPrice}, StopLoss=$${stopLoss}`);
|
||
|
||
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||
accountValue,
|
||
availableBalance,
|
||
entryPrice: currentPrice,
|
||
stopLossPrice: stopLoss,
|
||
side: side === 'BUY' ? 'long' : 'short',
|
||
maxLeverageAllowed: 10, // Drift platform max
|
||
safetyBuffer: 0.10 // 10% safety buffer
|
||
});
|
||
|
||
optimalLeverage = leverageResult.recommendedLeverage;
|
||
console.log(`🎯 AI Calculated Leverage: ${optimalLeverage.toFixed(1)}x (Risk: ${leverageResult.riskAssessment})`);
|
||
console.log(`📊 Leverage Details: ${leverageResult.reasoning}`);
|
||
} else {
|
||
console.log('🔧 DEBUG: Skipping leverage calc - Calculator:', !!AILeverageCalculator, 'StopLoss:', !!stopLoss);
|
||
}
|
||
} catch (leverageError) {
|
||
console.warn('⚠️ Leverage calculation failed, using default 1x:', leverageError.message);
|
||
}
|
||
|
||
console.log(`🔧 DEBUG: Final leverage to use: ${optimalLeverage}x`);
|
||
|
||
// Use the trading API with proper fields for Drift
|
||
const tradePayload = {
|
||
symbol: this.config.symbol,
|
||
side: side,
|
||
amount: this.config.tradingAmount || 49, // Use available balance
|
||
leverage: optimalLeverage, // Use AI-calculated optimal leverage
|
||
stopLoss: stopLoss,
|
||
takeProfit: takeProfit,
|
||
useRealDEX: this.config.enableTrading === true, // Use real DEX when live trading enabled
|
||
analysis: analysis // Include analysis for reference
|
||
};
|
||
|
||
console.log('📊 TRADE PAYLOAD:', tradePayload);
|
||
|
||
const baseUrl = process.env.INTERNAL_API_URL || 'http://localhost:3000';
|
||
const response = await fetch(`${baseUrl}/api/trading/execute-drift`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(tradePayload)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
console.log('✅ TRADE EXECUTED: ' + result.message);
|
||
this.stats.totalTrades = (this.stats.totalTrades || 0) + 1;
|
||
this.stats.successfulTrades = (this.stats.successfulTrades || 0) + 1;
|
||
} else {
|
||
console.log('❌ TRADE FAILED: ' + result.error);
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
console.error('❌ TRADE ERROR:', error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
getStatus() {
|
||
const baseStatus = {
|
||
isActive: this.isRunning,
|
||
mode: this.config?.mode || 'SIMULATION',
|
||
enableTrading: this.config?.enableTrading || false,
|
||
tradingStatus: this.config?.enableTrading ? 'REAL TRADES' : 'SIMULATED ONLY',
|
||
symbol: this.config?.symbol || 'SOLUSD',
|
||
timeframes: this.config?.selectedTimeframes || [],
|
||
automationType: 'SIMPLE',
|
||
...this.stats
|
||
};
|
||
|
||
// Add more descriptive status based on running state
|
||
if (this.isRunning) {
|
||
baseStatus.detailedStatus = 'Running - Monitoring for trade opportunities';
|
||
baseStatus.nextAction = 'Next analysis cycle in progress';
|
||
} else {
|
||
baseStatus.detailedStatus = 'Stopped - No monitoring active';
|
||
baseStatus.nextAction = 'Start automation to begin monitoring';
|
||
}
|
||
|
||
return baseStatus;
|
||
}
|
||
}
|
||
|
||
// Export singleton instance
|
||
const simpleAutomation = new SimpleAutomation();
|
||
export { simpleAutomation };
|