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 typing import Any
|
||||||
|
|
||||||
from fastapi.responses import RedirectResponse
|
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.domain.backtesting_math import asset_quantity_from_workspace_config
|
||||||
from app.models.backtest import ProviderRef
|
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
|
# Keep existing entry spot, don't re-derive
|
||||||
mark_results_stale()
|
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("")
|
validation_label.set_text("")
|
||||||
|
|
||||||
|
# Show loading state
|
||||||
|
run_button.props('loading')
|
||||||
|
validation_label.set_text("Running backtest...")
|
||||||
|
ui.notify("Running backtest...", type="info")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate date range for symbol
|
# Validate date range for symbol
|
||||||
start_date = parse_iso_date(start_input.value, "Start date")
|
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."
|
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")
|
render_result_state("Invalid start date", validation_label.text, tone="warning")
|
||||||
|
run_button.props(remove='loading')
|
||||||
return
|
return
|
||||||
|
|
||||||
date_range_error = validate_date_range_for_symbol(start_date, end_date, symbol)
|
date_range_error = validate_date_range_for_symbol(start_date, end_date, symbol)
|
||||||
if date_range_error:
|
if date_range_error:
|
||||||
validation_label.set_text(date_range_error)
|
validation_label.set_text(date_range_error)
|
||||||
render_result_state("Scenario validation failed", date_range_error, tone="warning")
|
render_result_state("Scenario validation failed", date_range_error, tone="warning")
|
||||||
|
run_button.props(remove='loading')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Validate numeric inputs
|
# Validate numeric inputs
|
||||||
@@ -846,12 +855,15 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
if numeric_error:
|
if numeric_error:
|
||||||
validation_label.set_text(numeric_error)
|
validation_label.set_text(numeric_error)
|
||||||
render_result_state("Input validation failed", numeric_error, tone="warning")
|
render_result_state("Input validation failed", numeric_error, tone="warning")
|
||||||
|
run_button.props(remove='loading')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save settings before running
|
# Save settings before running
|
||||||
save_backtest_settings()
|
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,
|
symbol=symbol,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_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
|
# Update cost in saved settings after successful run
|
||||||
if str(data_source_select.value) == "databento":
|
if str(data_source_select.value) == "databento":
|
||||||
update_cost_estimate()
|
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:
|
except (ValueError, KeyError) as exc:
|
||||||
|
run_button.props(remove='loading')
|
||||||
entry_spot, entry_error = derive_entry_spot()
|
entry_spot, entry_error = derive_entry_spot()
|
||||||
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
||||||
if entry_spot is None:
|
if entry_spot is None:
|
||||||
entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.")
|
entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.")
|
||||||
validation_label.set_text(str(exc))
|
validation_label.set_text(str(exc))
|
||||||
render_result_state("Scenario validation failed", str(exc), tone="warning")
|
render_result_state("Scenario validation failed", str(exc), tone="warning")
|
||||||
return
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
run_button.props(remove='loading')
|
||||||
entry_spot, entry_error = derive_entry_spot()
|
entry_spot, entry_error = derive_entry_spot()
|
||||||
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
|
||||||
if entry_spot is None:
|
if entry_spot is None:
|
||||||
@@ -912,8 +932,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
)
|
)
|
||||||
validation_label.set_text(message)
|
validation_label.set_text(message)
|
||||||
render_result_state("Backtest failed", message, tone="error")
|
render_result_state("Backtest failed", message, tone="error")
|
||||||
return
|
|
||||||
render_result(result)
|
|
||||||
|
|
||||||
# Wire up event handlers
|
# Wire up event handlers
|
||||||
# Only call expensive derive_entry_spot on date changes
|
# Only call expensive derive_entry_spot on date changes
|
||||||
|
|||||||
Reference in New Issue
Block a user