From 0ae835d09606c7a4e9d056dc58f328931a34f5d1 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 13 Nov 2024 01:13:29 -0400 Subject: [PATCH] The test are displaying in the user interface. --- src/backtesting.py | 61 +++++++---- src/static/backtesting.js | 176 ++++++++++++++++++++++++++++-- src/static/communication.js | 5 + src/templates/backtest_popup.html | 31 +++++- 4 files changed, 239 insertions(+), 34 deletions(-) diff --git a/src/backtesting.py b/src/backtesting.py index 6c64402..c499369 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -149,9 +149,8 @@ class Backtester: logger.error(f"Error during cleanup of backtest '{backtest_key}': {e}", exc_info=True) - def map_user_strategy(self, user_strategy: dict, precomputed_indicators: dict[str, pd.DataFrame], - mode: str = 'testing') -> any: + mode: str = 'testing', socketio=None, socket_conn_id=None, data_length=None) -> any: """ Maps user strategy details into a Backtrader-compatible strategy class. """ @@ -176,6 +175,9 @@ class Backtester: params = ( ('mode', mode), ('strategy_instance', None), # Will be set during instantiation + ('socketio', socketio), + ('socket_conn_id', socket_conn_id), + ('data_length', data_length) ) def __init__(self): @@ -198,26 +200,35 @@ class Backtester: # Initialize any other needed variables self.starting_balance = self.broker.getvalue() + # Existing code... + self.current_step = 0 + self.last_progress = 0 # Initialize last_progress + def next(self): - # Increment pointers - for name in self.indicator_names: - self.indicator_pointers[name] += 1 - - # Increment current step self.current_step += 1 - - # Generated strategy logic + # Execute the strategy logic try: - # Execute the strategy logic via StrategyInstance execution_result = self.strategy_instance.execute() if not execution_result.get('success', False): error_msg = execution_result.get('message', 'Unknown error during strategy execution.') logger.error(f"Strategy execution failed: {error_msg}") - # Handle the failure (stop the strategy) self.stop() except Exception as e: logger.error(f"Error in strategy execution: {e}") + # Calculate progress + progress = (self.current_step / self.p.data_length) * 100 + progress = min(int(progress), 100) # Ensure progress doesn't exceed 100% + + # Emit progress only if it has increased by at least 1% + if progress > self.last_progress: + self.p.socketio.emit( + 'message', + {'reply': 'progress', 'data': {'progress': progress}}, + room=self.p.socket_conn_id + ) + self.last_progress = progress + return MappedStrategy # Add custom handlers to the StrategyInstance @@ -703,7 +714,7 @@ class Backtester: trades = self.parse_trade_analyzer(trade_analyzer) # Send 100% completion - self.socketio.emit('progress_update', {"progress": 100}, room=socket_conn_id) + self.socketio.emit('message', {'reply': 'progress', 'data': {'progress': 100}}, room=socket_conn_id) # Prepare the results to pass into the callback backtest_results = { @@ -721,7 +732,8 @@ class Backtester: except Exception as e: # Handle exceptions and send error messages to the client error_message = f"Backtest execution failed: {str(e)}" - self.socketio.emit('backtest_error', {"message": error_message}, room=socket_conn_id) + self.socketio.emit('message', {'reply': 'backtest_error', 'data': {'message': error_message}}, + room=socket_conn_id) logger.error(f"[BACKTEST ERROR] {error_message}", exc_info=True) # Invoke callback with failure details to ensure cleanup @@ -786,8 +798,15 @@ class Backtester: # Override any methods that access exchanges and market data with custom handlers for backtesting strategy_instance = self.add_custom_handlers(strategy_instance) - # Map the user strategy to a Backtrader-compatible strategy class - mapped_strategy_class = self.map_user_strategy(user_strategy, precomputed_indicators) + data_length = len(data_feed) + + mapped_strategy_class = self.map_user_strategy( + user_strategy, + precomputed_indicators, + socketio=self.socketio, + socket_conn_id=socket_conn_id, + data_length=data_length + ) # Define the backtest key for caching backtest_key = f"backtest:{user_name}:{backtest_name}" @@ -808,11 +827,11 @@ class Backtester: self.update_strategy_stats(user_id_int, strategy_name, results) # Emit the results back to the client - self.socketio.emit( - 'backtest_results', - {"test_id": backtest_name, "results": results}, - room=socket_conn_id - ) + self.socketio.emit('message', + {"reply": 'backtest_results', + "data": {'test_id': backtest_name, "results": results}}, + room=socket_conn_id + ) logger.info(f"[BACKTEST COMPLETE] Results emitted to user '{user_name}'.") finally: # Cleanup regardless of success or failure @@ -830,7 +849,7 @@ class Backtester: ) logger.info(f"Backtest '{backtest_name}' started for user '{user_name}'.") - return {"reply": "backtest_started"} + return {"status": "started", "backtest_name": backtest_name} def start_periodic_purge(self, interval_seconds: int = 3600): """ diff --git a/src/static/backtesting.js b/src/static/backtesting.js index f9ea634..44fcb22 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -6,11 +6,57 @@ class Backtesting { this.target_id = 'backtest_display'; // The container to display backtests // 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('progress', this.handleProgress.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)); - this.comms.on('updates', this.handleUpdates.bind(this)); + } + + handleBacktestSubmitted(data) { + console.log("Backtest response received:", data.status); + if (data.status === "started") { + // Show the progress bar or any other UI updates + this.showRunningAnimation(); + + // Add the new backtest to the tests array + const newTest = { + name: data.backtest_name, + strategy: data.strategy_name, + start_date: data.start_date, + // Include any other relevant data from the response if available + }; + this.tests.push(newTest); + + // Update the HTML to reflect the new backtest + this.updateHTML(); + } + } + + handleBacktestError(data) { + console.error("Backtest error:", data.message); + + // Display error message in the status message area + const statusMessage = document.getElementById('backtest-status-message'); + if (statusMessage) { + statusMessage.style.display = 'block'; + statusMessage.style.color = 'red'; // Set text color to red for errors + statusMessage.textContent = `Backtest error: ${data.message}`; + } else { + // Fallback to alert if the element is not found + alert(`Backtest error: ${data.message}`); + } + + // Optionally hide progress bar and results + const progressContainer = document.getElementById('backtest-progress-container'); + if (progressContainer) { + progressContainer.classList.remove('show'); + } + const resultsContainer = document.getElementById('backtest-results'); + if (resultsContainer) { + resultsContainer.style.display = 'none'; + } } handleBacktestResults(data) { @@ -25,6 +71,7 @@ class Backtesting { this.updateProgressBar(data.progress); } + handleBacktestsList(data) { console.log("Backtests list received:", data.tests); // Logic to update backtesting UI @@ -37,48 +84,149 @@ class Backtesting { this.fetchSavedTests(); } - handleUpdates(data) { - const { trade_updts } = data; - if (trade_updts) { - this.ui.trade.update_received(trade_updts); - } - } - updateProgressBar(progress) { const progressBar = document.getElementById('progress_bar'); if (progressBar) { + console.log(`Updating progress bar to ${progress}%`); progressBar.style.width = `${progress}%`; progressBar.textContent = `${progress}%`; + } else { + console.log('Progress bar element not found'); } } + showRunningAnimation() { const resultsContainer = document.getElementById('backtest-results'); const resultsDisplay = document.getElementById('results_display'); const progressContainer = document.getElementById('backtest-progress-container'); const progressBar = document.getElementById('progress_bar'); + const statusMessage = document.getElementById('backtest-status-message'); resultsContainer.style.display = 'none'; - progressContainer.style.display = 'block'; + progressContainer.classList.add('show'); // Use class to control display progressBar.style.width = '0%'; progressBar.textContent = '0%'; resultsDisplay.innerHTML = ''; + statusMessage.style.display = 'block'; + statusMessage.style.color = 'blue'; // Reset text color to blue + statusMessage.textContent = 'Backtest started...'; } + + displayTestResults(results) { const resultsContainer = document.getElementById('backtest-results'); const resultsDisplay = document.getElementById('results_display'); resultsContainer.style.display = 'block'; - resultsDisplay.innerHTML = `
${JSON.stringify(results, null, 2)}
`; + + // Calculate total return + const totalReturn = (((results.final_portfolio_value - results.initial_capital) / results.initial_capital) * 100).toFixed(2); + + // Create HTML content + let html = ` + Initial Capital: ${results.initial_capital} + Final Portfolio Value: ${results.final_portfolio_value} + Total Return: ${totalReturn}% + Run Duration: ${results.run_duration.toFixed(2)} seconds + `; + + // Add a container for the chart + html += `

Equity Curve

+
`; + + // If there are trades, display them + if (results.trades && results.trades.length > 0) { + html += `

Trades Executed

+
+ + + + + + + `; + results.trades.forEach(trade => { + html += ` + + + + + `; + }); + html += `
Trade IDSizePriceP&L
${trade.ref}${trade.size}${trade.price}${trade.pnl}
`; + } else { + html += `

No trades were executed.

`; + } + + resultsDisplay.innerHTML = html; + + // Generate the equity curve chart + this.drawEquityCurveChart(results.equity_curve); } + drawEquityCurveChart(equityCurve) { + const chartContainer = document.getElementById('equity_curve_chart'); + if (!chartContainer) { + console.error('Chart container not found'); + return; + } + + // Get the dimensions of the container + const width = chartContainer.clientWidth || 600; + const height = chartContainer.clientHeight || 300; + const padding = 40; + + // Find min and max values + const minValue = Math.min(...equityCurve); + const maxValue = Math.max(...equityCurve); + + // Avoid division by zero if all values are the same + const valueRange = maxValue - minValue || 1; + + // Normalize data points + 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 }; + }); + + // Create SVG content + let svgContent = ``; + + // Draw axes + svgContent += ``; // X-axis + svgContent += ``; // Y-axis + + // Draw equity curve + svgContent += ``; + + // Close SVG + svgContent += ``; + + // Set SVG content + chartContainer.innerHTML = svgContent; + } + stopRunningAnimation(results) { const progressContainer = document.getElementById('backtest-progress-container'); - progressContainer.style.display = 'none'; + progressContainer.classList.remove('show'); + + // Hide the status message + const statusMessage = document.getElementById('backtest-status-message'); + statusMessage.style.display = 'none'; + statusMessage.textContent = ''; + this.displayTestResults(results); } + + fetchSavedTests() { this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); } @@ -151,6 +299,10 @@ class Backtesting { closeForm() { document.getElementById("backtest_form").style.display = "none"; + // Hide and clear the status message + const statusMessage = document.getElementById('backtest-status-message'); + statusMessage.style.display = 'none'; + statusMessage.textContent = ''; } clearForm() { diff --git a/src/static/communication.js b/src/static/communication.js index 0b9d532..e8adaa0 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -62,12 +62,17 @@ class Comms { } else if (data.reply === 'error') { console.error('Socket.IO: Authentication error:', data.data); // Optionally, handle authentication errors (e.g., redirect to login) + } else if (data.reply === 'progress') { + // Handle progress updates specifically + console.log('Progress update received:', data.data.progress); + this.emit('backtest_progress', data.data); } else { // Emit the event to registered handlers this.emit(data.reply, data.data); } }); } + /** * Flushes the message queue by sending all queued messages. */ diff --git a/src/templates/backtest_popup.html b/src/templates/backtest_popup.html index 0caac51..6b079c4 100644 --- a/src/templates/backtest_popup.html +++ b/src/templates/backtest_popup.html @@ -40,8 +40,11 @@ + + + -