feat: add option contracts to overview, fix default dates, add roadmap items

- Move option contracts from daily results table to overview cards (constant throughout backtest)
- Fix default dates to March 2026 (2026-03-02 to 2026-03-25)
- Add BT-004 backlog item: candlestick chart with portfolio value line on secondary axis
- Add BT-005 backlog item: defer entry spot derivation to backtest run (not on every date change)
This commit is contained in:
Bu5hm4nn
2026-04-05 09:24:25 +02:00
parent 6b8336ab7e
commit 4af7a09c6e
4 changed files with 59 additions and 18 deletions

View File

@@ -255,9 +255,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
default_data_source = "databento" default_data_source = "databento"
default_dataset = "XNAS.BASIC" default_dataset = "XNAS.BASIC"
default_schema = "ohlcv-1d" default_schema = "ohlcv-1d"
# Use a start date that's valid for the default dataset (XNAS.BASIC starts 2024-07-01) # Default to March 2026 for testing
default_start_date = date(2024, 7, 1).isoformat() default_start_date = date(2026, 3, 2).isoformat()
default_end_date = date(2024, 12, 31).isoformat() default_end_date = date(2026, 3, 25).isoformat()
default_symbol = "GLD" default_symbol = "GLD"
default_start_price = 0.0 default_start_price = 0.0
@@ -621,12 +621,17 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
ui.label(f"Template: {template_result.template_name}").classes( ui.label(f"Template: {template_result.template_name}").classes(
"text-sm text-slate-500 dark:text-slate-400" "text-sm text-slate-500 dark:text-slate-400"
) )
# Get option contracts from first day (constant throughout backtest)
option_contracts = (
template_result.daily_path[0].option_contracts if template_result.daily_path else 0.0
)
with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"): with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
cards = [ cards = [
("Start value", f"${summary.start_value:,.0f}"), ("Start value", f"${summary.start_value:,.0f}"),
("End value hedged", f"${summary.end_value_hedged_net:,.0f}"), ("End value hedged", f"${summary.end_value_hedged_net:,.0f}"),
("Max LTV hedged", f"{summary.max_ltv_hedged:.1%}"), ("Max LTV hedged", f"{summary.max_ltv_hedged:.1%}"),
("Hedge cost", f"${summary.total_hedge_cost:,.0f}"), ("Hedge cost", f"${summary.total_hedge_cost:,.0f}"),
("Option contracts", f"{option_contracts:,.0f}"),
("Margin call days hedged", str(summary.margin_call_days_hedged)), ("Margin call days hedged", str(summary.margin_call_days_hedged)),
("Margin call days unhedged", str(summary.margin_call_days_unhedged)), ("Margin call days unhedged", str(summary.margin_call_days_unhedged)),
( (
@@ -671,12 +676,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
"field": "option_value", "field": "option_value",
"align": "right", "align": "right",
}, },
{
"name": "contracts",
"label": "Contracts",
"field": "contracts",
"align": "right",
},
{ {
"name": "ltv_hedged", "name": "ltv_hedged",
"label": "LTV hedged", "label": "LTV hedged",
@@ -698,7 +697,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
"close": f"${point.spot_close:,.2f}", "close": f"${point.spot_close:,.2f}",
"portfolio_value": f"${point.net_portfolio_value:,.0f}", "portfolio_value": f"${point.net_portfolio_value:,.0f}",
"option_value": f"${point.option_market_value:,.0f}", "option_value": f"${point.option_market_value:,.0f}",
"contracts": f"{point.option_contracts:,.0f}",
"ltv_hedged": f"{point.ltv_hedged:.1%}", "ltv_hedged": f"{point.ltv_hedged:.1%}",
"margin_call": "Yes" if point.margin_call_hedged else "No", "margin_call": "Yes" if point.margin_call_hedged else "No",
} }
@@ -982,10 +980,16 @@ 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(label).classes("text-sm text-slate-500 dark:text-slate-400")
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100") ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
# Get option contracts from first day (constant throughout backtest)
daily_path = first_template.get("daily_path", [])
first_day = daily_path[0] if daily_path else {}
option_contracts = first_day.get("option_contracts", 0)
# Additional metrics row # Additional metrics row
with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"): with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
extra_data = [ extra_data = [
("Max LTV hedged", f"{max_ltv_hedged:.1%}"), ("Max LTV hedged", f"{max_ltv_hedged:.1%}"),
("Option contracts", f"{option_contracts:,.0f}"),
("Margin call days hedged", str(margin_days_hedged)), ("Margin call days hedged", str(margin_days_hedged)),
("Margin call days unhedged", str(margin_days_unhedged)), ("Margin call days unhedged", str(margin_days_unhedged)),
("Breached threshold", "No" if margin_days_hedged == 0 else "Yes"), ("Breached threshold", "No" if margin_days_hedged == 0 else "Yes"),
@@ -998,7 +1002,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100") ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
# Daily path table # Daily path table
daily_path = first_template.get("daily_path", [])
if daily_path: if daily_path:
with ui.card().classes( with ui.card().classes(
"w-full mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950" "w-full mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
@@ -1024,12 +1027,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
"field": "option_value", "field": "option_value",
"align": "right", "align": "right",
}, },
{
"name": "contracts",
"label": "Contracts",
"field": "contracts",
"align": "right",
},
{ {
"name": "ltv_hedged", "name": "ltv_hedged",
"label": "LTV hedged", "label": "LTV hedged",
@@ -1051,7 +1048,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
"close": f"${dp.get('spot_close', 0):,.2f}", "close": f"${dp.get('spot_close', 0):,.2f}",
"portfolio_value": f"${dp.get('net_portfolio_value', 0):,.0f}", "portfolio_value": f"${dp.get('net_portfolio_value', 0):,.0f}",
"option_value": f"${dp.get('option_market_value', 0):,.0f}", "option_value": f"${dp.get('option_market_value', 0):,.0f}",
"contracts": f"{dp.get('option_contracts', 0):,.0f}",
"ltv_hedged": f"{dp.get('ltv_hedged', 0):.1%}", "ltv_hedged": f"{dp.get('ltv_hedged', 0):.1%}",
"margin_call": "Yes" if dp.get("margin_call_hedged") else "No", "margin_call": "Yes" if dp.get("margin_call_hedged") else "No",
} }

View File

@@ -13,6 +13,8 @@ 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-005
- BT-004
- EXEC-002 - EXEC-002
- DATA-DB-005 - DATA-DB-005
- DATA-002A - DATA-002A
@@ -48,6 +50,8 @@ recently_completed:
- CORE-002B - CORE-002B
states: states:
backlog: backlog:
- BT-005
- BT-004
- DATA-DB-005 - DATA-DB-005
- DATA-DB-006 - DATA-DB-006
- EXEC-002 - EXEC-002

View File

@@ -0,0 +1,19 @@
id: BT-004
title: Backtest Visualization Chart
status: backlog
priority: P1
effort: M
depends_on:
- BT-001
tags: [backtesting, visualization, charts]
summary: Add interactive candlestick chart showing price OHLC with portfolio value line overlay.
acceptance_criteria:
- Candlestick chart displays OHLC data (open, high, low, close) for each day.
- Portfolio value (underlying + option) shown as line on secondary Y-axis.
- Chart updates when backtest results change.
- Chart is responsive and readable on mobile/tablet viewports.
- Chart library matches existing app styling (dark mode support).
notes: |
Consider using a lightweight charting library that integrates well with NiceGUI.
Portfolio value line should be clearly distinguishable from candlesticks.
May want to add toggle for showing/hiding different series.

View File

@@ -0,0 +1,22 @@
id: BT-005
title: Defer Entry Spot Derivation to Backtest Run
status: backlog
priority: P1
effort: S
depends_on: []
tags: [backtesting, ux, performance]
summary: Only derive entry spot from historical data when user clicks Run, not on every form change.
acceptance_criteria:
- Entry spot is NOT fetched when user changes start/end dates.
- Entry spot is NOT fetched when user changes other form fields.
- Entry spot IS derived when user clicks the Run button.
- Form remains responsive during date/field changes (no API calls blocking).
- Clear loading indicator shows when entry spot is being fetched during run.
- Previous entry spot value is retained until new one is derived.
notes:
Current behavior calls derive_entry_spot on every date change which causes
API errors if user is still configuring other fields.
The refresh_workspace_seeded_units function should not be called on date changes.
Entry spot derivation should happen inside start_backtest or as a separate
explicit "fetch spot" button if user wants to preview.