class Backtesting { constructor(ui) { this.ui = ui; this.comms = ui.data.comms; this.tests = []; // Stores the list of saved backtests this.target_id = 'backtest_display'; // The container to display backtests this.currentTest = null; // Tracks the currently open test // Register handlers for backtesting messages this.comms.on('backtest_error', this.handleBacktestError.bind(this)); this.comms.on('backtest_submitted', this.handleBacktestSubmitted.bind(this)); this.comms.on('backtest_results', this.handleBacktestResults.bind(this)); this.comms.on('backtest_progress', this.handleProgress.bind(this)); this.comms.on('backtests_list', this.handleBacktestsList.bind(this)); this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this)); } // Initialize method to cache DOM elements initialize() { this.cacheDOMElements(); // Optionally, fetch saved tests or perform other initialization } cacheDOMElements() { this.progressContainer = document.getElementById('backtest-progress-container'); this.progressBar = document.getElementById('progress_bar'); this.formElement = document.getElementById('backtest_form'); this.statusMessage = document.getElementById('backtest-status-message'); this.resultsContainer = document.getElementById('backtest-results'); this.resultsDisplay = document.getElementById('results_display'); this.backtestDraggableHeader = document.getElementById('backtest-form-header'); // Updated to single h1 this.backtestDisplay = document.getElementById(this.target_id); this.strategyDropdown = document.getElementById('strategy_select'); this.equityCurveChart = document.getElementById('equity_curve_chart'); } // Utility Methods showElement(element) { if (element) element.classList.add('show'); } hideElement(element) { if (element) element.classList.remove('show'); } setText(element, text) { if (element) element.textContent = text; } displayMessage(message, color = 'blue') { if (this.statusMessage) { this.showElement(this.statusMessage); this.statusMessage.style.color = color; this.setText(this.statusMessage, message); } } clearMessage() { if (this.statusMessage) { this.hideElement(this.statusMessage); this.setText(this.statusMessage, ''); } } // Event Handlers handleBacktestSubmitted(data) { if (data.status === "started") { const existingTest = this.tests.find(t => t.name === data.backtest_name); const availableStrategies = this.getAvailableStrategies(); // Find the strategy that matches the tbl_key from the data const matchedStrategy = availableStrategies.find(s => s.tbl_key === data.strategy); if (existingTest) { Object.assign(existingTest, { status: 'running', progress: 0, start_date: data.start_date, results: null, strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy' }); } else { const newTest = { name: data.backtest_name, strategy: matchedStrategy ? matchedStrategy.name : availableStrategies[0]?.name || 'default_strategy', start_date: data.start_date, status: 'running', progress: 0, results: null }; this.tests.push(newTest); } // Set currentTest to the test name received from backend this.currentTest = data.backtest_name; console.log(`handleBacktestSubmitted: Backtest "${data.backtest_name}" started.`); this.showRunningAnimation(); // Display progress container this.updateHTML(); } } handleBacktestError(data) { console.error("Backtest error:", data.message); const test = this.tests.find(t => t.name === this.currentTest); if (test) { test.status = 'error'; // Update the test status console.log(`Backtest "${test.name}" encountered an error.`); this.updateHTML(); } this.displayMessage(`Backtest error: ${data.message}`, 'red'); // Hide progress bar and results this.hideElement(this.progressContainer); this.hideElement(this.resultsContainer); } handleBacktestResults(data) { const test = this.tests.find(t => t.name === data.test_id); if (test) { Object.assign(test, { status: 'complete', progress: 100, results: data.results }); // Validate strategy if (!test.strategy) { console.warn(`Test "${test.name}" is missing a strategy. Setting a default.`); test.strategy = 'default_strategy'; // Use a sensible default } this.updateHTML(); this.stopRunningAnimation(data.results); } } handleProgress(data) { console.log("handleProgress: Backtest progress:", data.progress); if (!this.progressContainer) { console.error('handleProgress: Progress container not found.'); return; } // Find the test that matches the progress update const test = this.tests.find(t => t.name === data.test_id); if (!test) { console.warn(`handleProgress: Progress update received for unknown test: ${data.test_id}`); return; } // Update the progress for the correct test test.progress = data.progress; console.log(`handleProgress: Updated progress for "${test.name}" to ${data.progress}%.`); // If the currently open test matches, update the dialog's progress bar if (this.currentTest === test.name && this.formElement.style.display === "grid") { this.showElement(this.progressContainer); // Adds 'show' class this.updateProgressBar(data.progress); this.displayMessage('Backtest in progress...', 'blue'); console.log(`handleProgress: Progress container updated for "${test.name}".`); } } handleBacktestsList(data) { console.log("Backtests list received:", data.tests); // Update the tests array this.tests = data.tests; this.updateHTML(); } handleBacktestDeleted(data) { console.log(`Backtest "${data.name}" was successfully deleted.`); // Remove the deleted test from the tests array this.tests = this.tests.filter(t => t.name !== data.name); this.updateHTML(); } // Helper Methods getAvailableStrategies() { return this.ui.strats.dataManager.getAllStrategies().map(s => ({ name: s.name, // Strategy name tbl_key: s.tbl_key // Unique identifier })); } updateProgressBar(progress) { if (this.progressBar) { console.log(`Updating progress bar to ${progress}%`); this.progressBar.style.width = `${progress}%`; this.setText(this.progressBar, `${progress}%`); } else { console.log('Progress bar element not found'); } } showRunningAnimation() { this.hideElement(this.resultsContainer); this.showElement(this.progressContainer); this.updateProgressBar(0); this.setText(this.progressBar, '0%'); this.resultsDisplay.innerHTML = ''; // Clear previous results this.displayMessage('Backtest started...', 'blue'); } displayTestResults(results) { this.showElement(this.resultsContainer); let html = ` Initial Capital: ${results.initial_capital}
Final Portfolio Value: ${results.final_portfolio_value}
Total Return: ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%
Run Duration: ${results.run_duration ? results.run_duration.toFixed(2) : 'N/A'} seconds `; // Equity Curve html += `

Equity Curve

`; // Stats Section if (results.stats) { html += `

Statistics

`; for (const [key, value] of Object.entries(results.stats)) { const description = this.getStatDescription(key); const formattedValue = value != null ? value.toFixed(2) : 'N/A'; // Safeguard against null or undefined html += `
${this.formatStatKey(key)}: ${formattedValue}
`; } html += `
`; } // Trades Table if (results.trades && results.trades.length > 0) { html += `

Trades Executed

`; results.trades.forEach(trade => { const size = trade.size != null ? trade.size.toFixed(8) : 'N/A'; const openPrice = trade.open_price != null ? trade.open_price.toFixed(2) : 'N/A'; const closePrice = trade.close_price != null ? trade.close_price.toFixed(2) : 'N/A'; const pnl = trade.pnl != null ? trade.pnl.toFixed(2) : 'N/A'; html += ` `; }); html += `
Trade ID Size Open Price Close Price P&L
${trade.ref} ${size} ${openPrice} ${closePrice} ${pnl}
`; } else { html += `

No trades were executed.

`; } this.resultsDisplay.innerHTML = html; this.drawEquityCurveChart(results.equity_curve); } // Helper to format stat keys formatStatKey(key) { return key.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); } // Helper to get stat descriptions getStatDescription(key) { const descriptions = { total_return: "The percentage change in portfolio value over the test period.", sharpe_ratio: "A measure of risk-adjusted return; higher values indicate better risk-adjusted performance.", sortino_ratio: "Similar to Sharpe Ratio but penalizes only downside volatility.", calmar_ratio: "A measure of return relative to maximum drawdown.", volatility: "The annualized standard deviation of portfolio returns, representing risk.", max_drawdown: "The largest percentage loss from a peak to a trough in the equity curve.", profit_factor: "The ratio of gross profits to gross losses; values above 1 indicate profitability.", average_pnl: "The average profit or loss per trade.", number_of_trades: "The total number of trades executed.", win_loss_ratio: "The ratio of winning trades to losing trades.", max_consecutive_wins: "The highest number of consecutive profitable trades.", max_consecutive_losses: "The highest number of consecutive losing trades.", win_rate: "The percentage of trades that were profitable.", loss_rate: "The percentage of trades that resulted in a loss." }; return descriptions[key] || "No description available."; } drawEquityCurveChart(equityCurve) { const equityCurveChart = document.getElementById('equity_curve_chart'); if (!equityCurveChart) { console.error('Chart container not found'); return; } // Clear previous chart equityCurveChart.innerHTML = ''; const width = equityCurveChart.clientWidth || 600; const height = equityCurveChart.clientHeight || 300; const padding = 40; const minValue = Math.min(...equityCurve); const maxValue = Math.max(...equityCurve); const valueRange = maxValue - minValue || 1; const normalizedData = equityCurve.map((value, index) => { const x = padding + (index / (equityCurve.length - 1)) * (width - 2 * padding); const y = height - padding - ((value - minValue) / valueRange) * (height - 2 * padding); return { x, y }; }); const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("width", width); svg.setAttribute("height", height); // Draw axes const xAxis = document.createElementNS(svgNS, "line"); xAxis.setAttribute("x1", padding); xAxis.setAttribute("y1", height - padding); xAxis.setAttribute("x2", width - padding); xAxis.setAttribute("y2", height - padding); xAxis.setAttribute("stroke", "black"); svg.appendChild(xAxis); const yAxis = document.createElementNS(svgNS, "line"); yAxis.setAttribute("x1", padding); yAxis.setAttribute("y1", padding); yAxis.setAttribute("x2", padding); yAxis.setAttribute("y2", height - padding); yAxis.setAttribute("stroke", "black"); svg.appendChild(yAxis); // Add labels const xLabel = document.createElementNS(svgNS, "text"); xLabel.textContent = "Time (Steps)"; xLabel.setAttribute("x", width / 2); xLabel.setAttribute("y", height - 5); xLabel.setAttribute("text-anchor", "middle"); svg.appendChild(xLabel); const yLabel = document.createElementNS(svgNS, "text"); yLabel.textContent = "Equity Value"; yLabel.setAttribute("x", -height / 2); yLabel.setAttribute("y", 15); yLabel.setAttribute("transform", "rotate(-90)"); yLabel.setAttribute("text-anchor", "middle"); svg.appendChild(yLabel); // Draw equity curve const polyline = document.createElementNS(svgNS, "polyline"); const points = normalizedData.map(point => `${point.x},${point.y}`).join(' '); polyline.setAttribute("points", points); polyline.setAttribute("fill", "none"); polyline.setAttribute("stroke", "blue"); polyline.setAttribute("stroke-width", "2"); svg.appendChild(polyline); equityCurveChart.appendChild(svg); } stopRunningAnimation(results) { this.hideElement(this.progressContainer); this.clearMessage(); this.displayTestResults(results); } fetchSavedTests() { this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); } updateHTML() { let html = ''; for (const test of this.tests) { const statusClass = test.status || 'default'; // Use the status or fallback to 'default' html += `
${test.name}
`; } this.backtestDisplay.innerHTML = html; } openTestDialog(testName) { const test = this.tests.find(t => t.name === testName); if (!test) { alert('Test not found.'); return; } this.currentTest = testName; // Set the currently open test // Populate the strategy dropdown this.populateStrategyDropdown(); // Validate and set strategy const availableStrategies = this.getAvailableStrategies(); const matchedStrategy = availableStrategies.find(s => s.name === test.strategy || s.tbl_key === test.strategy); if (matchedStrategy) { this.strategyDropdown.value = matchedStrategy.tbl_key; // Set dropdown to tbl_key } else { console.warn(`openTestDialog: Strategy "${test.strategy}" not found in dropdown. Defaulting to first available.`); this.strategyDropdown.value = availableStrategies[0]?.tbl_key || ''; } // Populate other form fields document.getElementById('start_date').value = test.start_date ? this.formatDateToLocalInput(new Date(test.start_date)) : this.formatDateToLocalInput(new Date(Date.now() - 60 * 60 * 1000)); // 1 hour ago document.getElementById('initial_capital').value = test.results?.initial_capital || 10000; document.getElementById('commission').value = test.results?.commission || 0.001; console.log(`openTestDialog: Set start_date to ${document.getElementById('start_date').value}`); // Display results or show progress if (test.status === 'complete') { this.displayTestResults(test.results); this.hideElement(this.progressContainer); } else { this.hideElement(this.resultsContainer); this.showElement(this.progressContainer); this.updateProgressBar(test.progress); this.displayMessage('Backtest in progress...', 'blue'); } // Update header and show form this.setText(this.backtestDraggableHeader, `Edit Backtest - ${test.name}`); // Manage button visibility this.showElement(document.getElementById('backtest-submit-edit')); this.hideElement(document.getElementById('backtest-submit-create')); this.formElement.style.display = "grid"; console.log(`openTestDialog: Opened dialog for backtest "${test.name}".`); } runTest(testName) { const testData = { name: testName, user_name: this.ui.data.user_name }; this.comms.sendToApp('run_backtest', testData); } deleteTest(testName) { const testData = { name: testName, user_name: this.ui.data.user_name }; this.comms.sendToApp('delete_backtest', testData); } populateStrategyDropdown() { if (!this.strategyDropdown) { console.error('Strategy dropdown element not found.'); return; } this.strategyDropdown.innerHTML = ''; // Clear existing options const strategies = this.getAvailableStrategies(); if (!strategies || strategies.length === 0) { console.warn('No strategies available to populate dropdown.'); return; } strategies.forEach(strategy => { const option = document.createElement('option'); option.value = strategy.tbl_key; // Use tbl_key as the value option.text = strategy.name; // Use strategy name as the display text this.strategyDropdown.appendChild(option); }); } openForm(testName = null) { if (testName) { this.openTestDialog(testName); } else { this.currentTest = null; // Reset the currently open test // Populate the strategy dropdown this.populateStrategyDropdown(); // Update header and show form this.setText(this.backtestDraggableHeader, "Create New Backtest"); this.clearForm(); // Set default start_date to 1 hour ago const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); // Current time minus 1 hour const formattedDate = this.formatDateToLocalInput(oneHourAgo); document.getElementById('start_date').value = formattedDate; console.log(`openForm: Set default start_date to ${formattedDate}`); // Manage button visibility this.showElement(document.getElementById('backtest-submit-create')); this.hideElement(document.getElementById('backtest-submit-edit')); this.formElement.style.display = "grid"; console.log('openForm: Opened form for creating a new backtest.'); } } closeForm() { this.formElement.style.display = "none"; this.currentTest = null; // Reset the currently open test // Optionally hide progress/results to avoid stale UI this.hideElement(this.resultsContainer); this.hideElement(this.progressContainer); this.clearMessage(); } clearForm() { if (this.strategyDropdown) this.strategyDropdown.value = ''; document.getElementById('start_date').value = ''; document.getElementById('initial_capital').value = 10000; document.getElementById('commission').value = 0.001; } submitTest() { // Retrieve selected strategy const strategyTblKey = this.strategyDropdown ? this.strategyDropdown.value : null; const selectedStrategy = this.getAvailableStrategies().find(s => s.tbl_key === strategyTblKey); const strategyName = selectedStrategy ? selectedStrategy.name : null; const start_date = document.getElementById('start_date').value; const capital = parseFloat(document.getElementById('initial_capital').value) || 10000; const commission = parseFloat(document.getElementById('commission').value) || 0.001; // Validate strategy selection if (!strategyTblKey || !strategyName) { alert("Please select a strategy."); console.log('submitTest: Submission failed - No strategy selected.'); return; } const now = new Date(); const startDate = new Date(start_date); // Validate start date if (startDate > now) { alert("Start date cannot be in the future."); console.log('submitTest: Submission failed - Start date is in the future.'); return; } let testName; if (this.currentTest && this.tests.find(t => t.name === this.currentTest)) { // Editing an existing test testName = this.currentTest; console.log(`submitTest: Editing existing backtest "${testName}".`); } else { // Creating a new test using the strategy's name for readability testName = `${strategyName}_backtest`; console.log(`submitTest: Creating new backtest "${testName}".`); } // Check if a test with the same name is already running if (this.tests.find(t => t.name === testName && t.status === 'running')) { alert(`A test named "${testName}" is already running.`); console.log(`submitTest: Submission blocked - "${testName}" is already running.`); return; } // Prepare test data payload const testData = { strategy: strategyTblKey, // Use tbl_key as identifier start_date, capital, commission, user_name: this.ui.data.user_name, backtest_name: testName, }; // Disable the submit button to prevent duplicate submissions const submitButton = document.getElementById('backtest-submit-create'); if (submitButton) { submitButton.disabled = true; } // Submit the test data to the backend this.comms.sendToApp('submit_backtest', testData); // Log the submission and keep the form open for progress monitoring console.log('submitTest: Backtest data submitted and form remains open for progress monitoring.'); // Re-enable the submit button after a delay setTimeout(() => { if (submitButton) { submitButton.disabled = false; } }, 2000); // Example: Re-enable after 2 seconds or on callback } // Utility function to format Date object to 'YYYY-MM-DDTHH:MM' format formatDateToLocalInput(date) { const pad = (num) => num.toString().padStart(2, '0'); const year = date.getFullYear(); const month = pad(date.getMonth() + 1); // Months are zero-based const day = pad(date.getDate()); const hours = pad(date.getHours()); const minutes = pad(date.getMinutes()); return `${year}-${month}-${day}T${hours}:${minutes}`; } }