feat(BT-003B): add event comparison drilldown
This commit is contained in:
@@ -309,6 +309,8 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
)
|
)
|
||||||
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
|
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
|
||||||
chart_model = service.chart_model(report)
|
chart_model = service.chart_model(report)
|
||||||
|
drilldown_options = service.drilldown_options(report)
|
||||||
|
initial_drilldown_slug = next(iter(drilldown_options), None)
|
||||||
|
|
||||||
with result_panel:
|
with result_panel:
|
||||||
with ui.card().classes(
|
with ui.card().classes(
|
||||||
@@ -388,6 +390,132 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
row_key="rank",
|
row_key="rank",
|
||||||
).classes("w-full")
|
).classes("w-full")
|
||||||
|
|
||||||
|
with ui.card().classes(
|
||||||
|
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
):
|
||||||
|
ui.label("Strategy Drilldown").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||||
|
ui.label(
|
||||||
|
"Select a ranked strategy to inspect margin-call pressure, payoff realization, and the full seeded daily path."
|
||||||
|
).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||||
|
drilldown_select = ui.select(
|
||||||
|
drilldown_options,
|
||||||
|
value=initial_drilldown_slug,
|
||||||
|
label="Strategy drilldown",
|
||||||
|
).classes("w-full")
|
||||||
|
drilldown_container = ui.column().classes("w-full gap-4")
|
||||||
|
|
||||||
|
def render_drilldown() -> None:
|
||||||
|
drilldown_container.clear()
|
||||||
|
if drilldown_select.value is None:
|
||||||
|
return
|
||||||
|
drilldown = service.drilldown_model(report, template_slug=str(drilldown_select.value))
|
||||||
|
breach_dates = ", ".join(drilldown.breach_dates) if drilldown.breach_dates else "None"
|
||||||
|
worst_ltv_point = (
|
||||||
|
f"{drilldown.worst_ltv_date} · {drilldown.worst_ltv_hedged:.1%}"
|
||||||
|
if drilldown.worst_ltv_date is not None
|
||||||
|
else "Unavailable"
|
||||||
|
)
|
||||||
|
with drilldown_container:
|
||||||
|
ui.label(f"Selected strategy: {drilldown.template_name}").classes(
|
||||||
|
"text-lg font-semibold text-slate-900 dark:text-slate-100"
|
||||||
|
)
|
||||||
|
ui.label(
|
||||||
|
f"Rank #{drilldown.rank} · {'Survived margin call' if drilldown.survived_margin_call else 'Breached margin threshold'}"
|
||||||
|
).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||||
|
with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
|
||||||
|
cards = [
|
||||||
|
("Margin-call days", str(drilldown.margin_call_days_hedged)),
|
||||||
|
("Payoff realized", f"${drilldown.total_option_payoff_realized:,.0f}"),
|
||||||
|
("Hedge cost", f"${drilldown.hedge_cost:,.0f}"),
|
||||||
|
("Final equity", f"${drilldown.final_equity:,.0f}"),
|
||||||
|
]
|
||||||
|
for label, value in cards:
|
||||||
|
with ui.card().classes(
|
||||||
|
"rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
|
||||||
|
):
|
||||||
|
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")
|
||||||
|
with ui.grid(columns=2).classes("w-full gap-4 max-md:grid-cols-1"):
|
||||||
|
with ui.card().classes(
|
||||||
|
"rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
|
||||||
|
):
|
||||||
|
ui.label("Worst LTV point").classes("text-sm text-slate-500 dark:text-slate-400")
|
||||||
|
ui.label(worst_ltv_point).classes(
|
||||||
|
"text-xl font-bold text-slate-900 dark:text-slate-100"
|
||||||
|
)
|
||||||
|
with ui.card().classes(
|
||||||
|
"rounded-xl border border-amber-200 bg-amber-50 p-4 shadow-none dark:border-amber-900/60 dark:bg-amber-950/30"
|
||||||
|
):
|
||||||
|
ui.label("Margin threshold breach dates").classes(
|
||||||
|
"text-sm text-amber-700 dark:text-amber-300"
|
||||||
|
)
|
||||||
|
ui.label(breach_dates).classes(
|
||||||
|
"text-base font-semibold text-amber-800 dark:text-amber-200"
|
||||||
|
)
|
||||||
|
with ui.card().classes(
|
||||||
|
"w-full rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
|
||||||
|
):
|
||||||
|
ui.label("Daily path details").classes(
|
||||||
|
"text-base font-semibold text-slate-900 dark:text-slate-100"
|
||||||
|
)
|
||||||
|
ui.table(
|
||||||
|
columns=[
|
||||||
|
{"name": "date", "label": "Date", "field": "date", "align": "left"},
|
||||||
|
{
|
||||||
|
"name": "spot_close",
|
||||||
|
"label": "Spot",
|
||||||
|
"field": "spot_close",
|
||||||
|
"align": "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "net_portfolio_value",
|
||||||
|
"label": "Net hedged",
|
||||||
|
"field": "net_portfolio_value",
|
||||||
|
"align": "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "option_market_value",
|
||||||
|
"label": "Option value",
|
||||||
|
"field": "option_market_value",
|
||||||
|
"align": "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "realized_option_cashflow",
|
||||||
|
"label": "Payoff realized",
|
||||||
|
"field": "realized_option_cashflow",
|
||||||
|
"align": "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ltv_hedged",
|
||||||
|
"label": "Hedged LTV",
|
||||||
|
"field": "ltv_hedged",
|
||||||
|
"align": "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "margin_call_hedged",
|
||||||
|
"label": "Breach",
|
||||||
|
"field": "margin_call_hedged",
|
||||||
|
"align": "center",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows=[
|
||||||
|
{
|
||||||
|
"date": row.date,
|
||||||
|
"spot_close": f"${row.spot_close:,.2f}",
|
||||||
|
"net_portfolio_value": f"${row.net_portfolio_value:,.0f}",
|
||||||
|
"option_market_value": f"${row.option_market_value:,.0f}",
|
||||||
|
"realized_option_cashflow": f"${row.realized_option_cashflow:,.0f}",
|
||||||
|
"ltv_hedged": f"{row.ltv_hedged:.1%}",
|
||||||
|
"margin_call_hedged": "Yes" if row.margin_call_hedged else "No",
|
||||||
|
}
|
||||||
|
for row in drilldown.rows
|
||||||
|
],
|
||||||
|
row_key="date",
|
||||||
|
).classes("w-full")
|
||||||
|
|
||||||
|
drilldown_select.on_value_change(lambda _: render_drilldown())
|
||||||
|
render_drilldown()
|
||||||
|
|
||||||
with ui.card().classes(
|
with ui.card().classes(
|
||||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from app.models.backtest import BacktestScenario, EventComparisonReport
|
from app.models.backtest import BacktestScenario, EventComparisonRanking, EventComparisonReport
|
||||||
from app.services.backtesting.comparison import EventComparisonService
|
from app.services.backtesting.comparison import EventComparisonService
|
||||||
from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider
|
from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider
|
||||||
from app.services.backtesting.input_normalization import normalize_historical_scenario_inputs
|
from app.services.backtesting.input_normalization import normalize_historical_scenario_inputs
|
||||||
@@ -63,6 +63,35 @@ class EventComparisonChartModel:
|
|||||||
series: tuple[EventComparisonChartSeries, ...]
|
series: tuple[EventComparisonChartSeries, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EventComparisonDrilldownRow:
|
||||||
|
date: str
|
||||||
|
spot_close: float
|
||||||
|
net_portfolio_value: float
|
||||||
|
option_market_value: float
|
||||||
|
realized_option_cashflow: float
|
||||||
|
ltv_unhedged: float
|
||||||
|
ltv_hedged: float
|
||||||
|
margin_call_hedged: bool
|
||||||
|
active_position_ids: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EventComparisonDrilldownModel:
|
||||||
|
rank: int
|
||||||
|
template_slug: str
|
||||||
|
template_name: str
|
||||||
|
survived_margin_call: bool
|
||||||
|
margin_call_days_hedged: int
|
||||||
|
total_option_payoff_realized: float
|
||||||
|
hedge_cost: float
|
||||||
|
final_equity: float
|
||||||
|
worst_ltv_hedged: float
|
||||||
|
worst_ltv_date: str | None
|
||||||
|
breach_dates: tuple[str, ...]
|
||||||
|
rows: tuple[EventComparisonDrilldownRow, ...]
|
||||||
|
|
||||||
|
|
||||||
class EventComparisonPageService:
|
class EventComparisonPageService:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -181,9 +210,9 @@ class EventComparisonPageService:
|
|||||||
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=template_slugs,
|
template_slugs=template_slugs,
|
||||||
underlying_units=underlying_units,
|
underlying_units=normalized_inputs.underlying_units,
|
||||||
loan_amount=loan_amount,
|
loan_amount=normalized_inputs.loan_amount,
|
||||||
margin_call_ltv=margin_call_ltv,
|
margin_call_ltv=normalized_inputs.margin_call_ltv,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
if str(exc) == "loan_amount must be less than initial collateral value":
|
if str(exc) == "loan_amount must be less than initial collateral value":
|
||||||
@@ -232,3 +261,60 @@ class EventComparisonPageService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return EventComparisonChartModel(dates=dates, series=tuple(series))
|
return EventComparisonChartModel(dates=dates, series=tuple(series))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def drilldown_model(
|
||||||
|
report: EventComparisonReport,
|
||||||
|
*,
|
||||||
|
template_slug: str | None = None,
|
||||||
|
) -> EventComparisonDrilldownModel:
|
||||||
|
ranking = EventComparisonPageService._select_ranking(report, template_slug=template_slug)
|
||||||
|
daily_path = ranking.result.daily_path
|
||||||
|
worst_ltv_point = max(daily_path, key=lambda point: point.ltv_hedged, default=None)
|
||||||
|
breach_dates = tuple(point.date.isoformat() for point in daily_path if point.margin_call_hedged)
|
||||||
|
return EventComparisonDrilldownModel(
|
||||||
|
rank=ranking.rank,
|
||||||
|
template_slug=ranking.template_slug,
|
||||||
|
template_name=ranking.template_name,
|
||||||
|
survived_margin_call=ranking.survived_margin_call,
|
||||||
|
margin_call_days_hedged=ranking.margin_call_days_hedged,
|
||||||
|
total_option_payoff_realized=ranking.result.summary_metrics.total_option_payoff_realized,
|
||||||
|
hedge_cost=ranking.hedge_cost,
|
||||||
|
final_equity=ranking.final_equity,
|
||||||
|
worst_ltv_hedged=ranking.max_ltv_hedged,
|
||||||
|
worst_ltv_date=worst_ltv_point.date.isoformat() if worst_ltv_point is not None else None,
|
||||||
|
breach_dates=breach_dates,
|
||||||
|
rows=tuple(
|
||||||
|
EventComparisonDrilldownRow(
|
||||||
|
date=point.date.isoformat(),
|
||||||
|
spot_close=point.spot_close,
|
||||||
|
net_portfolio_value=point.net_portfolio_value,
|
||||||
|
option_market_value=point.option_market_value,
|
||||||
|
realized_option_cashflow=point.realized_option_cashflow,
|
||||||
|
ltv_unhedged=point.ltv_unhedged,
|
||||||
|
ltv_hedged=point.ltv_hedged,
|
||||||
|
margin_call_hedged=point.margin_call_hedged,
|
||||||
|
active_position_ids=point.active_position_ids,
|
||||||
|
)
|
||||||
|
for point in daily_path
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def drilldown_options(report: EventComparisonReport) -> dict[str, str]:
|
||||||
|
return {ranking.template_slug: f"#{ranking.rank} — {ranking.template_name}" for ranking in report.rankings}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _select_ranking(
|
||||||
|
report: EventComparisonReport,
|
||||||
|
*,
|
||||||
|
template_slug: str | None = None,
|
||||||
|
) -> EventComparisonRanking:
|
||||||
|
if not report.rankings:
|
||||||
|
raise ValueError("Event comparison report has no ranked results")
|
||||||
|
if template_slug is None:
|
||||||
|
return report.rankings[0]
|
||||||
|
for ranking in report.rankings:
|
||||||
|
if ranking.template_slug == template_slug:
|
||||||
|
return ranking
|
||||||
|
raise ValueError(f"Unknown ranked template: {template_slug}")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ notes:
|
|||||||
- Pre-alpha policy: we may cut or replace old features without backward compatibility until alpha is declared.
|
- 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.
|
- Alpha migration policy: once alpha is declared, compatibility only needs to move forward; backward migrations are not required.
|
||||||
priority_queue:
|
priority_queue:
|
||||||
- BT-003B
|
- DEV-DOCKER-001
|
||||||
- PORT-003
|
- PORT-003
|
||||||
- BT-002
|
- BT-002
|
||||||
- BT-001C
|
- BT-001C
|
||||||
@@ -41,14 +41,15 @@ states:
|
|||||||
- DATA-002A
|
- DATA-002A
|
||||||
- DATA-001A
|
- DATA-001A
|
||||||
- OPS-001
|
- OPS-001
|
||||||
|
- DEV-DOCKER-001
|
||||||
- PORT-003
|
- PORT-003
|
||||||
- EXEC-001
|
- EXEC-001
|
||||||
- EXEC-002
|
- EXEC-002
|
||||||
- BT-002
|
- BT-002
|
||||||
- BT-003
|
- BT-003
|
||||||
- BT-003B
|
|
||||||
- BT-001C
|
- BT-001C
|
||||||
in_progress: []
|
in_progress:
|
||||||
|
- BT-003B
|
||||||
done:
|
done:
|
||||||
- DATA-001
|
- DATA-001
|
||||||
- DATA-002
|
- DATA-002
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
id: BT-003B
|
|
||||||
title: Event Comparison Drilldown
|
|
||||||
status: backlog
|
|
||||||
priority: P1
|
|
||||||
effort: M
|
|
||||||
depends_on:
|
|
||||||
- BT-003A
|
|
||||||
tags: [backtesting, ui]
|
|
||||||
summary: Explain why one ranked strategy beat another on the event comparison page.
|
|
||||||
acceptance_criteria:
|
|
||||||
- Selecting a ranked strategy shows daily path details.
|
|
||||||
- UI exposes margin-call days, payoff realized, hedge cost, and final equity.
|
|
||||||
- Worst LTV point and breach dates are highlighted.
|
|
||||||
- Browser test verifies drilldown content updates when selecting a ranked result.
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
id: DEV-DOCKER-001
|
||||||
|
title: Local Docker Bind Mount Integrity
|
||||||
|
status: backlog
|
||||||
|
priority: P0
|
||||||
|
effort: S
|
||||||
|
depends_on: []
|
||||||
|
tags:
|
||||||
|
- devops
|
||||||
|
- docker
|
||||||
|
- local-dev
|
||||||
|
summary: Restore trustworthy local Docker validation by fixing the current empty bind-mount/import failure for `./app -> /app/app` under the local OrbStack workflow.
|
||||||
|
acceptance_criteria:
|
||||||
|
- `docker compose up -d --build` starts the local stack cleanly.
|
||||||
|
- `docker compose ps` shows the app container healthy instead of restart-looping.
|
||||||
|
- `docker compose run --rm --entrypoint python app -c 'import app.main'` succeeds.
|
||||||
|
- Inside the app container, `/app/app` contains the repository's actual application files.
|
||||||
|
- `/health` and at least one changed route can be validated against the Docker-served app, not only a direct local uvicorn process.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
id: BT-003B
|
||||||
|
title: Event Comparison Drilldown
|
||||||
|
status: in_progress
|
||||||
|
priority: P1
|
||||||
|
effort: M
|
||||||
|
depends_on:
|
||||||
|
- BT-003A
|
||||||
|
tags:
|
||||||
|
- backtesting
|
||||||
|
- ui
|
||||||
|
summary: Explain why one ranked strategy beat another on the event comparison page.
|
||||||
|
acceptance_criteria:
|
||||||
|
- Selecting a ranked strategy shows daily path details.
|
||||||
|
- UI exposes margin-call days, payoff realized, hedge cost, and final equity.
|
||||||
|
- Worst LTV point and breach dates are highlighted.
|
||||||
|
- Browser test verifies drilldown content updates when selecting a ranked result.
|
||||||
|
progress_notes:
|
||||||
|
- The drilldown UI and service models are implemented in `app/pages/event_comparison.py` and `app/services/event_comparison_ui.py`.
|
||||||
|
- Focused unit coverage is green and direct local-browser validation against a fresh local uvicorn process succeeded.
|
||||||
|
- Closure is waiting on the local Docker validation path because `docker compose up -d --build` currently surfaces an environment-specific empty bind-mount/import failure for `./app -> /app/app`.
|
||||||
@@ -196,12 +196,24 @@ def test_homepage_and_options_page_render() -> None:
|
|||||||
page.get_by_role("button", name="Run comparison").click()
|
page.get_by_role("button", name="Run comparison").click()
|
||||||
expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000)
|
||||||
expect(page.locator("text=Scenario Results").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Scenario Results").first).to_be_visible(timeout=15000)
|
||||||
|
expect(page.locator("text=Strategy Drilldown").first).to_be_visible(timeout=15000)
|
||||||
expect(page.locator("text=Portfolio Value Paths").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Portfolio Value Paths").first).to_be_visible(timeout=15000)
|
||||||
|
expect(page.locator("text=Selected strategy: Protective Put ATM").first).to_be_visible(timeout=15000)
|
||||||
rerun_event_text = page.locator("body").inner_text(timeout=15000)
|
rerun_event_text = page.locator("body").inner_text(timeout=15000)
|
||||||
assert "Baseline series shows the unhedged collateral value path" in rerun_event_text
|
assert "Baseline series shows the unhedged collateral value path" in rerun_event_text
|
||||||
assert "Templates compared" in rerun_event_text and "4" in rerun_event_text
|
assert "Templates compared" in rerun_event_text and "4" in rerun_event_text
|
||||||
|
assert "Worst LTV point" in rerun_event_text
|
||||||
|
assert "Margin threshold breach dates" in rerun_event_text
|
||||||
|
assert "Daily path details" in rerun_event_text
|
||||||
assert "Historical scenario starts undercollateralized:" not in rerun_event_text
|
assert "Historical scenario starts undercollateralized:" not in rerun_event_text
|
||||||
|
|
||||||
|
page.get_by_label("Strategy drilldown").click()
|
||||||
|
page.get_by_text("#4 — Protective Put 90%", exact=True).click()
|
||||||
|
expect(page.locator("text=Selected strategy: Protective Put 90%").first).to_be_visible(timeout=15000)
|
||||||
|
expect(page.locator("text=Rank #4 · Breached margin threshold").first).to_be_visible(timeout=15000)
|
||||||
|
expect(page.locator("text=2024-01-08 · 82.6%").first).to_be_visible(timeout=15000)
|
||||||
|
expect(page.locator("text=$17,939").first).to_be_visible(timeout=15000)
|
||||||
|
|
||||||
page.get_by_label("Event preset").click()
|
page.get_by_label("Event preset").click()
|
||||||
page.get_by_text("GLD January 2024 Drawdown", exact=True).click()
|
page.get_by_text("GLD January 2024 Drawdown", exact=True).click()
|
||||||
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
||||||
|
|||||||
@@ -199,3 +199,61 @@ def test_event_comparison_page_service_builds_chart_model_with_unhedged_referenc
|
|||||||
assert chart_model.series[0].name == "Unhedged collateral baseline"
|
assert chart_model.series[0].name == "Unhedged collateral baseline"
|
||||||
assert chart_model.series[1].name == "Protective Put ATM"
|
assert chart_model.series[1].name == "Protective Put ATM"
|
||||||
assert len(chart_model.series[0].values) == len(chart_model.dates)
|
assert len(chart_model.series[0].values) == len(chart_model.dates)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_comparison_page_service_builds_drilldown_for_selected_ranking() -> None:
|
||||||
|
service = EventComparisonPageService()
|
||||||
|
|
||||||
|
report = service.run_read_only_comparison(
|
||||||
|
preset_slug="gld-jan-2024-selloff",
|
||||||
|
template_slugs=("protective-put-atm-12m", "protective-put-95pct-12m"),
|
||||||
|
underlying_units=1000.0,
|
||||||
|
loan_amount=68000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
drilldown = service.drilldown_model(report, template_slug="protective-put-95pct-12m")
|
||||||
|
|
||||||
|
assert drilldown.rank == 2
|
||||||
|
assert drilldown.template_name == "Protective Put 95%"
|
||||||
|
assert drilldown.margin_call_days_hedged == report.rankings[1].margin_call_days_hedged
|
||||||
|
assert drilldown.hedge_cost == report.rankings[1].hedge_cost
|
||||||
|
assert drilldown.final_equity == report.rankings[1].final_equity
|
||||||
|
assert (
|
||||||
|
drilldown.total_option_payoff_realized == report.rankings[1].result.summary_metrics.total_option_payoff_realized
|
||||||
|
)
|
||||||
|
assert drilldown.worst_ltv_date == "2024-01-08"
|
||||||
|
assert drilldown.rows[0].date == "2024-01-02"
|
||||||
|
assert drilldown.rows[-1].date == "2024-01-08"
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_comparison_page_service_defaults_drilldown_to_top_ranked_strategy() -> None:
|
||||||
|
service = EventComparisonPageService()
|
||||||
|
|
||||||
|
report = service.run_read_only_comparison(
|
||||||
|
preset_slug="gld-jan-2024-selloff",
|
||||||
|
template_slugs=("protective-put-atm-12m", "protective-put-95pct-12m"),
|
||||||
|
underlying_units=1000.0,
|
||||||
|
loan_amount=68000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
drilldown = service.drilldown_model(report)
|
||||||
|
|
||||||
|
assert drilldown.rank == 1
|
||||||
|
assert drilldown.template_slug == report.rankings[0].template_slug
|
||||||
|
assert drilldown.template_name == report.rankings[0].template_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_comparison_page_service_rejects_unknown_drilldown_template_slug() -> None:
|
||||||
|
service = EventComparisonPageService()
|
||||||
|
|
||||||
|
report = service.run_read_only_comparison(
|
||||||
|
preset_slug="gld-jan-2024-selloff",
|
||||||
|
template_slugs=("protective-put-atm-12m",),
|
||||||
|
underlying_units=1000.0,
|
||||||
|
loan_amount=68000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Unknown ranked template"):
|
||||||
|
service.drilldown_model(report, template_slug="missing-template")
|
||||||
|
|||||||
Reference in New Issue
Block a user