Files
vault-dash/docs/EXEC-001A_BT-001_MVP_ARCHITECTURE.md
2026-03-27 18:31:28 +01:00

32 KiB
Raw Permalink Blame History

EXEC-001A / BT-001 MVP Architecture

Scope

This document defines the MVP design for four related roadmap items:

  • EXEC-001A — Named Strategy Templates
  • BT-001 — Synthetic Historical Backtesting
  • BT-002 — Historical Daily Options Snapshot Provider
  • BT-003 — Selloff Event Comparison Report

The goal is to give implementation agents a concrete architecture without requiring a database or a full UI rewrite. The MVP should fit the current codebase shape:

  • domain models in app/models/
  • IO and orchestration in app/services/
  • strategy math in app/strategies/ or a new app/backtesting/ package
  • lightweight docs under docs/

Design goals

  1. Keep current live quote/options flows working. Do not overload app/services/data_service.py with historical backtest state.
  2. Make templates reusable and named. A strategy definition should be saved once and referenced by many backtests.
  3. Support synthetic-first backtests. BT-001 must work before BT-002 exists.
  4. Prevent lookahead bias by design. Providers and the run engine must expose only data available at each as_of_date.
  5. Preserve a migration path to real daily options snapshots. Synthetic pricing and snapshot-based pricing must share the same provider contract.
  6. Stay file-backed for MVP persistence. Repositories may use JSON files under data/ first, behind interfaces.

Terminology decision

The current code uses LombardPortfolio.gold_ounces, but the strategy engine effectively treats that field as generic underlying units. For historical backtesting, implementation agents should not extend that ambiguity.

Recommendation

  • Keep LombardPortfolio unchanged for existing live pages.
  • Introduce backtesting-specific portfolio state using the neutral term underlying_units.
  • Treat symbol + underlying_units as the canonical tradable exposure.

This avoids mixing physical ounces, GLD shares, and synthetic units in the backtest engine.


MVP architecture summary

Main decision

Create a new isolated subsystem:

  • app/models/strategy_template.py
  • app/models/backtest.py
  • app/models/event_preset.py
  • app/services/historical/
  • app/services/backtesting/
  • optional thin adapters in app/strategies/ for reusing existing payoff logic

Why isolate it

The current DataService is a live/synthetic read service with cache-oriented payload shaping. Historical backtesting needs:

  • versioned saved definitions
  • run lifecycle state
  • daily path simulation
  • historical provider abstraction
  • reproducible result storage

Those concerns should not be mixed into the current request-time quote service.


Domain model proposals

1. Strategy templates (EXEC-001A)

A strategy template is a named, versioned, reusable hedge definition. It is not a run result and it is not a specific dated option contract.

StrategyTemplate

Recommended fields:

Field Type Notes
template_id str Stable UUID/string key
slug str Human-readable unique name, e.g. protective-put-atm-12m
display_name str UI/report label
description str Short rationale
template_kind enum protective_put, laddered_put, collar (future-safe)
status enum draft, active, archived
version int Increment on material rule changes
underlying_symbol str MVP may allow one symbol per template
contract_mode enum continuous_units for synthetic MVP, listed_contracts for BT-002+
legs list[TemplateLeg] One or more parametric legs
roll_policy RollPolicy How/when to replace expiring hedges
entry_policy EntryPolicy When the initial hedge is entered
tags list[str] e.g. conservative, income-safe
created_at datetime Audit
updated_at datetime Audit

TemplateLeg

Recommended fields:

Field Type Notes
leg_id str Stable within template version
side enum long or short; MVP uses long only for puts
option_type enum put or call
allocation_weight float Must sum to 1.0 across active hedge legs in MVP
strike_rule StrikeRule MVP: spot_pct only
target_expiry_days int e.g. 365, 180, 90
quantity_rule enum MVP: target_coverage_pct
target_coverage_pct float Usually 1.0 for full hedge, but supports partial hedges later

StrikeRule

MVP shape:

Field Type Notes
rule_type enum spot_pct
value float e.g. 1.00, 0.95, 0.90

Future-safe, but not in MVP:

  • delta_target
  • fixed_strike
  • moneyness_bucket

RollPolicy

Recommended MVP fields:

Field Type Notes
policy_type enum hold_to_expiry, roll_n_days_before_expiry
days_before_expiry int Required for rolling mode
rebalance_on_new_deposit bool Default false in MVP

EntryPolicy

Recommended MVP fields:

