2. Data model

July 26, 2026 · View on GitHub

The decoded MessagePack body is a single map with the following top-level shape. All types below are described in terms of msgpack primitives (map, array, str, int, uint, float, bool, nil, bin).

Top level: Bundle

Bundle (msgpack map)
├── format_version: uint            # mirrors the version in the envelope header
├── algorithm_id: str               # producer-defined stable identifier (opaque)
├── anchor_algorithm_id: str | nil  # lineage edge: id of the anchor algorithm this
│                                   #   bundle was derived from. nil on anchors,
│                                   #   set on perturbed / sibling bundles.
├── parameters: map                 # producer-defined canonical param fingerprint
├── metadata: map                   # algorithm-level, static metadata (see below)
├── studies: map[str, Study]        # keyed by study_name
└── blobs: map[str, bin]            # blob key → raw Parquet bytes; MAY be empty

Producers MUST set format_version and algorithm_id. All other top-level keys are OPTIONAL but RECOMMENDED.

The algorithm_id is opaque to the format. Two bundles with the same algorithm_id are asserted by the producer to represent the same underlying algorithm; readers MUST NOT infer semantic identity beyond string equality.

Reserved metadata keys

The following top-level metadata keys are RESERVED and SHOULD be populated by producers when the information is available:

KeyTypeMeaning
algorithm_id_schemestrHow algorithm_id was computed (e.g. "sha256:code+params").
producerstrName of the tool/framework that wrote the bundle.
producer_versionstrVersion of that tool.
writer_created_atstrISO 8601 timestamp of when the bundle was written.

Any additional keys are permitted. Unknown keys MUST be preserved on round-trip.

Study

A Study represents a single strategy idea / signal within an algorithm. A bundle MAY hold multiple studies (e.g. an in-sample sweep and an out-of-sample validation side by side).

Study (msgpack map)
├── name: str                          # human-readable idea, e.g. "ema_cross"
├── description: str                   # free-form
├── created_at: str                    # ISO 8601 timestamp
├── windows: array[BacktestWindow | BacktestDateRange]
│                                      # date ranges this study has runs for.
│                                      #   Simple backtests use BacktestDateRange;
│                                      #   walk-forward / k-fold workflows use
│                                      #   BacktestWindow. See §5.
├── universes: array[Universe]         # registry of universes this study has runs for
│                                      #   (per-run universe lives on Run)
├── sample_type: str | nil             # categorical role — see §5 for the vocabulary
├── execution_config: ExecutionConfig | nil
│                                      # cost / fill assumptions for this study's runs
├── engines: map[str, EngineSlot]      # engine slot, keyed by engine name.
│                                      #   Well-known keys: "vector", "event".
│                                      #   Vendor-defined names allowed (see §10).
└── metadata: map                      # free-form study-level metadata

Readers MUST tolerate absence of any field other than name. Readers MUST treat unknown sample_type values as opaque strings (see 5. Universes & windows).

Two studies inside the same bundle MAY carry different execution_config values — e.g. an optimistic zero-cost sweep next to a realistic-cost validation. Anchoring execution_config to Study (rather than to Bundle) keeps each study self-describing.

EngineSlot

An EngineSlot holds all the runs and pooled statistics for one engine class within a study.

EngineSlot (msgpack map)
├── runs: array[Run]                          # each Run carries its own Universe
├── summary: Summary | nil                    # pooled across all runs
├── summaries_by_universe: map[str, Summary]  # per-universe cached summaries;
│                                             #   MAY be absent or empty
└── monte_carlo_tests: array[MonteCarloTest]  # advisory metadata; MAY be empty

The summary field is a cache of aggregate metrics across runs. Readers MAY recompute it from runs if they choose; conforming producers SHOULD populate it.

summaries_by_universe is a per-universe cache keyed by Universe.key. Its purpose is to make the "same signal evaluated across 3 regimes" case cheap to read: three entries in the map + one pooled summary.

Run

A Run corresponds to a single execution of the algorithm over one date range / window on one universe.

Run (msgpack map)
├── backtest_start_date: str         # ISO 8601, inclusive
├── backtest_end_date: str           # ISO 8601, inclusive
├── backtest_date_range_name: str    # e.g. "in_sample", "oos_2023"
├── universe: Universe               # which universe this run was evaluated on
├── trading_symbol: str              # base currency, e.g. "EUR"
│                                    #   (mirror of universe.trading_symbol)
├── initial_unallocated: float       # initial cash balance
├── created_at: str                  # ISO 8601 timestamp
├── number_of_days: uint             # length of the window in days
├── symbols: array[str]              # symbols traded (mirror of universe.symbols)

├── backtest_metrics: BacktestMetrics       # per-run metrics; see §3
├── portfolio_snapshots: array[map]         # periodic portfolio state
├── trades: array[map]                      # all trades executed
├── orders: array[map]                      # all orders placed
├── positions: array[map]                   # all position changes

├── number_of_trades: uint
├── number_of_trades_closed: uint
├── number_of_trades_open: uint
├── number_of_orders: uint
├── number_of_positions: uint

├── data_sources: array[map]                # data source references (not raw data)
├── signals: map[str, map[str, array]]      # buy/sell signals keyed by symbol
├── signal_events: array[map]               # chronological log of fired signals
├── recorded_values: map[str, array]        # producer-recorded custom values
└── metadata: map                           # free-form per-run metadata

The row schemas of portfolio_snapshots, trades, orders, positions, signals, and signal_events are producer-defined in v1. Consumers that need cross-producer interoperability on these fields SHOULD consult a producer's own documentation. Future revisions of OBTF MAY standardise these row schemas.

BacktestMetrics, Summary, MonteCarloTest

See 3. Metrics for the full field catalog and 4. Blobs for the extraction protocol used to move large time-series out of these maps.

Universe, BacktestDateRange

See 5. Universes & windows.

ExecutionConfig

See 6. Execution config.