From 79980c33ec3c13060a68cd9202d93e9b2b6078b2 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Mon, 30 Mar 2026 14:37:04 +0200 Subject: [PATCH] feat(backtest): add dataset-specific date validation and better error handling - Add DATABENTO_DATASET_MIN_DATES for XNAS.BASIC (2024-07-01) and GLBX.MDP3 (2010-01-01) - Validate start date against dataset minimum before running backtest - Parse Databento API errors and show user-friendly messages - Update date range hint to show dataset-specific availability - Catch BentoClientError and show appropriate warning tone --- app/pages/backtests.py | 86 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/app/pages/backtests.py b/app/pages/backtests.py index d2c999c..a27572f 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -48,6 +48,12 @@ SYMBOL_MIN_DATES = { "XAU": date(1970, 1, 1), # XAU index historical data } +# Minimum dates for Databento datasets (when data became available) +DATABENTO_DATASET_MIN_DATES = { + "XNAS.BASIC": date(2024, 7, 1), # XNAS.BASIC data available from July 2024 + "GLBX.MDP3": date(2010, 1, 1), # GLBX.MDP3 futures data from 2010 +} + def get_default_backtest_dates() -> tuple[date, date]: """Get default backtest date range (~2 years ending on most recent Friday or earlier). @@ -446,8 +452,21 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: return "GLD" # Default for XNAS.BASIC def update_date_range_hint() -> None: - """Update the date range hint based on selected symbol.""" + """Update the date range hint based on selected symbol and data source.""" symbol = get_symbol_from_dataset() + data_source = str(data_source_select.value) + + # Use dataset-specific minimum for Databento + if data_source == "databento": + dataset = str(dataset_select.value) + min_date = DATABENTO_DATASET_MIN_DATES.get(dataset) + if min_date: + date_range_hint.set_text( + f"{dataset} data available from {min_date.strftime('%Y-%m-%d')}" + ) + return + + # Fall back to symbol minimum min_date = SYMBOL_MIN_DATES.get(symbol) if min_date: date_range_hint.set_text( @@ -805,6 +824,20 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: start_date = parse_iso_date(start_input.value, "Start date") end_date = parse_iso_date(end_input.value, "End date") symbol = get_symbol_from_dataset() + + # Validate dataset-specific minimum dates for Databento + data_source = str(data_source_select.value) + if data_source == "databento": + dataset = str(dataset_select.value) + dataset_min = DATABENTO_DATASET_MIN_DATES.get(dataset) + if dataset_min and start_date < dataset_min: + validation_label.set_text( + f"Start date must be on or after {dataset_min.strftime('%Y-%m-%d')} for {dataset} dataset. " + 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") + 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) @@ -845,25 +878,46 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: validation_label.set_text(str(exc)) render_result_state("Scenario validation failed", str(exc), tone="warning") return - except Exception: + except Exception as exc: 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.") - message = "Backtest failed. Please verify the scenario inputs and try again." - logger.exception( - "Backtest page run failed for workspace=%s symbol=%s start=%s end=%s template=%s units=%s loan=%s margin_call_ltv=%s", - workspace_id, - symbol_select.value, - start_input.value, - end_input.value, - template_select.value, - units_input.value, - loan_input.value, - ltv_input.value, - ) - validation_label.set_text(message) - render_result_state("Backtest failed", message, tone="error") + + # Check for Databento API errors + error_msg = str(exc) + if "data_start_before_available_start" in error_msg: + # Extract the available start date from the error message + import re + match = re.search(r"available start of dataset [^(]+\('([^']+)'\)", error_msg) + if match: + available_start = match.group(1).split()[0] # Extract date part + validation_label.set_text( + f"Data not available before {available_start}. Please set start date to {available_start} or later." + ) + else: + validation_label.set_text( + "Selected start date is before data is available for this dataset. Please choose a later date." + ) + render_result_state("Invalid start date", validation_label.text, tone="warning") + elif "BentoClientError" in error_msg or "422" in error_msg: + validation_label.set_text(f"Data source error: {error_msg}") + render_result_state("Data unavailable", validation_label.text, tone="warning") + else: + message = "Backtest failed. Please verify the scenario inputs and try again." + logger.exception( + "Backtest page run failed for workspace=%s symbol=%s start=%s end=%s template=%s units=%s loan=%s margin_call_ltv=%s", + workspace_id, + symbol_select.value, + start_input.value, + end_input.value, + template_select.value, + units_input.value, + loan_input.value, + ltv_input.value, + ) + validation_label.set_text(message) + render_result_state("Backtest failed", message, tone="error") return render_result(result)