mirror of
https://github.com/FxPouya/FxMathQuantWebApp.git
synced 2026-07-27 18:27:44 +00:00
884 lines
30 KiB
JavaScript
884 lines
30 KiB
JavaScript
/**
|
|
* Main Application Controller
|
|
*/
|
|
|
|
// Global state
|
|
let appData = {
|
|
csvData: null,
|
|
dataName: '',
|
|
symbol: '',
|
|
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;
|
|
|
|
// Extract symbol from filename
|
|
appData.symbol = typeof extractSymbolFromFilename === 'function'
|
|
? extractSymbolFromFilename(file.name)
|
|
: '';
|
|
|
|
console.log('✅ Data loaded successfully:', appData.csvData.length, 'bars');
|
|
console.log('📊 Detected symbol:', appData.symbol || 'Unknown');
|
|
|
|
// 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 = `
|
|
<td>${row.time || '-'}</td>
|
|
<td>${row.open.toFixed(5)}</td>
|
|
<td>${row.high.toFixed(5)}</td>
|
|
<td>${row.low.toFixed(5)}</td>
|
|
<td>${row.close.toFixed(5)}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply data size limit
|
|
*/
|
|
function applyDataSizeLimit() {
|
|
const limitSelect = document.getElementById('data-size-limit');
|
|
const customInput = document.getElementById('custom-bars-input');
|
|
|
|
let limit;
|
|
if (limitSelect.value === 'custom') {
|
|
limit = parseInt(customInput.value) || 0;
|
|
} else {
|
|
limit = parseInt(limitSelect.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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle custom bars input visibility
|
|
*/
|
|
function toggleCustomBarsInput() {
|
|
const limitSelect = document.getElementById('data-size-limit');
|
|
const customInput = document.getElementById('custom-bars-input');
|
|
|
|
if (limitSelect.value === 'custom') {
|
|
customInput.style.display = 'block';
|
|
customInput.focus();
|
|
} else {
|
|
customInput.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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),
|
|
// Walk-Forward Analysis settings
|
|
enableWalkForward: document.getElementById('enable-walkforward').checked,
|
|
trainingRatio: parseInt(document.getElementById('training-ratio').value) / 100
|
|
};
|
|
|
|
// 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);
|
|
console.log('📊 Using symbol:', appData.symbol || 'Unknown');
|
|
appData.optimizer = new GeneticOptimizer(appData.csvData, config, appData.symbol);
|
|
|
|
// 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 = '<p style="color: #a0aec0; text-align: center; padding: 40px;">No strategies found. Try adjusting your criteria.</p>';
|
|
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 = '<p style="color: red;">Error: Invalid strategy data</p>';
|
|
return errorCard;
|
|
}
|
|
|
|
const m = strategy.metrics;
|
|
const card = document.createElement('div');
|
|
card.className = 'strategy-card';
|
|
|
|
|
|
// Generate walk-forward badge HTML (only if walk-forward was enabled and data exists)
|
|
let wfBadgeHTML = '';
|
|
if (strategy.walkForward && typeof strategy.walkForward === 'object') {
|
|
const wf = strategy.walkForward;
|
|
if (wf.robustness !== undefined && wf.passed !== undefined) {
|
|
const badgeClass = wf.passed ? 'wf-badge-pass' : 'wf-badge-fail';
|
|
const badgeIcon = wf.passed ? '✓' : '⚠';
|
|
const badgeText = wf.passed ? 'Robust' : 'Overfitted';
|
|
wfBadgeHTML = `
|
|
<div class="wf-badge ${badgeClass}" title="Robustness Score: ${wf.robustness}/100">
|
|
<span class="wf-icon">${badgeIcon}</span>
|
|
<span class="wf-text">${badgeText}</span>
|
|
<span class="wf-score">${wf.robustness}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
const name = `Strategy #${number}`;
|
|
card.innerHTML = `
|
|
<div class="strategy-header">
|
|
<div class="strategy-name">${name}</div>
|
|
${wfBadgeHTML}
|
|
<input type="checkbox" class="strategy-select" onchange="toggleStrategySelection(${number - 1}, this.checked)">
|
|
</div>
|
|
<div class="strategy-metrics">
|
|
<div class="metric">
|
|
<span class="metric-label">Profit Factor</span>
|
|
<span class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">
|
|
${m.profitFactor.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Win Rate</span>
|
|
<span class="metric-value">${m.winRate.toFixed(1)}%</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Trades</span>
|
|
<span class="metric-value">${m.totalTrades}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Max DD</span>
|
|
<span class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="strategy-actions">
|
|
<button class="btn-secondary" onclick="viewStrategyDetails(${number - 1})">View Details</button>
|
|
<button class="btn-info" onclick="runMonteCarloForStrategy(${number - 1})" title="Run Monte Carlo Simulation">🎲 MC</button>
|
|
<button class="btn-primary" onclick="downloadStrategy(${number - 1}, 'mq4')">MQ4</button>
|
|
<button class="btn-primary" onclick="downloadStrategy(${number - 1}, 'mq5')">MQ5</button>
|
|
<button class="btn-success" onclick="downloadStrategy(${number - 1}, 'report')">Report</button>
|
|
</div>
|
|
`;
|
|
|
|
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 with symbol
|
|
content = generateHTMLReport(strategy, index, appData.symbol);
|
|
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 = `
|
|
<h3>${name}</h3>
|
|
<div class="strategy-metrics">
|
|
<div class="metric">
|
|
<span class="metric-label">PF</span>
|
|
<span class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">${m.profitFactor.toFixed(2)}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Win Rate</span>
|
|
<span class="metric-value">${m.winRate.toFixed(1)}%</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Trades</span>
|
|
<span class="metric-value">${m.totalTrades}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">Max DD</span>
|
|
<span class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</span>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 15px;">
|
|
<canvas id="comp-chart-${index}" style="height: 150px;"></canvas>
|
|
</div>
|
|
<div class="strategy-actions" style="margin-top: 15px;">
|
|
<button class="btn-small" onclick="viewStrategyDetails(${index})">Full Details</button>
|
|
</div>
|
|
`;
|
|
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`;
|
|
}
|
|
|
|
/**
|
|
* Update training/testing split display
|
|
*/
|
|
function updateTrainingSplit() {
|
|
const ratio = parseInt(document.getElementById('training-ratio').value);
|
|
document.getElementById('training-percent').textContent = ratio;
|
|
|
|
// Update bar counts if data is loaded
|
|
if (appData.csvData) {
|
|
const totalBars = appData.csvData.length;
|
|
const trainingBars = Math.floor(totalBars * (ratio / 100));
|
|
const testingBars = totalBars - trainingBars;
|
|
|
|
document.getElementById('training-bars').textContent = trainingBars.toLocaleString();
|
|
document.getElementById('testing-bars').textContent = testingBars.toLocaleString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle walk-forward settings visibility
|
|
*/
|
|
function toggleWalkForwardSettings() {
|
|
const enabled = document.getElementById('enable-walkforward').checked;
|
|
const settings = document.getElementById('walkforward-settings');
|
|
if (settings) {
|
|
settings.style.display = enabled ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update Monte Carlo settings display
|
|
*/
|
|
function updateMonteCarloSettings() {
|
|
const iterations = parseInt(document.getElementById('mc-iterations').value);
|
|
const threshold = parseInt(document.getElementById('ror-threshold').value);
|
|
|
|
document.getElementById('mc-iterations-value').textContent = iterations.toLocaleString();
|
|
document.getElementById('ror-threshold-value').textContent = threshold;
|
|
}
|
|
|
|
/**
|
|
* Toggle Monte Carlo settings visibility
|
|
*/
|
|
function toggleMonteCarloSettings() {
|
|
const enabled = document.getElementById('enable-montecarlo').checked;
|
|
const settings = document.getElementById('montecarlo-settings');
|
|
if (settings) {
|
|
settings.classList.toggle('hidden', !enabled);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run Monte Carlo analysis for a specific strategy
|
|
*/
|
|
function runMonteCarloForStrategy(index) {
|
|
const strategy = appData.foundStrategies[index];
|
|
if (!strategy) {
|
|
alert('Strategy not found!');
|
|
return;
|
|
}
|
|
|
|
const mcEnabled = document.getElementById('enable-montecarlo')?.checked;
|
|
if (!mcEnabled) {
|
|
alert('Monte Carlo is not enabled!\\n\\nPlease enable it in the settings before generating strategies.');
|
|
return;
|
|
}
|
|
|
|
const m = strategy.metrics;
|
|
if (!m.trades || m.trades.length < 20) {
|
|
alert(`Monte Carlo requires at least 20 trades.\\nThis strategy has ${m.trades?.length || 0} trades.`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const iterations = parseInt(document.getElementById('mc-iterations').value) || 1000;
|
|
const rorThreshold = parseInt(document.getElementById('ror-threshold').value) / 100 || 0.2;
|
|
|
|
console.log(`🎲 Running Monte Carlo analysis (${iterations} iterations)...`);
|
|
|
|
const trades = MonteCarloSimulation.extractTrades(strategy);
|
|
const mc = new MonteCarloSimulation(trades, iterations, rorThreshold);
|
|
const results = mc.run();
|
|
|
|
strategy.monteCarlo = results;
|
|
|
|
// Display results in professional modal with Chart.js visualization
|
|
displayMonteCarloModal(strategy, index, results);
|
|
|
|
} catch (error) {
|
|
console.error('Monte Carlo error:', error);
|
|
alert(`Monte Carlo analysis failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Explicitly expose functions to global scope for inline onclick handlers
|
|
window.downloadStrategy = downloadStrategy;
|
|
window.downloadAllStrategies = downloadAllStrategies;
|
|
window.toggleStrategySelection = toggleStrategySelection;
|
|
window.showComparison = showComparison;
|
|
window.updateTrainingSplit = updateTrainingSplit;
|
|
window.toggleWalkForwardSettings = toggleWalkForwardSettings;
|
|
window.updateMonteCarloSettings = updateMonteCarloSettings;
|
|
window.toggleMonteCarloSettings = toggleMonteCarloSettings;
|
|
window.runMonteCarloForStrategy = runMonteCarloForStrategy;
|
|
window.hideComparison = hideComparison;
|
|
window.toggleCustomBarsInput = toggleCustomBarsInput;
|