640 lines
25 KiB
JavaScript
640 lines
25 KiB
JavaScript
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 = `
|
|
<span><strong>Initial Capital:</strong> ${results.initial_capital}</span><br>
|
|
<span><strong>Final Portfolio Value:</strong> ${results.final_portfolio_value}</span><br>
|
|
<span><strong>Total Return:</strong> ${(((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2)}%</span><br>
|
|
<span><strong>Run Duration:</strong> ${results.run_duration ? results.run_duration.toFixed(2) : 'N/A'} seconds</span>
|
|
`;
|
|
|
|
// Equity Curve
|
|
html += `
|
|
<h4>Equity Curve</h4>
|
|
<div id="equity_curve_chart" style="width: 100%; height: 300px;"></div>
|
|
`;
|
|
|
|
// Stats Section
|
|
if (results.stats) {
|
|
html += `
|
|
<h4>Statistics</h4>
|
|
<div class="stats-container" style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
`;
|
|
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 += `
|
|
<div class="stat-item" title="${description}" style="flex: 1 1 30%; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
|
<strong>${this.formatStatKey(key)}:</strong>
|
|
<span>${formattedValue}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
|
|
// Trades Table
|
|
if (results.trades && results.trades.length > 0) {
|
|
html += `
|
|
<h4>Trades Executed</h4>
|
|
<div style="max-height: 200px; overflow-y: auto;">
|
|
<table border="1" cellpadding="5" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>Trade ID</th>
|
|
<th>Size</th>
|
|
<th>Open Price</th>
|
|
<th>Close Price</th>
|
|
<th>P&L</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
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 += `
|
|
<tr>
|
|
<td>${trade.ref}</td>
|
|
<td>${size}</td>
|
|
<td>${openPrice}</td>
|
|
<td>${closePrice}</td>
|
|
<td>${pnl}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
html += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
else {
|
|
html += `<p>No trades were executed.</p>`;
|
|
}
|
|
|
|
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 += `
|
|
<div class="backtest-item ${statusClass}" onclick="UI.backtesting.openTestDialog('${test.name}')">
|
|
<button class="delete-button" onclick="UI.backtesting.deleteTest('${test.name}'); event.stopPropagation();">✘</button>
|
|
<div class="backtest-name">${test.name}</div>
|
|
</div>`;
|
|
}
|
|
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}`;
|
|
}
|
|
|
|
|
|
}
|