From 6b8336ab7e0b8b5336a3c1bdaa3c5c30aa12de9a Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Sun, 5 Apr 2026 08:54:38 +0200 Subject: [PATCH] feat: add Portfolio Value, Option Value, and Contracts columns to daily results - Add option_contracts field to BacktestDailyPoint (number of contracts held) - Update engine to calculate total option contracts from positions - Update job serialization to include underlying_value, option_market_value, net_portfolio_value, option_contracts - Update both render_result and render_job_result tables to show: - Low, High, Close (from previous commit) - Portfolio value (net_portfolio_value) - Option value (option_market_value) - Contracts (option_contracts) - LTV hedged - Margin call status --- app/backtesting/engine.py | 4 +++ app/models/backtest.py | 23 ++++++++++++++++- app/pages/backtests.py | 42 ++++++++++++++++++++++++++++++++ app/services/backtesting/jobs.py | 1 + 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/app/backtesting/engine.py b/app/backtesting/engine.py index 53e7140..ed5989b 100644 --- a/app/backtesting/engine.py +++ b/app/backtesting/engine.py @@ -87,6 +87,9 @@ class SyntheticBacktestEngine: ltv_unhedged_worst = scenario.initial_portfolio.loan_amount / underlying_value_worst ltv_hedged_worst = scenario.initial_portfolio.loan_amount / net_portfolio_value_worst + # Total option contracts held + option_contracts = sum(p.quantity for p in open_positions) + daily_points.append( BacktestDailyPoint( date=day.date, @@ -103,6 +106,7 @@ class SyntheticBacktestEngine: ltv_hedged=ltv_hedged, margin_call_unhedged=ltv_unhedged_worst >= scenario.initial_portfolio.margin_call_ltv, margin_call_hedged=ltv_hedged_worst >= scenario.initial_portfolio.margin_call_ltv, + option_contracts=option_contracts, active_position_ids=tuple(active_position_ids), ) ) diff --git a/app/models/backtest.py b/app/models/backtest.py index f02bf38..3bdafe3 100644 --- a/app/models/backtest.py +++ b/app/models/backtest.py @@ -97,10 +97,12 @@ class BacktestDailyPoint: ltv_hedged: float margin_call_unhedged: bool margin_call_hedged: bool + active_position_ids: tuple[str, ...] = field(default_factory=tuple) # Optional OHLC fields for worst-case margin call evaluation spot_low: float | None = None # Day's low for margin call evaluation spot_high: float | None = None # Day's high - active_position_ids: tuple[str, ...] = field(default_factory=tuple) + # Option position info + option_contracts: float = 0.0 # Number of option contracts held @dataclass(frozen=True) @@ -154,3 +156,22 @@ class EventComparisonReport: scenario: BacktestScenario rankings: tuple[EventComparisonRanking, ...] run_result: BacktestRunResult + + +@dataclass(frozen=True) +class BacktestPortfolioPreset: + """User-facing preset for quick scenario configuration.""" + + preset_id: str + name: str + description: str + underlying_symbol: str + start_date: date + end_date: date + entry_spot: float | None = None # If None, derive from historical data + underlying_units: float = 1000.0 + loan_amount: float = 50000.0 + margin_call_ltv: float = 0.80 + template_slug: str = "protective-put-atm-12m" + # Event-specific overrides + scenario_overrides: dict[str, object] | None = None diff --git a/app/pages/backtests.py b/app/pages/backtests.py index 7d5bb38..9278e2d 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -659,6 +659,24 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: {"name": "low", "label": "Low", "field": "low", "align": "right"}, {"name": "high", "label": "High", "field": "high", "align": "right"}, {"name": "close", "label": "Close", "field": "close", "align": "right"}, + { + "name": "portfolio_value", + "label": "Portfolio", + "field": "portfolio_value", + "align": "right", + }, + { + "name": "option_value", + "label": "Option value", + "field": "option_value", + "align": "right", + }, + { + "name": "contracts", + "label": "Contracts", + "field": "contracts", + "align": "right", + }, { "name": "ltv_hedged", "label": "LTV hedged", @@ -678,6 +696,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "low": f"${point.spot_low:,.2f}", "high": f"${point.spot_high:,.2f}", "close": f"${point.spot_close:,.2f}", + "portfolio_value": f"${point.net_portfolio_value:,.0f}", + "option_value": f"${point.option_market_value:,.0f}", + "contracts": f"{point.option_contracts:,.0f}", "ltv_hedged": f"{point.ltv_hedged:.1%}", "margin_call": "Yes" if point.margin_call_hedged else "No", } @@ -991,6 +1012,24 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: {"name": "low", "label": "Low", "field": "low", "align": "right"}, {"name": "high", "label": "High", "field": "high", "align": "right"}, {"name": "close", "label": "Close", "field": "close", "align": "right"}, + { + "name": "portfolio_value", + "label": "Portfolio", + "field": "portfolio_value", + "align": "right", + }, + { + "name": "option_value", + "label": "Option value", + "field": "option_value", + "align": "right", + }, + { + "name": "contracts", + "label": "Contracts", + "field": "contracts", + "align": "right", + }, { "name": "ltv_hedged", "label": "LTV hedged", @@ -1010,6 +1049,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "low": f"${dp.get('spot_low', dp.get('spot_close', 0)):,.2f}", "high": f"${dp.get('spot_high', dp.get('spot_close', 0)):,.2f}", "close": f"${dp.get('spot_close', 0):,.2f}", + "portfolio_value": f"${dp.get('net_portfolio_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%}", "margin_call": "Yes" if dp.get("margin_call_hedged") else "No", } diff --git a/app/services/backtesting/jobs.py b/app/services/backtesting/jobs.py index ba9479b..e2809c7 100644 --- a/app/services/backtesting/jobs.py +++ b/app/services/backtesting/jobs.py @@ -264,6 +264,7 @@ def run_backtest_job( "underlying_value": dp.underlying_value, "option_market_value": dp.option_market_value, "net_portfolio_value": dp.net_portfolio_value, + "option_contracts": dp.option_contracts, "ltv_hedged": dp.ltv_hedged, "ltv_unhedged": dp.ltv_unhedged, "margin_call_hedged": dp.margin_call_hedged,