Field Type Notes
entry_timing enum scenario_start_close
stagger_days int | None Not used in MVP, keep nullable

MVP template invariants

Implementation agents should enforce:

  • slug unique among active templates
  • template versions immutable once referenced by a completed run
  • weights sum to 1.0 for protective_put/laddered_put templates
  • all legs use the same target_expiry_days in MVP unless explicitly marked as a ladder with shared roll policy
  • underlying_symbol on the template must either match the scenario symbol or be */generic if generic templates are later supported

Template examples

  • protective-put-atm-12m
  • protective-put-95pct-12m
  • ladder-50-50-atm-95pct-12m
  • ladder-33-33-33-atm-95pct-90pct-12m

These map cleanly onto the existing strategy set in app/strategies/engine.py.


2. Backtest scenarios

A backtest scenario is the saved experiment definition. It says what portfolio, time window, templates, provider, and execution rules are used.

BacktestScenario

Recommended fields:

Field Type Notes
scenario_id str Stable UUID/string key
slug str Human-readable name
display_name str Report label
description str Optional scenario intent
symbol str Underlying being hedged
start_date date Inclusive
end_date date Inclusive
initial_portfolio BacktestPortfolioState Portfolio at day 0
template_refs list[TemplateRef] One or more template versions to compare
provider_ref ProviderRef Which historical provider to use
execution_model ExecutionModel Daily close-to-close for MVP
valuation_frequency enum daily in MVP
benchmark_mode enum unhedged_only in MVP
event_preset_id str | None Optional link for BT-003
notes list[str] Optional warnings/assumptions
created_at datetime Audit

BacktestPortfolioState

Recommended fields:

Field Type Notes
currency str USD in MVP
underlying_units float Canonical exposure size
entry_spot float Starting spot reference
loan_amount float Outstanding loan
margin_call_ltv float Stress threshold
cash_balance float Usually 0.0 in MVP
financing_rate float Optional, default 0.0 in MVP

TemplateRef

Use a small immutable reference object:

Field Type Notes
template_id str Stable template key
version int Required for reproducibility
display_name_override str | None Optional report label

ProviderRef

Recommended fields:

Field Type Notes
provider_id str e.g. synthetic_v1, daily_snapshots_v1
config_key str Named config/profile used by the run
pricing_mode enum synthetic_bs_mid or snapshot_mid

ExecutionModel

MVP decision:

  • Daily close-to-close engine
  • Positions are evaluated once per trading day
  • If a template rule triggers on date T, entry/roll is executed using provider data as of date T close
  • Mark-to-market for date T uses the same T snapshot

This is a simplification, but it is deterministic and compatible with BT-002 daily snapshots.

Scenario invariants

  • start_date <= end_date
  • at least one template_ref
  • all referenced template versions must exist before run submission
  • initial_portfolio.loan_amount < initial_portfolio.underlying_units * entry_spot
  • scenario must declare the provider explicitly; no hidden global default inside the engine

3. Backtest runs and results

A run is the execution record of one scenario against one or more templates under one provider.

BacktestRun

Recommended fields:

Field Type Notes
run_id str Stable UUID
scenario_id str Source scenario
status enum queued, running, completed, failed, cancelled
provider_snapshot ProviderSnapshot Frozen provider config used at run time
submitted_at datetime Audit
started_at datetime | None Audit
completed_at datetime | None Audit
engine_version str Git SHA or app version
rules_version str Semantic rules hash for reproducibility
warnings list[str] Missing data fallback, skipped dates, etc.
error str | None Failure detail

ProviderSnapshot

Freeze the provider state used by a run:

Field Type Notes
provider_id str Resolved provider implementation
config dict[str, Any] Frozen provider config used for the run
source_version str | None Optional data snapshot/build hash

BacktestRunResult

Top-level recommended fields:

Field Type Notes
run_id str Foreign key
scenario_snapshot BacktestScenario or frozen subset Freeze used inputs
template_results list[TemplateBacktestResult] One per template
comparison_summary RunComparisonSummary Ranked table
generated_at datetime Audit

TemplateBacktestResult

Recommended fields:

Field Type Notes
template_id str Identity
template_version int Reproducibility
template_name str Display
summary_metrics BacktestSummaryMetrics Compact ranking metrics
daily_path list[BacktestDailyPoint] Daily timeseries
position_log list[BacktestPositionRecord] Open/roll/expire events
trade_log list[BacktestTradeRecord] Cashflow events
validation_notes list[str] e.g. synthetic IV fallback used

