fix: address PR review issues for event comparison and backtests
Critical fixes: - Add validate_and_calculate_units() helper with proper error handling - Handle division by zero for entry_spot in refresh_preview() and render_report() - Add server-side validation for initial_value > 0 - Add try/except for derive_entry_spot() to handle fixture source limitations Important improvements: - Add dynamic default dates with get_default_backtest_dates() - Add validate_date_range_for_symbol() for symbol-specific date bounds - Add SYMBOL_MIN_DATES validation for backtests - Update date_range_hint based on selected symbol Tests: - Add test_page_validation.py with 21 tests for: - validate_and_calculate_units edge cases - validate_date_range_for_symbol bounds checking - get_default_backtest_dates dynamic generation - SYMBOL_MIN_DATES constant verification
This commit is contained in:
@@ -13,6 +13,18 @@ from app.services.event_comparison_ui import EventComparisonPageService
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_and_calculate_units(initial_value: float, entry_spot: float) -> tuple[float, str | None]:
|
||||
"""Validate inputs and calculate underlying units.
|
||||
|
||||
Returns (units, error_message). If error_message is not None, units is 0.0.
|
||||
"""
|
||||
if initial_value <= 0:
|
||||
return 0.0, "Initial portfolio value must be positive."
|
||||
if entry_spot <= 0:
|
||||
return 0.0, "Cannot calculate units: entry spot is invalid. Please select a valid preset."
|
||||
return initial_value / entry_spot, None
|
||||
|
||||
|
||||
def _chart_options(dates: tuple[str, ...], series: tuple[dict[str, object], ...]) -> dict:
|
||||
return {
|
||||
"tooltip": {"trigger": "axis"},
|
||||
@@ -143,10 +155,13 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
selected_summary.clear()
|
||||
with selected_summary:
|
||||
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
# Calculate underlying units from initial value and entry spot
|
||||
computed_units = 0.0
|
||||
if entry_spot is not None and entry_spot > 0:
|
||||
computed_units = float(initial_value_input.value or 0.0) / entry_spot
|
||||
# Calculate underlying units with validation
|
||||
initial_value = float(initial_value_input.value or 0.0)
|
||||
computed_units, units_error = (
|
||||
validate_and_calculate_units(initial_value, entry_spot)
|
||||
if entry_spot is not None
|
||||
else (0.0, "Entry spot unavailable.")
|
||||
)
|
||||
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
|
||||
cards = [
|
||||
(
|
||||
@@ -165,8 +180,11 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
):
|
||||
ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100")
|
||||
if entry_spot_error:
|
||||
ui.label(entry_spot_error).classes("text-sm text-amber-700 dark:text-amber-300")
|
||||
# Show validation errors (units_error takes priority, then entry_spot_error)
|
||||
display_error = units_error or entry_spot_error
|
||||
if display_error:
|
||||
tone_class = "text-rose-600 dark:text-rose-300" if "must be positive" in display_error else "text-amber-700 dark:text-amber-300"
|
||||
ui.label(display_error).classes(f"text-sm {tone_class}")
|
||||
|
||||
def render_result_state(title: str, message: str, *, tone: str = "info") -> None:
|
||||
tone_classes = {
|
||||
@@ -218,8 +236,13 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
preset_slug=str(option["slug"]),
|
||||
template_slugs=template_slugs,
|
||||
)
|
||||
# Calculate underlying units from initial value and entry spot
|
||||
preview_units = preview_initial_value / preview_entry_spot if preview_entry_spot > 0 else 0.0
|
||||
# Validate and calculate underlying units
|
||||
preview_units, units_error = validate_and_calculate_units(preview_initial_value, preview_entry_spot)
|
||||
if units_error:
|
||||
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
||||
scenario_label.set_text(units_error)
|
||||
render_selected_summary(entry_spot=preview_entry_spot, entry_spot_error=units_error)
|
||||
return units_error
|
||||
|
||||
if workspace_id and config is not None and reseed_units:
|
||||
# Recalculate from workspace config
|
||||
@@ -282,7 +305,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
result_panel.clear()
|
||||
template_slugs = selected_template_slugs()
|
||||
try:
|
||||
# Get initial portfolio value and calculate underlying units
|
||||
# Get initial portfolio value and calculate underlying units with validation
|
||||
initial_value = float(initial_value_input.value or 0.0)
|
||||
# Get entry spot from preview
|
||||
option = preset_lookup.get(str(preset_select.value or ""))
|
||||
@@ -293,7 +316,12 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
preset_slug=str(option["slug"]),
|
||||
template_slugs=template_slugs,
|
||||
)
|
||||
underlying_units = initial_value / entry_spot if entry_spot > 0 else 0.0
|
||||
# Validate and calculate underlying units
|
||||
underlying_units, units_error = validate_and_calculate_units(initial_value, entry_spot)
|
||||
if units_error:
|
||||
validation_label.set_text(units_error)
|
||||
render_result_state("Input validation failed", units_error, tone="warning")
|
||||
return
|
||||
|
||||
report = service.run_read_only_comparison(
|
||||
preset_slug=str(preset_select.value or ""),
|
||||
|
||||
Reference in New Issue
Block a user