mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 03:07:53 +00:00
79d860e9a8
✨ Major Features Added: • Complete Indonesian & English bilingual support (80+ UI elements) • Global language switcher with persistent preferences • Smart MT5 data integration with web controls • One-click market data download for 50+ instruments • Automated MT5 installer and setup guide • Enhanced settings with profile and preferences management • Professional documentation and troubleshooting guides 🚀 Technical Improvements: • Internationalization (i18n) system with extensible architecture • MT5 environment detection with graceful fallbacks • API architecture for future cross-platform expansion • Improved error handling and user guidance • Cross-browser compatibility and responsive design 📚 Documentation: • 50+ page comprehensive MT5 setup guide • Automated installer with dependency management • Public roadmap announcing QuantumBotX API development • Updated CHANGELOG and platform compatibility notices 🎯 Platform Strategy: • Clear Windows-first positioning for MT5 learning platform • Preparatory work for QuantumBotX API cloud platform • Education-first approach maintaining Indonesian heritage • Foundation for international expansion Co-authored-by: AI Assistant <assistant@quantumbotx.ai>
285 lines
13 KiB
JavaScript
285 lines
13 KiB
JavaScript
// static/js/backtesting.js
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const strategySelect = document.getElementById('strategy-select');
|
|
const paramsContainer = document.getElementById('params-container');
|
|
const form = document.getElementById('backtest-form');
|
|
const runBtn = document.getElementById('run-backtest-btn');
|
|
const resultsContainer = document.getElementById('results-container');
|
|
const resultsSummary = document.getElementById('results-summary');
|
|
const loadingSpinner = document.getElementById('loading-spinner');
|
|
let equityChart = null; // Variabel untuk menyimpan instance grafik
|
|
const resultsLog = document.getElementById('results-log');
|
|
const downloadBtn = document.getElementById('download-data-btn');
|
|
|
|
// Muat strategi ke dropdown
|
|
async function loadStrategies() {
|
|
try {
|
|
const response = await fetch('/api/strategies');
|
|
if (!response.ok) throw new Error('Gagal memuat strategi.');
|
|
const strategies = await response.json();
|
|
|
|
strategySelect.innerHTML = '<option value="" disabled selected>Pilih sebuah strategi</option>';
|
|
strategies.forEach(strategy => {
|
|
const option = document.createElement('option');
|
|
option.value = strategy.id;
|
|
option.textContent = strategy.name;
|
|
strategySelect.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading strategies:', error);
|
|
strategySelect.innerHTML = '<option value="">Gagal memuat strategi</option>';
|
|
}
|
|
}
|
|
|
|
// Muat parameter saat strategi dipilih
|
|
strategySelect.addEventListener('change', async () => {
|
|
const strategyId = strategySelect.value;
|
|
paramsContainer.innerHTML = '<p class="text-sm text-gray-500">Memuat parameter...</p>';
|
|
if (!strategyId) {
|
|
paramsContainer.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/strategies/${strategyId}/params`);
|
|
const params = await res.json();
|
|
paramsContainer.innerHTML = ''; // Kosongkan lagi
|
|
|
|
if (params.length > 0) {
|
|
params.forEach(param => {
|
|
paramsContainer.innerHTML += `
|
|
<div>
|
|
<label for="${param.name}" class="block text-sm font-medium text-gray-700">${param.label}</label>
|
|
<input type="${param.type || 'number'}" name="${param.name}" id="${param.name}" value="${param.default}" step="${param.step || 'any'}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
</div>`;
|
|
});
|
|
} else {
|
|
paramsContainer.innerHTML = '<p class="text-sm text-gray-500">Strategi ini tidak memiliki parameter kustom.</p>';
|
|
}
|
|
} catch (err) {
|
|
console.error('Gagal memuat parameter strategi:', err);
|
|
paramsContainer.innerHTML = '<p class="text-sm text-red-500">Gagal memuat parameter.</p>';
|
|
}
|
|
});
|
|
|
|
// Jalankan backtest saat form disubmit
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
loadingSpinner.classList.remove('hidden');
|
|
resultsContainer.classList.add('hidden');
|
|
runBtn.disabled = true;
|
|
runBtn.textContent = 'Menjalankan...';
|
|
|
|
const formData = new FormData(form);
|
|
|
|
// Kumpulkan parameter kustom
|
|
const params = {};
|
|
// Ambil parameter dari form utama (SL/TP)
|
|
params['sl_pips'] = parseFloat(document.getElementById('sl_atr_multiplier').value);
|
|
params['tp_pips'] = parseFloat(document.getElementById('tp_atr_multiplier').value);
|
|
|
|
// Kumpulkan parameter strategi kustom
|
|
paramsContainer.querySelectorAll('input').forEach(input => {
|
|
const value = parseFloat(input.value);
|
|
// Pastikan hanya angka valid yang di-parse, selain itu ambil string
|
|
params[input.name] = isNaN(value) ? input.value : value;
|
|
});
|
|
formData.append('params', JSON.stringify(params));
|
|
|
|
try {
|
|
const response = await fetch('/api/backtest/run', {
|
|
method: 'POST',
|
|
body: formData, // Kirim sebagai multipart/form-data
|
|
});
|
|
const results = await response.json();
|
|
|
|
if (response.ok) {
|
|
displayResults(results);
|
|
} else {
|
|
alert(`Error: ${results.error}`);
|
|
}
|
|
} catch (err) {
|
|
console.error("Backtest failed:", err);
|
|
alert('Gagal terhubung ke server.');
|
|
} finally {
|
|
loadingSpinner.classList.add('hidden');
|
|
runBtn.disabled = false;
|
|
runBtn.textContent = 'Jalankan Backtest';
|
|
}
|
|
});
|
|
|
|
function displayResults(data) {
|
|
resultsContainer.classList.remove('hidden');
|
|
|
|
// Enhanced display with spread costs and protection info
|
|
const spreadCosts = data.total_spread_costs || 0;
|
|
const netProfit = data.net_profit_after_costs || data.total_profit_usd;
|
|
const instrument = data.instrument || 'UNKNOWN';
|
|
|
|
// Check if protection was applied
|
|
const engineConfig = data.engine_config || {};
|
|
const instrumentConfig = engineConfig.instrument_config || {};
|
|
const maxRisk = instrumentConfig.max_risk_percent || 2.0;
|
|
const maxLot = instrumentConfig.max_lot_size || 10.0;
|
|
const spreadPips = instrumentConfig.typical_spread_pips || 2.0;
|
|
|
|
resultsSummary.innerHTML = `
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Instrument</p>
|
|
<p class="text-lg font-bold text-blue-600">${instrument}</p>
|
|
<p class="text-xs text-gray-400">Max Risk: ${maxRisk}%</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Gross Profit</p>
|
|
<p class="text-2xl font-bold text-green-600">${data.total_profit_usd.toFixed(2)} $</p>
|
|
<p class="text-xs text-gray-400">Before costs</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Spread Costs</p>
|
|
<p class="text-2xl font-bold text-red-600">-${spreadCosts.toFixed(2)} $</p>
|
|
<p class="text-xs text-gray-400">${spreadPips} pips spread</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Net Profit</p>
|
|
<p class="text-2xl font-bold ${netProfit >= 0 ? 'text-green-600' : 'text-red-600'}">${netProfit.toFixed(2)} $</p>
|
|
<p class="text-xs text-gray-400">After all costs</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Max Drawdown</p>
|
|
<p class="text-2xl font-bold text-red-600">${data.max_drawdown_percent.toFixed(2)}%</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Win Rate</p>
|
|
<p class="text-2xl font-bold text-blue-600">${data.win_rate_percent.toFixed(2)}%</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Total Trades</p>
|
|
<p class="text-2xl font-bold">${data.total_trades}</p>
|
|
<p class="text-xs text-gray-400">Max Lot: ${maxLot}</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Wins</p>
|
|
<p class="text-2xl font-bold">${data.wins}</p>
|
|
</div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<p class="text-sm text-gray-500">Losses</p>
|
|
<p class="text-2xl font-bold">${data.losses}</p>
|
|
</div>
|
|
`;
|
|
|
|
// Tampilkan grafik kurva ekuitas
|
|
displayEquityChart(data.equity_curve);
|
|
|
|
// Enhanced trade log with spread costs
|
|
if (data.trades && data.trades.length > 0) {
|
|
let logHtml = '<h4 class="text-lg font-semibold mt-6 mb-2">20 Trade Terakhir (Enhanced Engine)</h4>';
|
|
logHtml += '<div class="text-xs font-mono border rounded p-2 bg-gray-50 max-h-64 overflow-y-auto">';
|
|
|
|
data.trades.forEach(trade => {
|
|
const profitClass = trade.profit > 0 ? 'text-green-600' : 'text-red-600';
|
|
const spreadCost = trade.spread_cost || 0;
|
|
const lotSize = trade.lot_size || 0;
|
|
const reason = trade.reason || 'N/A';
|
|
|
|
logHtml += `<p class="mb-1">`;
|
|
logHtml += `<span class="font-bold">${trade.position_type}</span> | `;
|
|
logHtml += `Entry: ${trade.entry.toFixed(4)} | `;
|
|
logHtml += `Exit: ${trade.exit.toFixed(4)} | `;
|
|
logHtml += `Lot: ${lotSize.toFixed(2)} | `;
|
|
logHtml += `Profit: <span class="${profitClass}">${trade.profit.toFixed(2)}</span> | `;
|
|
logHtml += `Spread: $${spreadCost.toFixed(2)} | `;
|
|
logHtml += `Reason: ${reason}`;
|
|
logHtml += `</p>`;
|
|
});
|
|
|
|
logHtml += '</div>';
|
|
|
|
// Add enhanced engine info
|
|
logHtml += '<div class="mt-4 p-3 bg-blue-50 rounded border border-blue-200">';
|
|
logHtml += '<h5 class="text-sm font-semibold text-blue-800 mb-2">🚀 Enhanced Engine Features Applied:</h5>';
|
|
logHtml += '<div class="text-xs text-blue-700 space-y-1">';
|
|
logHtml += `<p>✅ Realistic spread costs: ${spreadPips} pips per trade</p>`;
|
|
logHtml += `<p>✅ ATR-based position sizing with ${maxRisk}% max risk</p>`;
|
|
logHtml += `<p>✅ Instrument protection: ${maxLot} max lot size</p>`;
|
|
logHtml += `<p>✅ Slippage simulation included</p>`;
|
|
logHtml += `<p>💰 Total spread costs deducted: $${spreadCosts.toFixed(2)}</p>`;
|
|
logHtml += '</div></div>';
|
|
|
|
resultsLog.innerHTML = logHtml;
|
|
} else {
|
|
resultsLog.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
function displayEquityChart(equityData) {
|
|
const ctx = document.getElementById('equity-chart').getContext('2d');
|
|
if (equityChart) {
|
|
equityChart.destroy(); // Hancurkan grafik lama sebelum membuat yang baru
|
|
}
|
|
equityChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: Array.from({ length: equityData.length }, (_, i) => i + 1), // Label 1, 2, 3, ...
|
|
datasets: [{
|
|
label: 'Equity Curve',
|
|
data: equityData,
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.1,
|
|
pointRadius: 0,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { display: false },
|
|
title: { display: true, text: 'Pertumbuhan Modal (Equity Curve)' }
|
|
},
|
|
scales: { y: { beginAtZero: false } }
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle data download
|
|
downloadBtn.addEventListener('click', async () => {
|
|
if (!confirm("This will download historical market data from MT5.\n\nRequirements:\n• MT5 Terminal must be running\n• MT5 Python library must be available\n• Account credentials in .env file\n\nThis may take several minutes. Continue?")) {
|
|
return;
|
|
}
|
|
|
|
const originalText = downloadBtn.innerHTML;
|
|
downloadBtn.disabled = true;
|
|
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Downloading...';
|
|
|
|
try {
|
|
const response = await fetch('/api/download-data', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
if (result.success) {
|
|
alert(`✅ Download completed successfully!\n\nDownloaded: ${result.downloaded_files.length} files\nFailed: ${result.failed_count}\n\nFiles saved to 'lab/backtest_data/' directory.`);
|
|
} else {
|
|
alert(`❌ Download failed: ${result.error}`);
|
|
}
|
|
} else {
|
|
alert(`❌ Download error: ${result.error}`);
|
|
}
|
|
} catch (err) {
|
|
console.error("Download failed:", err);
|
|
alert('❌ Failed to connect to server for data download.');
|
|
} finally {
|
|
downloadBtn.disabled = false;
|
|
downloadBtn.innerHTML = originalText;
|
|
}
|
|
});
|
|
|
|
loadStrategies();
|
|
});
|