BacktestSummaryMetrics

Recommended MVP metrics:

Field Type Notes
start_value float Initial collateral value
end_value_unhedged float Baseline terminal collateral
end_value_hedged_net float After hedge P&L and premiums
total_hedge_cost float Sum of paid premiums
total_option_payoff_realized float Expiry/close realized payoff
max_ltv_unhedged float Path max
max_ltv_hedged float Path max
margin_call_days_unhedged int Count
margin_call_days_hedged int Count
worst_drawdown_unhedged float Optional but useful
worst_drawdown_hedged float Optional but useful
days_protected_below_threshold int Optional convenience metric
roll_count int Operational complexity

BacktestDailyPoint

Recommended daily path fields:

Field Type Notes
date date Trading date
spot_close float Underlying close
underlying_value float underlying_units * spot_close
option_market_value float Mark-to-market of open hedge
premium_cashflow float Negative on entry/roll
realized_option_cashflow float Expiry/sale value
net_portfolio_value float Underlying + option MTM + cash
loan_amount float Constant in MVP
ltv_unhedged float Baseline
ltv_hedged float Hedge-aware
margin_call_unhedged bool Baseline
margin_call_hedged bool Hedge-aware
active_position_ids list[str] Traceability

BacktestTradeRecord

Recommended fields:

Field Type Notes
trade_id str Stable key
date date Execution date
action enum buy_open, sell_close, expire, roll
leg_id str Template leg link
instrument_key HistoricalInstrumentKey Strike/expiry/type
quantity float Continuous or discrete
price float Fill price
cashflow float Signed
reason enum initial_entry, scheduled_roll, expiry, scenario_end

Run/result invariants

  • runs are append-only after completion
  • results must freeze template versions and scenario inputs used at execution time
  • failed runs may omit template_results but must preserve warnings/error
  • ranking should never rely on a metric that can be absent without a fallback rule

4. Event presets (BT-003)

An event preset is a named reusable market window used to compare strategy behavior across selloffs.

EventPreset

Recommended fields:

Field Type Notes
event_preset_id str Stable key
slug str e.g. covid-crash-2020
display_name str Report label
symbol str Underlying symbol
window_start date Inclusive
window_end date Inclusive
anchor_date date | None Optional focal date
event_type enum selloff, recovery, stress_test
tags list[str] e.g. macro, liquidity, vol-spike
description str Why this event exists
scenario_overrides EventScenarioOverrides Optional defaults
created_at datetime Audit

EventScenarioOverrides

MVP fields:

Field Type Notes
lookback_days int | None Optional pre-window warmup
recovery_days int | None Optional post-event tail
default_template_slugs list[str] Suggested comparison set
normalize_start_value bool Default true for event comparison charts

BT-003 usage pattern

  • a report selects one or more EventPresets
  • each preset materializes a BacktestScenario
  • the same template set is run across all events
  • report compares normalized daily paths and summary metrics

MVP event decision

Use manual date windows only. Do not attempt automatic peak/trough detection in the first slice.


Historical provider abstraction

Core interface

Create a provider contract that exposes only point-in-time historical data.

HistoricalMarketDataProvider

Recommended methods:

class HistoricalMarketDataProvider(Protocol):
    provider_id: str

    def get_trading_days(self, symbol: str, start_date: date, end_date: date) -> list[date]: ...

    def get_underlying_bars(
        self, symbol: str, start_date: date, end_date: date
    ) -> list[UnderlyingBar]: ...

    def get_option_snapshot(
        self, query: OptionSnapshotQuery
    ) -> OptionSnapshot: ...

    def price_open_position(
        self, position: HistoricalOptionPosition, as_of_date: date
    ) -> HistoricalOptionMark: ...

Why this interface

It cleanly supports both provider types:

  • BT-001 synthetic provider — generate option values from deterministic assumptions
  • BT-002 snapshot provider — read real daily option quotes/surfaces from stored snapshots

It also makes lookahead control explicit: every method is asked for data as of a specific date.

Supporting provider models

UnderlyingBar

Field Type Notes
date date Trading day
open float Optional for future use
high float Optional
low float Optional
close float Required
volume float | None Optional
source str Provider/source tag

OptionSnapshotQuery

Field Type Notes
symbol str Underlying
as_of_date date Point-in-time date
option_type enum put/call
target_expiry_days int Desired tenor
strike_rule StrikeRule Resolved against current spot
pricing_side enum mid in MVP

OptionSnapshot

