/** * Main Application Controller */ // Global state let appData = { csvData: null, dataName: '', optimizer: null, foundStrategies: [], selectedStrategies: new Set(), isGenerating: false }; // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', () => { initializeApp(); }); function initializeApp() { setupEventListeners(); showSection('upload-section'); } /** * Setup all event listeners */ function setupEventListeners() { // Upload zone const uploadZone = document.getElementById('upload-zone'); const fileInput = document.getElementById('file-input'); const browseBtn = document.getElementById('browse-btn'); uploadZone.addEventListener('click', () => fileInput.click()); browseBtn.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFileUpload(e.target.files[0]); } }); // Drag and drop uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { handleFileUpload(e.dataTransfer.files[0]); } }); // Navigation buttons document.getElementById('continue-btn').addEventListener('click', () => { showSection('config-section'); }); document.getElementById('back-btn').addEventListener('click', () => { showSection('upload-section'); }); document.getElementById('start-btn').addEventListener('click', startGeneration); document.getElementById('stop-btn').addEventListener('click', stopGeneration); document.getElementById('new-search-btn').addEventListener('click', () => { showSection('upload-section'); appData.foundStrategies = []; }); document.getElementById('download-all-btn').addEventListener('click', downloadAllStrategies); // Comparison button const compareBtn = document.getElementById('compare-btn'); if (compareBtn) { compareBtn.addEventListener('click', () => { if (appData.selectedStrategies.size < 2) { alert('Please select at least 2 strategies to compare.'); return; } showComparison(); }); } } /** * Handle CSV file upload */ function handleFileUpload(file) { console.log('📁 File upload started:', file.name, 'Size:', file.size); if (!file.name.endsWith('.csv')) { alert('Please upload a CSV file'); return; } Papa.parse(file, { header: true, dynamicTyping: true, skipEmptyLines: true, complete: (results) => { console.log('📊 CSV parsed:', results.data.length, 'rows'); if (results.data.length === 0) { alert('CSV file is empty'); return; } // Validate CSV structure (accept both lowercase and capitalized column names) const firstRow = results.data[0]; const requiredFields = ['open', 'high', 'low', 'close']; const hasRequiredFields = requiredFields.every(field => { // Check lowercase, capitalized, and uppercase versions return field in firstRow || field.charAt(0).toUpperCase() + field.slice(1) in firstRow || field.toUpperCase() in firstRow; }); if (!hasRequiredFields) { alert('CSV must contain: time, open, high, low, close columns'); return; } // Normalize column names to lowercase appData.csvData = results.data.map(row => ({ time: row.time || row.Time || row.TIME, open: parseFloat(row.open || row.Open || row.OPEN), high: parseFloat(row.high || row.High || row.HIGH), low: parseFloat(row.low || row.Low || row.LOW), close: parseFloat(row.close || row.Close || row.CLOSE) })); appData.dataName = file.name; console.log('✅ Data loaded successfully:', appData.csvData.length, 'bars'); // Apply data size limit applyDataSizeLimit(); displayDataPreview(); }, error: (error) => { alert('Error parsing CSV: ' + error.message); } }); } /** * Display data preview */ function displayDataPreview() { const preview = document.getElementById('data-preview'); preview.classList.remove('hidden'); // Update data info document.getElementById('data-symbol').textContent = `File: ${appData.dataName}`; document.getElementById('data-bars').textContent = `Bars: ${appData.csvData.length}`; // Detect timeframe (simple heuristic) const timeframe = detectTimeframe(); document.getElementById('data-timeframe').textContent = `Timeframe: ${timeframe}`; // Show first 10 rows const tbody = document.getElementById('preview-body'); tbody.innerHTML = ''; const previewRows = appData.csvData.slice(0, 10); previewRows.forEach(row => { const tr = document.createElement('tr'); tr.innerHTML = ` ${row.time || '-'} ${row.open.toFixed(5)} ${row.high.toFixed(5)} ${row.low.toFixed(5)} ${row.close.toFixed(5)} `; tbody.appendChild(tr); }); } /** * Apply data size limit */ function applyDataSizeLimit() { const limit = parseInt(document.getElementById('data-size-limit').value); if (limit > 0 && appData.csvData.length > limit) { console.log(`✂️ Limiting data from ${appData.csvData.length} to ${limit} bars`); appData.csvData = appData.csvData.slice(-limit); // Take last N bars } } /** * Detect timeframe from data */ function detectTimeframe() { if (appData.csvData.length < 2) return 'Unknown'; // Try to parse time difference const time1 = new Date(appData.csvData[0].time); const time2 = new Date(appData.csvData[1].time); if (isNaN(time1) || isNaN(time2)) return 'Unknown'; const diffMinutes = Math.abs(time2 - time1) / (1000 * 60); if (diffMinutes <= 1) return 'M1'; if (diffMinutes <= 5) return 'M5'; if (diffMinutes <= 15) return 'M15'; if (diffMinutes <= 30) return 'M30'; if (diffMinutes <= 60) return 'H1'; if (diffMinutes <= 240) return 'H4'; if (diffMinutes <= 1440) return 'D1'; return 'Unknown'; } /** * Start strategy generation */ async function startGeneration() { console.log('🚀 Starting strategy generation...'); if (!appData.csvData) { console.error('❌ No data loaded!'); alert('Please upload data first'); return; } // Get configuration console.log('⚙️ Reading configuration...'); const config = { populationSize: parseInt(document.getElementById('population').value), generations: parseInt(document.getElementById('generations').value), strategiesCount: parseInt(document.getElementById('strategies-count').value), rulesRange: [ parseInt(document.getElementById('rules-min').value), parseInt(document.getElementById('rules-max').value) ], shiftRange: [ parseInt(document.getElementById('shift-min').value), parseInt(document.getElementById('shift-max').value) ], minPF: parseFloat(document.getElementById('min-pf').value), minWR: parseFloat(document.getElementById('min-wr').value), maxDD: parseFloat(document.getElementById('max-dd').value), minTrades: parseInt(document.getElementById('min-trades').value) }; // Show progress section showSection('progress-section'); appData.isGenerating = true; appData.foundStrategies = []; // Reset buttons visibility const stopBtn = document.getElementById('stop-btn'); if (stopBtn) stopBtn.classList.remove('hidden'); const viewResultsBtn = document.getElementById('view-results-btn'); if (viewResultsBtn) viewResultsBtn.classList.add('hidden'); // Clear progress log document.getElementById('progress-log').innerHTML = ''; // Create optimizer console.log('🧬 Creating GA optimizer with config:', config); appData.optimizer = new GeneticOptimizer(appData.csvData, config); // Start time const startTime = Date.now(); // Run optimization console.log('▶️ Starting GA run...'); const strategies = await appData.optimizer.run((progress) => { updateProgress(progress, startTime); }); // Handle completion console.log('✅ GA complete! Found', strategies.length, 'strategies'); appData.foundStrategies = strategies; appData.isGenerating = false; // Stay on page as requested, show redirection button showGenerationCompleteUI(); } /** * Update progress display */ function updateProgress(progress, startTime) { // Update stats if (progress.generation !== undefined) { document.getElementById('current-gen').textContent = `${progress.generation} / ${progress.totalGenerations}`; document.getElementById('best-fitness').textContent = progress.bestFitness.toFixed(2); document.getElementById('found-count').textContent = `${progress.foundCount} / ${progress.targetCount}`; // Update progress bar const progressPercent = (progress.generation / progress.totalGenerations) * 100; document.getElementById('progress-bar').style.width = progressPercent + '%'; } // Update elapsed time const elapsed = Math.floor((Date.now() - startTime) / 1000); document.getElementById('elapsed-time').textContent = formatTime(elapsed); // Log strategy found if (progress.strategyFound) { // Don't add to appData.foundStrategies here - we'll get them from GA at the end // This prevents duplicate storage and ensures metrics are preserved const log = document.getElementById('progress-log'); const m = progress.strategyFound.metrics; const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; logEntry.style.display = 'flex'; logEntry.style.justifyContent = 'space-between'; logEntry.style.alignItems = 'center'; logEntry.style.padding = '5px 0'; logEntry.style.color = '#48bb78'; const text = document.createElement('span'); text.textContent = `✓ Strategy ${progress.foundCount}: PF=${m.profitFactor.toFixed(2)}, WR=${m.winRate.toFixed(1)}%, Trades=${m.totalTrades}`; const viewBtn = document.createElement('button'); viewBtn.className = 'btn-small'; viewBtn.textContent = 'View Details'; viewBtn.onclick = () => { const index = progress.foundCount - 1; viewStrategyDetails(index); }; logEntry.appendChild(text); logEntry.appendChild(viewBtn); log.appendChild(logEntry); log.scrollTop = log.scrollHeight; } } /** * Stop generation */ function stopGeneration() { if (appData.optimizer) { appData.optimizer.stop(); appData.isGenerating = false; // Update collection with whatever was found so far if (appData.optimizer.foundStrategies) { appData.foundStrategies = appData.optimizer.foundStrategies; } } if (appData.foundStrategies.length > 0) { showGenerationCompleteUI(); } else { alert('No strategies found yet'); showSection('config-section'); } } /** * Show UI when generation is complete/stopped */ function showGenerationCompleteUI() { const stopBtn = document.getElementById('stop-btn'); if (stopBtn) stopBtn.classList.add('hidden'); let viewResultsBtn = document.getElementById('view-results-btn'); if (!viewResultsBtn) { viewResultsBtn = document.createElement('button'); viewResultsBtn.id = 'view-results-btn'; viewResultsBtn.className = 'btn-success'; viewResultsBtn.textContent = 'View All Results →'; viewResultsBtn.style.marginLeft = '10px'; viewResultsBtn.onclick = displayResults; if (stopBtn && stopBtn.parentNode) { stopBtn.parentNode.appendChild(viewResultsBtn); } } else { viewResultsBtn.classList.remove('hidden'); } } /** * Display results */ function displayResults() { console.log('📊 Displaying results...', appData.foundStrategies.length, 'strategies'); showSection('results-section'); const grid = document.getElementById('strategies-grid'); grid.innerHTML = ''; if (appData.foundStrategies.length === 0) { console.warn('⚠️ No strategies to display!'); grid.innerHTML = '

