fix(backtest): run backtest asynchronously to prevent WebSocket timeout
- Use run.io_bound() from NiceGUI to run Databento API calls in background thread - Add loading state to Run Backtest button - Show notification when backtest starts and completes - Remove loading state on completion/error This prevents 'Connection lost' errors when the backtest takes longer than the WebSocket timeout.
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import date, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from fastapi.responses import RedirectResponse
|
||||
from nicegui import ui
|
||||
from nicegui import run, ui
|
||||
|
||||
from app.domain.backtesting_math import asset_quantity_from_workspace_config
|
||||
from app.models.backtest import ProviderRef
|
||||
@@ -811,8 +811,15 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
||||
# Keep existing entry spot, don't re-derive
|
||||
mark_results_stale()
|
||||
|
||||
def run_backtest() -> None:
|
||||
async def run_backtest() -> None:
|
||||
"""Run the backtest asynchronously to avoid blocking the WebSocket."""
|
||||
validation_label.set_text("")
|
||||
|
||||
# Show loading state
|
||||
run_button.props('loading')
|
||||
validation_label.set_text("Running backtest...")
|
||||
ui.notify("Running backtest...", type="info")
|
||||
|
||||
try:
|
||||
# Validate date range for symbol
|
||||
start_date = parse_iso_date(start_input.value, "Start date")
|
||||
@@ -830,12 +837,14 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
||||
f"Selected start date {start_date.strftime('%Y-%m-%d')} is before available data."
|
||||
)
|
||||
render_result_state("Invalid start date", validation_label.text, tone="warning")
|
||||
run_button.props(remove='loading')
|
||||
return
|
||||
|
||||
date_range_error = validate_date_range_for_symbol(start_date, end_date, symbol)
|
||||
if date_range_error:
|
||||
validation_label.set_text(date_range_error)
|
||||
render_result_state("Scenario validation failed", date_range_error, tone="warning")
|
||||
run_button.props(remove='loading')
|
||||
return
|
||||
|
||||
# Validate numeric inputs
|
||||
@@ -846,12 +855,15 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
||||
if numeric_error:
|
||||
validation_label.set_text(numeric_error)
|
||||
render_result_state("Input validation failed", numeric_error, tone="warning")
|
||||
run_button.props(remove='loading')
|
||||
return
|
||||
|
||||
# Save settings before running
|
||||
save_backtest_settings()
|
||||
|
||||
result = service.run_read_only_scenario(
|
||||
# Run backtest in background thread to avoid blocking WebSocket
|
||||
result = await run.io_bound(
|
||||
service.run_read_only_scenario,
|
||||
symbol=symbol,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
@@ -864,15 +876,23 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
||||
# Update cost in saved settings after successful run
|
||||
if str(data_source_select.value) == "databento":
|
||||
update_cost_estimate()
|
||||
|
||||
render_result(result)
|
||||
run_button.props(remove='loading')
|
||||
validation_label.set_text("")
|
||||
ui.notify("Backtest completed!", type="positive")
|
||||
|
||||
except (ValueError, KeyError) as exc:
|
||||
run_button.props(remove='loading')
|
||||
entry_spot, entry_error = derive_entry_spot()
|
||||
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
||||
if entry_spot is None:
|
||||
entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.")
|
||||
validation_label.set_text(str(exc))
|
||||
render_result_state("Scenario validation failed", str(exc), tone="warning")
|
||||
return
|
||||
|
||||
except Exception as exc:
|
||||
run_button.props(remove='loading')
|
||||
entry_spot, entry_error = derive_entry_spot()
|
||||
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
||||
if entry_spot is None:
|
||||
@@ -912,8 +932,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
||||
)
|
||||
validation_label.set_text(message)
|
||||
render_result_state("Backtest failed", message, tone="error")
|
||||
return
|
||||
render_result(result)
|
||||
|
||||
# Wire up event handlers
|
||||
# Only call expensive derive_entry_spot on date changes
|
||||
|
||||
Reference in New Issue
Block a user