Field Type Notes
as_of_date date Snapshot date
symbol str Underlying
underlying_close float Spot used for selection/pricing
selected_contract HistoricalOptionQuote Resolved contract
selection_notes list[str] e.g. nearest expiry/nearest strike
source str Provider ID

HistoricalOptionQuote

Field Type Notes
instrument_key HistoricalInstrumentKey Canonical contract identity
bid float Optional for snapshot provider
ask float Optional
mid float Required for MVP valuation
implied_volatility float | None Required for BT-002, synthetic-derived for BT-001
delta float | None Optional now, useful later
open_interest int | None Optional now
volume int | None Optional now
source str Provider/source tag

HistoricalInstrumentKey

Field Type Notes
symbol str Underlying
option_type enum put/call
expiry date Contract expiry
strike float Contract strike

Provider implementations

A. SyntheticHistoricalProvider (BT-001 first)

Purpose:

  • generate deterministic historical backtests without requiring stored historical options chains
  • use historical underlying closes plus a synthetic volatility/rates regime
  • resolve template legs into synthetic option quotes on each rebalance date
  • reprice open positions daily using the same model family

Inputs:

  • underlying close series (from yfinance file cache, CSV fixture, or another deterministic source)
  • configured implied volatility regime, e.g. fixed 0.16 or dated step regime
  • configured risk-free rate regime
  • optional stress spread for transaction cost realism

Entry and valuation:

  • on a rebalance date, compute strike from spot_pct * spot_close
  • set expiry by nearest trading day to as_of_date + target_expiry_days
  • price using Black-Scholes with the current day's spot, configured IV, remaining time, and option type
  • on later dates, reprice the same contract using current spot and remaining time only

MVP synthetic assumptions

  • constant or schedule-based implied volatility; no future realized volatility leakage
  • no stochastic volatility process in first slice
  • no early exercise modeling
  • no assignment modeling
  • mid price only
  • deterministic rounding/selection rules

Why synthetic-first is acceptable

It validates:

  • template persistence
  • run lifecycle
  • path valuation
  • daily result rendering
  • anti-lookahead contract boundaries

before adding BT-002 data ingestion complexity.

B. DailyOptionsSnapshotProvider (BT-002)

Purpose:

  • load historical option quotes for each trading day
  • resolve actual listed contracts closest to template rules
  • mark open positions to historical daily mids thereafter
  • selection on entry day uses nearest eligible expiry and nearest eligible strike from that day's chain only
  • mark-to-market later uses the exact same contract key if a quote exists on later dates
  • if the contract is missing on a later date, provider returns a missing-data result and the engine applies a documented fallback policy

MVP fallback policy for missing marks

Implementation agents should choose one explicit fallback and test it. Recommended order:

  1. exact contract from same-day snapshot
  2. if unavailable, previous available mark from same contract with warning
  3. if unavailable and contract is expired, intrinsic value at expiry or zero afterward
  4. otherwise fail the run or mark the template result incomplete

Do not silently substitute a different strike/expiry for an already-open position.


Backtest engine flow

Create a dedicated engine under app/backtesting/engine.py. Keep orchestration and repository wiring in app/services/backtesting/.

High-level loop

For each template in the scenario:

  1. load trading days from provider
  2. create baseline unhedged path
  3. resolve initial hedge on start_date
  4. for each trading day:
    • read underlying close for day T
    • mark open option positions as of T
    • compute unhedged and hedged portfolio value
    • compute LTV and margin-call flags
    • check roll/expiry rules using only T data
    • if a roll is due, close/expire old position and open replacement using T snapshot
  5. liquidate remaining position at scenario end if still open
  6. calculate summary metrics
  7. rank templates inside comparison_summary

Position model recommendation

Use a separate open-position model rather than reusing OptionContract directly.

Recommended HistoricalOptionPosition fields:

  • position_id
  • instrument_key
  • opened_at
  • expiry
  • quantity
  • entry_price
  • current_mark
  • template_leg_id
  • source_snapshot_date

Reason: backtests need lifecycle state and audit fields that the current OptionContract model does not carry.

Ranking recommendation

For MVP comparison views, rank templates by:

  1. fewer margin_call_days_hedged
  2. lower max_ltv_hedged
  3. lower total_hedge_cost
  4. higher end_value_hedged_net

This is easier to explain than a single opaque score.


Data realism constraints

Implementation agents should treat the following as mandatory MVP rules.

1. Point-in-time only