No strategies found. Try adjusting your criteria.

'; return; } appData.foundStrategies.forEach((strategy, index) => { console.log(`Creating card for strategy ${index + 1}:`, strategy.metrics); const card = createStrategyCard(strategy, index + 1); grid.appendChild(card); }); // Reset selection on display appData.selectedStrategies.clear(); updateCompareButton(); console.log('✅ Strategy cards created:', grid.children.length); } /** * Create strategy card */ function createStrategyCard(strategy, number) { console.log('Creating card for strategy number:', number, 'Strategy:', strategy); if (!strategy || !strategy.metrics) { console.error('Invalid strategy object:', strategy); const errorCard = document.createElement('div'); errorCard.className = 'strategy-card'; errorCard.innerHTML = '

Error: Invalid strategy data

'; return errorCard; } const m = strategy.metrics; const card = document.createElement('div'); card.className = 'strategy-card'; card.innerHTML = `
${name}
Profit Factor ${m.profitFactor.toFixed(2)}
Win Rate ${m.winRate.toFixed(1)}%
Trades ${m.totalTrades}
Max DD ${m.maxDrawdown.toFixed(2)}%
`; console.log('Card HTML created successfully'); return card; } /** * Download single strategy */ function downloadStrategy(index, type) { console.log(`💾 Downloading strategy ${index} type ${type}...`); const strategy = appData.foundStrategies[index]; if (!strategy) { console.error(`❌ Strategy at index ${index} not found!`); return; } const m = strategy.metrics; const name = `FxMath_${String(index + 1).padStart(3, '0')}_PF${m.profitFactor.toFixed(2).replace('.', '_')}_WR${Math.round(m.winRate)}`; let content, filename; try { // Convert strategy to EA-Convertor format const strategyData = { parameters: { symbol: 'EURUSD', atr_period: strategy.atrPeriod, sl_multiplier: strategy.slMultiplier, tp_multiplier: strategy.tpMultiplier, close_at_opposite: strategy.closeAtOpposite || true }, buy_rules: strategy.rules, sell_rules: strategy.rules // SELL rules are inverted in the converters }; if (type === 'mq4') { const converter = new MQ4Converter(strategyData); content = converter.generate(); filename = name + '.mq4'; } else if (type === 'mq5') { const converter = new MQ5Converter(strategyData); content = converter.generate(); filename = name + '.mq5'; } else if (type === 'ctrader') { const converter = new CTraderConverter(strategyData); content = converter.generate(); filename = name + '.cs'; } else if (type === 'pine') { const converter = new PineConverter(strategyData); content = converter.generate(); filename = name + '.pine'; } else if (type === 'report') { // Use new HTML report generator content = generateHTMLReport(strategy, index); filename = name + '_report.html'; } else if (type === 'json') { content = JSON.stringify(strategy.toJSON(), null, 2); filename = name + '.json'; } console.log(`✅ Content generated for ${filename}, size: ${content.length}`); downloadFile(content, filename); } catch (error) { console.error(`❌ Error generating ${type} for strategy ${index}:`, error); } } /** * Download all strategies */ function downloadAllStrategies() { console.log('📥 Download All Strategies started...', appData.foundStrategies.length, 'strategies'); if (appData.foundStrategies.length === 0) { alert('No strategies to download'); return; } const btn = document.getElementById('download-all-btn'); const originalText = btn.textContent; btn.disabled = true; btn.textContent = 'Preparing Downloads...'; const total = appData.foundStrategies.length; let completed = 0; appData.foundStrategies.forEach((strategy, index) => { setTimeout(() => { console.log(`⏱️ Triggering downloads for strategy ${index + 1}/${total}...`); btn.textContent = `Downloading ${index + 1}/${total}...`; downloadStrategy(index, 'mq4'); setTimeout(() => downloadStrategy(index, 'mq5'), 150); setTimeout(() => downloadStrategy(index, 'report'), 300); setTimeout(() => downloadStrategy(index, 'json'), 450); completed++; if (completed === total) { setTimeout(() => { btn.disabled = false; btn.textContent = originalText; console.log('✅ All downloads triggered!'); }, 1000); } }, index * 1200); // 1.2s per strategy to be safe }); } /** * Download file helper */ function downloadFile(content, filename) { const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); // Delay removal and revocation to ensure browser captures the click setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 500); } /** * Show specific section */ function showSection(sectionId) { const sections = document.querySelectorAll('.section'); sections.forEach(section => section.classList.remove('active')); document.getElementById(sectionId).classList.add('active'); } /** * Toggle strategy selection for comparison */ function toggleStrategySelection(index, isSelected) { if (isSelected) { appData.selectedStrategies.add(index); } else { appData.selectedStrategies.delete(index); } updateCompareButton(); } /** * Update comparison button text */ function updateCompareButton() { const btn = document.getElementById('compare-btn'); if (btn) { const count = appData.selectedStrategies.size; btn.textContent = `Compare Selected (${count})`; } } /** * Show comparison section */ function showComparison() { const container = document.getElementById('comparison-container'); container.innerHTML = ''; appData.selectedStrategies.forEach(index => { const strategy = appData.foundStrategies[index]; const m = strategy.metrics; const name = `Strategy ${index + 1}`; const item = document.createElement('div'); item.className = 'comparison-item'; item.innerHTML = `

${name}

PF ${m.profitFactor.toFixed(2)}
Win Rate ${m.winRate.toFixed(1)}%
Trades ${m.totalTrades}
Max DD ${m.maxDrawdown.toFixed(2)}%
`; container.appendChild(item); // Draw small chart setTimeout(() => { const ctx = document.getElementById(`comp-chart-${index}`).getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: m.equity.map((_, i) => i), datasets: [{ data: m.equity, borderColor: '#667eea', borderWidth: 2, pointRadius: 0, fill: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { display: false } } } }); }, 100); }); showSection('results-section'); // Ensure we are in results document.getElementById('comparison-section').classList.remove('hidden'); document.getElementById('comparison-section').scrollIntoView({ behavior: 'smooth' }); } /** * Hide comparison section */ function hideComparison() { document.getElementById('comparison-section').classList.add('hidden'); } /** * Format time in seconds to readable format */ function formatTime(seconds) { if (seconds < 60) return seconds + 's'; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } // Explicitly expose functions to global scope for inline onclick handlers window.downloadStrategy = downloadStrategy; window.downloadAllStrategies = downloadAllStrategies; window.toggleStrategySelection = toggleStrategySelection; window.showComparison = showComparison; window.hideComparison = hideComparison;