fix(UX-001): address layout review findings

This commit is contained in:
Bu5hm4nn
2026-03-26 10:24:52 +01:00
parent a60c5fb1f2
commit 78de8782c4
7 changed files with 238 additions and 98 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from datetime import date, datetime
from fastapi.responses import RedirectResponse
@@ -10,6 +11,8 @@ from app.models.workspace import get_workspace_repository
from app.pages.common import dashboard_page, split_page_panes
from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService
logger = logging.getLogger(__name__)
def _chart_options(result: BacktestPageRunResult) -> dict:
template_result = result.run_result.template_results[0]
@@ -133,18 +136,23 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
except ValueError as exc:
raise ValueError(f"{field_name} must be in YYYY-MM-DD format") from exc
def render_seeded_summary(*, entry_spot: float | None = None) -> None:
def derive_entry_spot() -> tuple[float | None, str | None]:
try:
resolved_entry_spot = service.derive_entry_spot(
str(symbol_input.value or "GLD"),
parse_iso_date(start_input.value, "Start date"),
parse_iso_date(end_input.value, "End date"),
)
except (ValueError, KeyError) as exc:
return None, str(exc)
return resolved_entry_spot, None
def render_seeded_summary(*, entry_spot: float | None = None, entry_spot_error: str | None = None) -> None:
seeded_summary.clear()
resolved_entry_spot = entry_spot
if resolved_entry_spot is None:
try:
resolved_entry_spot = service.derive_entry_spot(
str(symbol_input.value or "GLD"),
parse_iso_date(start_input.value, "Start date"),
parse_iso_date(end_input.value, "End date"),
)
except (ValueError, KeyError):
resolved_entry_spot = None
resolved_error = entry_spot_error
if resolved_entry_spot is None and resolved_error is None:
resolved_entry_spot, resolved_error = derive_entry_spot()
with seeded_summary:
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
@@ -159,7 +167,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
),
(
"Entry spot",
f"${resolved_entry_spot:,.2f}" if resolved_entry_spot is not None else "Pending",
f"${resolved_entry_spot:,.2f}" if resolved_entry_spot is not None else "Unavailable",
),
]
for label, value in cards:
@@ -168,15 +176,34 @@ def _render_backtests_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 resolved_error:
ui.label(resolved_error).classes("text-sm text-amber-700 dark:text-amber-300")
def render_result_validation(message: str) -> None:
def render_result_state(title: str, message: str, *, tone: str = "info") -> None:
tone_classes = {
"info": "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/30",
"warning": "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30",
"error": "border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/30",
}
text_classes = {
"info": "text-sky-800 dark:text-sky-200",
"warning": "text-amber-800 dark:text-amber-200",
"error": "text-rose-800 dark:text-rose-200",
}
result_panel.clear()
with result_panel:
with ui.card().classes(
"w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30"
f"w-full rounded-2xl border shadow-sm {tone_classes.get(tone, tone_classes['info'])}"
):
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200")
ui.label(title).classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(message).classes(f"text-sm {text_classes.get(tone, text_classes['info'])}")
def mark_results_stale() -> None:
render_result_state(
"Results out of date",
"Inputs changed. Run backtest again to refresh charts and daily results for the current scenario.",
tone="info",
)
def render_result(result: BacktestPageRunResult) -> None:
result_panel.clear()
@@ -263,20 +290,23 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
).classes("w-full")
def refresh_workspace_seeded_units() -> None:
if not workspace_id or config is None or config.gold_value is None:
return
try:
entry_spot = service.derive_entry_spot(
str(symbol_input.value or "GLD"),
parse_iso_date(start_input.value, "Start date"),
parse_iso_date(end_input.value, "End date"),
)
except (ValueError, KeyError):
render_seeded_summary(entry_spot=None)
return
units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD")
entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}")
render_seeded_summary(entry_spot=entry_spot)
entry_spot, entry_error = derive_entry_spot()
if (
workspace_id
and config is not None
and config.gold_value is not None
and entry_spot is not None
):
units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD")
entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}")
else:
entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.")
render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error)
mark_results_stale()
def on_form_change() -> None:
render_seeded_summary()
mark_results_stale()
def run_backtest() -> None:
validation_label.set_text("")
@@ -291,25 +321,39 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
margin_call_ltv=float(ltv_input.value or 0.0),
)
except (ValueError, KeyError) as exc:
render_seeded_summary(entry_spot=None)
render_seeded_summary(entry_spot=None, entry_spot_error=None)
validation_label.set_text(str(exc))
render_result_validation(str(exc))
render_result_state("Scenario validation failed", str(exc), tone="warning")
return
except Exception:
render_seeded_summary(entry_spot=None)
render_seeded_summary(entry_spot=None, entry_spot_error=None)
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_input.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_validation(message)
render_result_state("Backtest failed", message, tone="error")
return
render_result(result)
if workspace_id:
start_input.on_value_change(lambda _event: refresh_workspace_seeded_units())
end_input.on_value_change(lambda _event: refresh_workspace_seeded_units())
template_select.on_value_change(lambda _event: render_seeded_summary())
units_input.on_value_change(lambda _event: render_seeded_summary())
loan_input.on_value_change(lambda _event: render_seeded_summary())
ltv_input.on_value_change(lambda _event: render_seeded_summary())
else:
start_input.on_value_change(lambda _event: on_form_change())
end_input.on_value_change(lambda _event: on_form_change())
template_select.on_value_change(lambda _event: on_form_change())
units_input.on_value_change(lambda _event: on_form_change())
loan_input.on_value_change(lambda _event: on_form_change())
ltv_input.on_value_change(lambda _event: on_form_change())
run_button.on_click(lambda: run_backtest())
render_seeded_summary(entry_spot=default_entry_spot)
run_backtest()