refactor(pre-alpha): fail closed on historical preview fallbacks
This commit is contained in:
@@ -289,9 +289,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
row_key="date",
|
row_key="date",
|
||||||
).classes("w-full")
|
).classes("w-full")
|
||||||
|
|
||||||
def validate_current_scenario() -> str | None:
|
def validate_current_scenario(*, entry_spot: float | None = None) -> str | None:
|
||||||
try:
|
try:
|
||||||
service.run_read_only_scenario(
|
service.validate_preview_inputs(
|
||||||
symbol=str(symbol_input.value or ""),
|
symbol=str(symbol_input.value or ""),
|
||||||
start_date=parse_iso_date(start_input.value, "Start date"),
|
start_date=parse_iso_date(start_input.value, "Start date"),
|
||||||
end_date=parse_iso_date(end_input.value, "End date"),
|
end_date=parse_iso_date(end_input.value, "End date"),
|
||||||
@@ -299,6 +299,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
underlying_units=float(units_input.value or 0.0),
|
underlying_units=float(units_input.value or 0.0),
|
||||||
loan_amount=float(loan_input.value or 0.0),
|
loan_amount=float(loan_input.value or 0.0),
|
||||||
margin_call_ltv=float(ltv_input.value or 0.0),
|
margin_call_ltv=float(ltv_input.value or 0.0),
|
||||||
|
entry_spot=entry_spot,
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError) as exc:
|
except (ValueError, KeyError) as exc:
|
||||||
return str(exc)
|
return str(exc)
|
||||||
@@ -320,12 +321,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
def refresh_workspace_seeded_units() -> None:
|
def refresh_workspace_seeded_units() -> None:
|
||||||
validation_label.set_text("")
|
validation_label.set_text("")
|
||||||
entry_spot, entry_error = derive_entry_spot()
|
entry_spot, entry_error = derive_entry_spot()
|
||||||
if (
|
if workspace_id and config is not None and config.gold_value is not None and entry_spot is not None:
|
||||||
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")
|
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}")
|
entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}")
|
||||||
else:
|
else:
|
||||||
@@ -335,7 +331,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
validation_label.set_text(entry_error)
|
validation_label.set_text(entry_error)
|
||||||
render_result_state("Scenario validation failed", entry_error, tone="warning")
|
render_result_state("Scenario validation failed", entry_error, tone="warning")
|
||||||
return
|
return
|
||||||
validation_error = validate_current_scenario()
|
validation_error = validate_current_scenario(entry_spot=entry_spot)
|
||||||
if validation_error:
|
if validation_error:
|
||||||
validation_label.set_text(validation_error)
|
validation_label.set_text(validation_error)
|
||||||
render_result_state("Scenario validation failed", validation_error, tone="warning")
|
render_result_state("Scenario validation failed", validation_error, tone="warning")
|
||||||
@@ -351,7 +347,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
validation_label.set_text(entry_error)
|
validation_label.set_text(entry_error)
|
||||||
render_result_state("Scenario validation failed", entry_error, tone="warning")
|
render_result_state("Scenario validation failed", entry_error, tone="warning")
|
||||||
return
|
return
|
||||||
validation_error = validate_current_scenario()
|
validation_error = validate_current_scenario(entry_spot=entry_spot)
|
||||||
if validation_error:
|
if validation_error:
|
||||||
validation_label.set_text(validation_error)
|
validation_label.set_text(validation_error)
|
||||||
render_result_state("Scenario validation failed", validation_error, tone="warning")
|
render_result_state("Scenario validation failed", validation_error, tone="warning")
|
||||||
@@ -371,12 +367,18 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
margin_call_ltv=float(ltv_input.value or 0.0),
|
margin_call_ltv=float(ltv_input.value or 0.0),
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError) as exc:
|
except (ValueError, KeyError) as exc:
|
||||||
render_seeded_summary(entry_spot=None, entry_spot_error=None)
|
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))
|
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
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
render_seeded_summary(entry_spot=None, entry_spot_error=None)
|
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."
|
message = "Backtest failed. Please verify the scenario inputs and try again."
|
||||||
logger.exception(
|
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",
|
"Backtest page run failed for workspace=%s symbol=%s start=%s end=%s template=%s units=%s loan=%s margin_call_ltv=%s",
|
||||||
|
|||||||
@@ -203,13 +203,6 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
syncing_controls["value"] = False
|
syncing_controls["value"] = False
|
||||||
|
|
||||||
template_slugs = selected_template_slugs()
|
template_slugs = selected_template_slugs()
|
||||||
if not template_slugs:
|
|
||||||
template_slugs = tuple(service.default_template_selection(str(option["slug"])))
|
|
||||||
syncing_controls["value"] = True
|
|
||||||
try:
|
|
||||||
template_select.value = list(template_slugs)
|
|
||||||
finally:
|
|
||||||
syncing_controls["value"] = False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
preview_units = float(units_input.value or 0.0)
|
preview_units = float(units_input.value or 0.0)
|
||||||
@@ -243,6 +236,21 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
scenario_label.set_text(str(exc))
|
scenario_label.set_text(str(exc))
|
||||||
render_selected_summary(entry_spot=None, entry_spot_error=str(exc))
|
render_selected_summary(entry_spot=None, entry_spot_error=str(exc))
|
||||||
return str(exc)
|
return str(exc)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Event comparison preview failed for workspace=%s preset=%s templates=%s units=%s loan=%s margin_call_ltv=%s",
|
||||||
|
workspace_id,
|
||||||
|
preset_select.value,
|
||||||
|
selected_template_slugs(),
|
||||||
|
units_input.value,
|
||||||
|
loan_input.value,
|
||||||
|
ltv_input.value,
|
||||||
|
)
|
||||||
|
message = "Event comparison preview failed. Please verify the seeded inputs and try again."
|
||||||
|
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
||||||
|
scenario_label.set_text(message)
|
||||||
|
render_selected_summary(entry_spot=None, entry_spot_error=message)
|
||||||
|
return message
|
||||||
preset = service.event_preset_service.get_preset(str(option["slug"]))
|
preset = service.event_preset_service.get_preset(str(option["slug"]))
|
||||||
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
||||||
scenario_label.set_text(
|
scenario_label.set_text(
|
||||||
@@ -261,15 +269,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
def render_report() -> None:
|
def render_report() -> None:
|
||||||
validation_label.set_text("")
|
validation_label.set_text("")
|
||||||
result_panel.clear()
|
result_panel.clear()
|
||||||
option = preset_lookup.get(str(preset_select.value or ""))
|
|
||||||
template_slugs = selected_template_slugs()
|
template_slugs = selected_template_slugs()
|
||||||
if option is not None and not template_slugs:
|
|
||||||
template_slugs = tuple(service.default_template_selection(str(option["slug"])))
|
|
||||||
syncing_controls["value"] = True
|
|
||||||
try:
|
|
||||||
template_select.value = list(template_slugs)
|
|
||||||
finally:
|
|
||||||
syncing_controls["value"] = False
|
|
||||||
try:
|
try:
|
||||||
report = service.run_read_only_comparison(
|
report = service.run_read_only_comparison(
|
||||||
preset_slug=str(preset_select.value or ""),
|
preset_slug=str(preset_select.value or ""),
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class BacktestPageService:
|
|||||||
)
|
)
|
||||||
return history[0].close
|
return history[0].close
|
||||||
|
|
||||||
def run_read_only_scenario(
|
def validate_preview_inputs(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
@@ -108,7 +108,8 @@ class BacktestPageService:
|
|||||||
underlying_units: float,
|
underlying_units: float,
|
||||||
loan_amount: float,
|
loan_amount: float,
|
||||||
margin_call_ltv: float,
|
margin_call_ltv: float,
|
||||||
) -> BacktestPageRunResult:
|
entry_spot: float | None = None,
|
||||||
|
) -> float:
|
||||||
normalized_symbol = symbol.strip().upper()
|
normalized_symbol = symbol.strip().upper()
|
||||||
if not normalized_symbol:
|
if not normalized_symbol:
|
||||||
raise ValueError("Symbol is required")
|
raise ValueError("Symbol is required")
|
||||||
@@ -125,9 +126,35 @@ class BacktestPageService:
|
|||||||
if not template_slug:
|
if not template_slug:
|
||||||
raise ValueError("Template selection is required")
|
raise ValueError("Template selection is required")
|
||||||
|
|
||||||
|
self.template_service.get_template(template_slug)
|
||||||
|
resolved_entry_spot = (
|
||||||
|
entry_spot if entry_spot is not None else self.derive_entry_spot(normalized_symbol, start_date, end_date)
|
||||||
|
)
|
||||||
|
_validate_initial_collateral(underlying_units, resolved_entry_spot, loan_amount)
|
||||||
|
return resolved_entry_spot
|
||||||
|
|
||||||
|
def run_read_only_scenario(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
symbol: str,
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
template_slug: str,
|
||||||
|
underlying_units: float,
|
||||||
|
loan_amount: float,
|
||||||
|
margin_call_ltv: float,
|
||||||
|
) -> BacktestPageRunResult:
|
||||||
|
normalized_symbol = symbol.strip().upper()
|
||||||
|
entry_spot = self.validate_preview_inputs(
|
||||||
|
symbol=normalized_symbol,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
template_slug=template_slug,
|
||||||
|
underlying_units=underlying_units,
|
||||||
|
loan_amount=loan_amount,
|
||||||
|
margin_call_ltv=margin_call_ltv,
|
||||||
|
)
|
||||||
template = self.template_service.get_template(template_slug)
|
template = self.template_service.get_template(template_slug)
|
||||||
entry_spot = self.derive_entry_spot(normalized_symbol, start_date, end_date)
|
|
||||||
_validate_initial_collateral(underlying_units, entry_spot, loan_amount)
|
|
||||||
initial_portfolio = materialize_backtest_portfolio_state(
|
initial_portfolio = materialize_backtest_portfolio_state(
|
||||||
symbol=normalized_symbol,
|
symbol=normalized_symbol,
|
||||||
underlying_units=underlying_units,
|
underlying_units=underlying_units,
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ class EventComparisonPageService:
|
|||||||
loan_amount: float,
|
loan_amount: float,
|
||||||
margin_call_ltv: float,
|
margin_call_ltv: float,
|
||||||
) -> BacktestScenario:
|
) -> BacktestScenario:
|
||||||
|
if not template_slugs:
|
||||||
|
raise ValueError("Select at least one strategy template.")
|
||||||
try:
|
try:
|
||||||
scenario = self.comparison_service.preview_scenario_from_inputs(
|
scenario = self.comparison_service.preview_scenario_from_inputs(
|
||||||
preset_slug=preset_slug,
|
preset_slug=preset_slug,
|
||||||
@@ -147,6 +149,8 @@ class EventComparisonPageService:
|
|||||||
) -> EventComparisonReport:
|
) -> EventComparisonReport:
|
||||||
if not preset_slug:
|
if not preset_slug:
|
||||||
raise ValueError("Preset selection is required")
|
raise ValueError("Preset selection is required")
|
||||||
|
if not template_slugs:
|
||||||
|
raise ValueError("Select at least one strategy template.")
|
||||||
if underlying_units <= 0:
|
if underlying_units <= 0:
|
||||||
raise ValueError("Underlying units must be positive")
|
raise ValueError("Underlying units must be positive")
|
||||||
if loan_amount < 0:
|
if loan_amount < 0:
|
||||||
@@ -162,7 +166,7 @@ class EventComparisonPageService:
|
|||||||
try:
|
try:
|
||||||
preview = self.comparison_service.preview_scenario_from_inputs(
|
preview = self.comparison_service.preview_scenario_from_inputs(
|
||||||
preset_slug=preset.slug,
|
preset_slug=preset.slug,
|
||||||
template_slugs=tuple(template_slugs or preset.scenario_overrides.default_template_slugs),
|
template_slugs=template_slugs,
|
||||||
underlying_units=underlying_units,
|
underlying_units=underlying_units,
|
||||||
loan_amount=loan_amount,
|
loan_amount=loan_amount,
|
||||||
margin_call_ltv=margin_call_ltv,
|
margin_call_ltv=margin_call_ltv,
|
||||||
@@ -180,7 +184,7 @@ class EventComparisonPageService:
|
|||||||
_validate_initial_collateral(underlying_units, preview.initial_portfolio.entry_spot, loan_amount)
|
_validate_initial_collateral(underlying_units, preview.initial_portfolio.entry_spot, loan_amount)
|
||||||
return self.comparison_service.compare_event_from_inputs(
|
return self.comparison_service.compare_event_from_inputs(
|
||||||
preset_slug=preset.slug,
|
preset_slug=preset.slug,
|
||||||
template_slugs=tuple(template_slugs or preset.scenario_overrides.default_template_slugs),
|
template_slugs=template_slugs,
|
||||||
underlying_units=underlying_units,
|
underlying_units=underlying_units,
|
||||||
loan_amount=loan_amount,
|
loan_amount=loan_amount,
|
||||||
margin_call_ltv=margin_call_ltv,
|
margin_call_ltv=margin_call_ltv,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ notes:
|
|||||||
- The roadmap source of truth is this index plus the per-task YAML files in the status folders.
|
- The roadmap source of truth is this index plus the per-task YAML files in the status folders.
|
||||||
- One task lives in one YAML file and changes state by moving between status folders.
|
- One task lives in one YAML file and changes state by moving between status folders.
|
||||||
- Priority ordering is maintained here so agents can parse one short file first.
|
- Priority ordering is maintained here so agents can parse one short file first.
|
||||||
|
- Pre-alpha policy: we may cut or replace old features without backward compatibility until alpha is declared.
|
||||||
|
- Alpha migration policy: once alpha is declared, compatibility only needs to move forward; backward migrations are not required.
|
||||||
priority_queue:
|
priority_queue:
|
||||||
- CORE-001D
|
- CORE-001D
|
||||||
- BT-003B
|
- BT-003B
|
||||||
|
|||||||
@@ -32,23 +32,26 @@ def test_event_comparison_page_service_runs_seeded_gld_preset_deterministically(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_event_comparison_page_service_uses_preset_default_templates_when_none_selected() -> None:
|
def test_event_comparison_page_service_rejects_empty_template_selection() -> None:
|
||||||
service = EventComparisonPageService()
|
service = EventComparisonPageService()
|
||||||
|
|
||||||
report = service.run_read_only_comparison(
|
with pytest.raises(ValueError, match="Select at least one strategy template"):
|
||||||
preset_slug="gld-jan-2024-selloff",
|
service.run_read_only_comparison(
|
||||||
template_slugs=(),
|
preset_slug="gld-jan-2024-selloff",
|
||||||
underlying_units=1000.0,
|
template_slugs=(),
|
||||||
loan_amount=68000.0,
|
underlying_units=1000.0,
|
||||||
margin_call_ltv=0.75,
|
loan_amount=68000.0,
|
||||||
)
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
assert [item.template_slug for item in report.rankings] == [
|
with pytest.raises(ValueError, match="Select at least one strategy template"):
|
||||||
"protective-put-atm-12m",
|
service.preview_scenario(
|
||||||
"ladder-50-50-atm-95pct-12m",
|
preset_slug="gld-jan-2024-selloff",
|
||||||
"protective-put-95pct-12m",
|
template_slugs=(),
|
||||||
"protective-put-90pct-12m",
|
underlying_units=1000.0,
|
||||||
]
|
loan_amount=68000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_event_comparison_page_service_exposes_seeded_preset_options() -> None:
|
def test_event_comparison_page_service_exposes_seeded_preset_options() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user