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:
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
22
docs/roadmap/backlog/BT-005-defer-entry-spot-derivation.yaml
Normal file
22
docs/roadmap/backlog/BT-005-defer-entry-spot-derivation.yaml
Normal 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.
|
||||||
Reference in New Issue
Block a user