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 += `
| Trade ID | Size | Open Price | Close Price | P&L |
|---|---|---|---|---|
| ${trade.ref} | ${size} | ${openPrice} | ${closePrice} | ${pnl} |
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 += `