On day T, the engine may use only:

  • underlying bar for T
  • option snapshot for T
  • provider configuration known before the run starts
  • open positions created earlier or on T

It may not use:

  • future closes
  • future implied vols
  • terminal event windows beyond T for trading decisions
  • any provider helper that precomputes the whole path and leaks future state into contract selection

2. Stable contract identity after entry

Once a contract is opened, daily valuation must use that exact contract identity:

  • same symbol
  • same expiry
  • same strike
  • same option type

No rolling relabeling of a live position to a “nearest” contract.

3. Explicit selection rules

Template rules must resolve to contracts with deterministic tiebreakers:

  • nearest expiry at or beyond target DTE
  • nearest strike to rule target
  • if tied, prefer more conservative strike for puts (higher strike) and earliest expiry

Tiebreakers must be documented and unit-tested.

4. Execution timing must be fixed

MVP should use same-day close execution consistently.

Do not mix:

  • signal at close / fill next open
  • signal at close / fill same close
  • signal intraday / mark at close

If this changes later, it must be a scenario-level parameter.

5. Continuous-vs-listed quantity must be explicit

MVP synthetic runs may use continuous_units. The shipped BT-002 provider slice also remains continuous_units-only. listed_contracts with contract-size rounding is deferred to follow-up slice BT-002A.

Do not hide rounding rules inside providers. They belong in the position sizing logic and must be recorded in the result.

6. Costs must be recorded as cashflows

Premiums and close/expiry proceeds must be stored as dated cashflows. Do not collapse the entire hedge economics into end-of-period payoff only.

7. Missing data cannot be silent

Any missing snapshot/mark fallback must add:

  • a run or template warning
  • a template validation note
  • and, in a fuller follow-up slice, a deterministic result status if the template becomes incomplete

Anti-lookahead rules

These should be copied into tests and implementation notes verbatim.

  1. Contract selection rule: select options using only the entry-day snapshot.
  2. Daily MTM rule: mark open positions using only same-day data for the same contract.
  3. Expiry rule: once as_of_date >= expiry, option value becomes intrinsic-at-expiry or zero after expiry according to the provider contract; it is not repriced with negative time-to-expiry.
  4. Event preset rule: event presets may define scenario dates, but the strategy engine may not inspect future event endpoints when deciding to roll or exit.
  5. Synthetic vol rule: synthetic providers may use fixed or date-indexed IV schedules, but never realized future path statistics from dates after as_of_date.
  6. Metric rule: comparison metrics may summarize the whole run only after the run completes; they may not feed back into trading decisions during the run.

Phased implementation plan with TDD slices

Each slice should leave behind tests and a minimal implementation path.

Slice 0 — Red tests for model invariants

Target:

  • create tests for StrategyTemplate, BacktestScenario, BacktestRun, EventPreset
  • validate weights, dates, versioned references, and uniqueness assumptions

Suggested tests:

  • invalid ladder weights rejected
  • scenario with end before start rejected
  • template ref requires explicit version
  • loan amount cannot exceed initial collateral value

Slice 1 — Named template repository (EXEC-001A core)

Target:

  • file-backed StrategyTemplateRepository
  • save/load/list active templates
  • version bump on immutable update

Suggested tests:

  • saving template round-trips cleanly
  • updating active template creates version 2, not in-place mutation
  • archived template stays loadable for historical runs

Slice 2 — Synthetic provider contract (BT-001 foundation)

Target:

  • HistoricalMarketDataProvider protocol
  • SyntheticHistoricalProvider
  • deterministic underlying fixture input + synthetic option pricing

Suggested tests:

  • provider returns stable trading day list
  • spot-pct strike resolution uses same-day spot only
  • repricing uses decreasing time to expiry
  • no future bar access required for day-T pricing

Slice 3 — Single-template backtest engine

Target:

  • run one protective-put template across a short scenario
  • output daily path + summary metrics

Suggested tests:

  • hedge premium paid on entry day
  • option MTM increases when spot falls materially below strike
  • hedged max LTV is <= unhedged max LTV in a monotonic selloff fixture
  • completed run freezes scenario and template version snapshots

Slice 4 — Multi-template comparison runs

Target:

  • compare ATM, 95% put, 50/50 ladder on same scenario
  • produce ranked comparison_summary

Suggested tests:

  • all template results share same scenario snapshot
  • ranking uses documented metric order
  • equal primary metric falls back to next metric deterministically

Slice 5 — Roll logic and expiry behavior

Target:

  • support roll_n_days_before_expiry
  • support expiry settlement and position replacement

