32 KiB
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 newapp/backtesting/package - lightweight docs under
docs/
Design goals
- Keep current live quote/options flows working. Do not overload
app/services/data_service.pywith historical backtest state. - Make templates reusable and named. A strategy definition should be saved once and referenced by many backtests.
- Support synthetic-first backtests. BT-001 must work before BT-002 exists.
- Prevent lookahead bias by design. Providers and the run engine must expose only data available at each
as_of_date. - Preserve a migration path to real daily options snapshots. Synthetic pricing and snapshot-based pricing must share the same provider contract.
- 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
LombardPortfoliounchanged for existing live pages. - Introduce backtesting-specific portfolio state using the neutral term
underlying_units. - Treat
symbol+underlying_unitsas 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.pyapp/models/backtest.pyapp/models/event_preset.pyapp/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_targetfixed_strikemoneyness_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:
slugunique among active templates- template versions immutable once referenced by a completed run
- weights sum to
1.0forprotective_put/laddered_puttemplates - all legs use the same
target_expiry_daysin MVP unless explicitly marked as a ladder with shared roll policy underlying_symbolon the template must either match the scenario symbol or be*/generic if generic templates are later supported
Template examples
protective-put-atm-12mprotective-put-95pct-12mladder-50-50-atm-95pct-12mladder-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 dateTclose - Mark-to-market for date
Tuses the sameTsnapshot
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_resultsbut must preservewarnings/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
Recommended behavior
Inputs:
- underlying close series (from yfinance file cache, CSV fixture, or another deterministic source)
- configured implied volatility regime, e.g. fixed
0.16or 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
midprice 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
Recommended behavior
- 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:
- exact contract from same-day snapshot
- if unavailable, previous available mark from same contract with warning
- if unavailable and contract is expired, intrinsic value at expiry or zero afterward
- 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:
- load trading days from provider
- create baseline unhedged path
- resolve initial hedge on
start_date - 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
Tdata - if a roll is due, close/expire old position and open replacement using
Tsnapshot
- read underlying close for day
- liquidate remaining position at scenario end if still open
- calculate summary metrics
- rank templates inside
comparison_summary
Position model recommendation
Use a separate open-position model rather than reusing OptionContract directly.
Recommended HistoricalOptionPosition fields:
position_idinstrument_keyopened_atexpiryquantityentry_pricecurrent_marktemplate_leg_idsource_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:
- fewer
margin_call_days_hedged - lower
max_ltv_hedged - lower
total_hedge_cost - 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
Tfor 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.
BT-002 listed snapshot runs should support listed_contracts with contract-size rounding.
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 warning
- a template validation note
- a deterministic result status if the template becomes incomplete
Anti-lookahead rules
These should be copied into tests and implementation notes verbatim.
- Contract selection rule: select options using only the entry-day snapshot.
- Daily MTM rule: mark open positions using only same-day data for the same contract.
- 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. - 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.
- 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. - 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:
HistoricalMarketDataProviderprotocolSyntheticHistoricalProvider- 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-
Tpricing
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
DailyOptionsSnapshotProviderbehind 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 doc’s 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 file/module layout
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
PortfolioRepositorystyle - 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.pyas seed templates
Do not reuse directly without adaptation
StrategySelectionEngineas the backtest engineDataServiceas a historical run orchestratorLombardPortfolio.gold_ouncesas 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
- Underlying source for synthetic BT-001: use yfinance historical closes directly, local fixture CSVs, or both?
- Quantity mode in first runnable slice: support only
continuous_unitsfirst, or implement listed contract rounding immediately? - Scenario end behavior: liquidate remaining option at final close, or leave terminal MTM only?
- Missing snapshot policy: hard-fail vs warn-and-carry-forward?
- 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_unitsfirst- 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
- Build BT-001 around a new provider interface, not around
DataService. - Treat templates as immutable versioned definitions. Runs must reference template versions, not mutable slugs only.
- Use a daily close-to-close engine for MVP and document it everywhere.
- Record every hedge premium and payoff as dated cashflows.
- Keep synthetic provider and daily snapshot provider behind the same contract.
- Introduce
underlying_unitsin backtesting models to avoidgold_ouncesambiguity. - Make missing data warnings explicit and persistent in run results.