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.
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.
- 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.