Suggested tests:

  • roll occurs exactly on configured trading-day offset
  • expired contracts stop carrying time value
  • no contract identity mutation between entry and close

Slice 6 — Event presets and BT-003 scenario materialization

Target:

  • repository for EventPreset
  • materialize preset -> scenario
  • run comparison over multiple named events

Suggested tests:

  • preset dates map cleanly into scenario dates
  • scenario overrides are applied explicitly
  • normalized event series start from common baseline

Slice 7 — Daily snapshot provider (BT-002)

Target:

  • add DailyOptionsSnapshotProvider behind same contract
  • reuse existing engine with provider swap only

Suggested tests:

  • entry picks nearest valid listed contract from snapshot
  • later MTM uses same contract key
  • missing mark generates warning and applies documented fallback
  • synthetic and snapshot providers both satisfy same provider test suite

Slice 8 — Thin API/UI integration after engine is proven

Not part of this docs implementation scope, but the natural next step is:

  • /api/templates
  • /api/backtests
  • /api/backtests/{run_id}
  • later a NiceGUI page for listing templates and runs

Per project rules, do not claim this feature is live until the UI consumes real run data.


Recommended minimal layout for this codebase:

app/
  backtesting/
    __init__.py
    engine.py                 # run loop, ranking, metric aggregation
    position_sizer.py         # continuous vs listed quantity rules
    result_metrics.py         # path -> summary metrics
    scenario_materializer.py  # event preset -> scenario
    selection.py              # strike/expiry resolution helpers
  models/
    strategy_template.py      # StrategyTemplate, TemplateLeg, RollPolicy, EntryPolicy
    backtest.py               # BacktestScenario, BacktestRun, results, daily points
    event_preset.py           # EventPreset, overrides
    historical_data.py        # UnderlyingBar, OptionSnapshot, InstrumentKey, marks
  services/
    backtesting/
      __init__.py
      orchestrator.py         # submit/load/list runs
      repositories.py         # file-backed run repository helpers
    historical/
      __init__.py
      base.py                 # HistoricalMarketDataProvider protocol
      synthetic.py            # BT-001 provider
      snapshots.py            # BT-002 provider
    templates/
      __init__.py
      repository.py           # save/load/list/version templates
    events/
      __init__.py
      repository.py           # save/load/list presets

Persistence recommendation for MVP

Use file-backed repositories first:

data/
  strategy_templates.json
  event_presets.json
  backtests/
    <run_id>.json

Reason:

  • aligns with current PortfolioRepository style
  • keeps the MVP small
  • allows deterministic fixtures in tests
  • can later move behind the same repository interfaces

Code reuse guidance

Implementation agents should reuse existing code selectively.

Safe to reuse

  • pricing helpers in app/core/pricing/
  • payoff logic concepts from app/models/option.py
  • existing strategy presets from app/strategies/engine.py as seed templates

Do not reuse directly without adaptation

  • StrategySelectionEngine as the backtest engine
  • DataService as a historical run orchestrator
  • LombardPortfolio.gold_ounces as the canonical backtest exposure field

Reason: these current types are optimized for present-time research payloads, not dated position lifecycle state.


Open implementation decisions to settle before coding

  1. Underlying source for synthetic BT-001: use yfinance historical closes directly, local fixture CSVs, or both?
  2. Quantity mode in first runnable slice: support only continuous_units first, or implement listed contract rounding immediately?
  3. Scenario end behavior: liquidate remaining option at final close, or leave terminal MTM only?
  4. Missing snapshot policy: hard-fail vs warn-and-carry-forward?
  5. Provider metadata freezing: store config only, or config + source data hash?

Recommended answers for MVP:

  • yfinance historical closes with deterministic test fixtures for unit tests
  • continuous_units first
  • liquidate at final close for clearer realized P&L
  • warn-and-carry-forward only for same-contract marks, otherwise fail
  • freeze provider config plus app/git version

Implementation-ready recommendations

  1. Build BT-001 around a new provider interface, not around DataService.
  2. Treat templates as immutable versioned definitions. Runs must reference template versions, not mutable slugs only.
  3. Use a daily close-to-close engine for MVP and document it everywhere.
  4. Record every hedge premium and payoff as dated cashflows.
  5. Keep synthetic provider and daily snapshot provider behind the same contract.
  6. Introduce underlying_units in backtesting models to avoid gold_ounces ambiguity.
  7. Make missing data warnings explicit and persistent in run results.