Skip to content

Sample dataset

A small, fixed dataset for tests, examples and documentation. Its values never change.

testing

Deterministic synthetic data for tests, examples and documentation.

Shipped rather than kept under tests/ because the documentation gallery and the examples need the same numbers the tests use. A chart in the docs built from a different helper than the baseline it is compared against is a chart nobody can debug, and asking a reader to invent their own data before they can run an example is friction with no upside.

from beacon.testing import dataset

fetcher = dataset.data_fetcher()
prices = dataset.prices()

Everything here is generated from fixed constants and returns a fresh copy, so two callers always see identical frames and neither can disturb the other. See dataset for why the arithmetic avoids exp; the short version is that bit-identical output across operating systems is a requirement, not a bonus.

Core-only: pandas and numpy, no optional dependency, because a test that needs data should not first need an extra installed.

Constituent dataclass

Constituent(
    identifier: str,
    name: str,
    sector: str,
    currency: str,
    initial_price: float,
    beta: float,
    idiosyncratic: float,
    drift: float,
    shares_outstanding: int,
    free_float: float,
)

One synthetic company and the behaviour its price path should show.

Attributes:

Name Type Description
identifier str

Ticker.

name str

Display name.

sector str

Classification, for group constraints and breakdowns.

currency str

Trading currency.

initial_price float

Price on the first date.

beta float

Sensitivity to the common market factor.

idiosyncratic float

Daily volatility of the asset's own noise.

drift float

Daily expected return on top of the market.

shares_outstanding int

For market-cap weighting.

free_float float

Fraction of shares actually investable.

data_fetcher

data_fetcher() -> DataFetcher

The dataset as a DataFetcher: the usual entry point.

Returns:

Name Type Description
DataFetcher DataFetcher

Wired to the full universe, the FX pair and the reference

DataFetcher

data, ready to hand to an IndexCalculator or a BacktestEngine.

equal_weights

equal_weights() -> dict[str, float]

An equally weighted portfolio over the universe.

fx_rates

fx_rates() -> pd.Series

The GBPUSD series.

market_data

market_data() -> MarketData

The dataset as a MarketData container.

market_frame

market_frame() -> pd.DataFrame

The long-form market data, in the shape MarketData expects.

One row per identifier per date, carrying OHLC, volume, shares outstanding and free float: enough for market-cap weighting and for a price_column override to have something else to point at. The GBPUSD pair is included as its own identifier, with its rate in every price column and no shares outstanding or free float.

prices

prices() -> pd.DataFrame

Closing prices, dates on the index and identifiers on the columns.

Returns:

Type Description
DataFrame

pd.DataFrame: A copy, so callers may modify it freely.

reference_data

reference_data() -> ReferenceData

The dataset as a ReferenceData container.

reference_frame

reference_frame() -> pd.DataFrame

The reference data, valid across the whole span.

One open-ended validity row per constituent, with no classification changes, so a test that does not care about validity windows does not have to think about them.

returns

returns() -> pd.DataFrame

Daily simple returns, with the first (undefined) row dropped.

sectors

sectors() -> dict[str, list[str]]

Constituents grouped by sector, for group-constraint tests.

trading_days

trading_days() -> pd.DatetimeIndex

The dataset's sessions: the days XNYS was open, inside the span.

START is 2 January 2023, the day NYSE observed New Year's Day, so the panel starts on the 3rd: 752 sessions in all, with no bars on market holidays such as 4 July and 25 December.

FFF is quoted in GBP and listed on the LSE, and gets a bar on every XNYS session regardless: one calendar for the dataset. A universe whose venues genuinely disagree about sessions is a real modelling question and deliberately not this fixture's.

index_result_from_weights

index_result_from_weights(
    schedule: dict[Timestamp, dict[str, float]],
    index_id: str = "TEST",
    base_value: float = 1000.0,
) -> IndexResult

Build a minimal valid :class:IndexResult from a weight schedule.

Parameters:

Name Type Description Default
schedule dict[Timestamp, dict[str, float]]

Mapping of rebalance date (anything pd.Timestamp accepts) to {asset_id: weight}. An empty inner dict is a rebalance into cash.

required
index_id str

Identifier stamped on the result.

'TEST'
base_value float

The flat level the synthesised series holds.

1000.0

Returns:

Name Type Description
IndexResult IndexResult

Snapshots straight from schedule, a flat level series

IndexResult

over its business-day span, divisor 1.0, and a daily weights panel

IndexResult

that forward-fills each snapshot until the next.

Raises:

Type Description
ValueError

If schedule is empty: an engine with no rebalance schedule at all has nothing to simulate.

dataset

The canonical synthetic dataset.

One fixed universe, one fixed date span, one set of price paths. Every test, example and documentation page that needs "some data" should use this rather than growing another local helper, because scaffolding written six times in six slightly different shapes means six subtly different answers and no way to tell which one a baseline was built against.

Reproducibility, and why the maths is deliberately dull

The paths are built from + and * only: no exp, no log. That is not an accident and not a simplification:

  • numpy's Generator produces bit-identical draws across platforms for a given seed, because the ziggurat sampler is numpy's own C rather than the system library.
  • IEEE 754 pins the results of +, -, * and / exactly, so every platform agrees on them to the last bit.
  • exp and log are not pinned that way. They come from the platform's libm and are allowed to differ in the last unit in the last place, which compounding then amplifies.

So returns are simple rather than logarithmic and prices compound by multiplication. A geometric path built the textbook way through exp would be reproducible on one machine and off by a hair on another: invisible in a unit test, and exactly the kind of thing that makes an image-regression baseline fail on a different runner for no reason anyone can see.

What is in it

Six assets chosen so the data has something to say:

  • AAA and BBB are close substitutes (high beta, small idiosyncratic noise), so correlation, shrinkage and substitution effects have a real signal to find.
  • CCC is defensive: low beta, low volatility, and only weakly correlated with the rest. Tuned that way on purpose: with a merely low-volatility CCC the minimum-variance portfolio would put 100% into it, and a corner solution tests an optimiser far less than an interior one. As it stands the answer blends three names and beats the least volatile single asset, so diversification has to be working for the numbers to come out right.
  • DDD is the volatile high-flyer, EEE the laggard, so return and risk rankings disagree and an optimiser has a genuine trade-off.
  • FFF trades in GBP, so anything touching FX has a case that exercises it. The GBPUSD pair is in the market data as its own identifier.

The span runs from START to END on the XNYS calendar's sessions.

Constituent dataclass

Constituent(
    identifier: str,
    name: str,
    sector: str,
    currency: str,
    initial_price: float,
    beta: float,
    idiosyncratic: float,
    drift: float,
    shares_outstanding: int,
    free_float: float,
)

One synthetic company and the behaviour its price path should show.

Attributes:

Name Type Description
identifier str

Ticker.

name str

Display name.

sector str

Classification, for group constraints and breakdowns.

currency str

Trading currency.

initial_price float

Price on the first date.

beta float

Sensitivity to the common market factor.

idiosyncratic float

Daily volatility of the asset's own noise.

drift float

Daily expected return on top of the market.

shares_outstanding int

For market-cap weighting.

free_float float

Fraction of shares actually investable.

trading_days

trading_days() -> pd.DatetimeIndex

The dataset's sessions: the days XNYS was open, inside the span.

START is 2 January 2023, the day NYSE observed New Year's Day, so the panel starts on the 3rd: 752 sessions in all, with no bars on market holidays such as 4 July and 25 December.

FFF is quoted in GBP and listed on the LSE, and gets a bar on every XNYS session regardless: one calendar for the dataset. A universe whose venues genuinely disagree about sessions is a real modelling question and deliberately not this fixture's.

prices

prices() -> pd.DataFrame

Closing prices, dates on the index and identifiers on the columns.

Returns:

Type Description
DataFrame

pd.DataFrame: A copy, so callers may modify it freely.

returns

returns() -> pd.DataFrame

Daily simple returns, with the first (undefined) row dropped.

fx_rates

fx_rates() -> pd.Series

The GBPUSD series.

market_frame

market_frame() -> pd.DataFrame

The long-form market data, in the shape MarketData expects.

One row per identifier per date, carrying OHLC, volume, shares outstanding and free float: enough for market-cap weighting and for a price_column override to have something else to point at. The GBPUSD pair is included as its own identifier, with its rate in every price column and no shares outstanding or free float.

reference_frame

reference_frame() -> pd.DataFrame

The reference data, valid across the whole span.

One open-ended validity row per constituent, with no classification changes, so a test that does not care about validity windows does not have to think about them.

market_data

market_data() -> MarketData

The dataset as a MarketData container.

reference_data

reference_data() -> ReferenceData

The dataset as a ReferenceData container.

data_fetcher

data_fetcher() -> DataFetcher

The dataset as a DataFetcher: the usual entry point.

Returns:

Name Type Description
DataFetcher DataFetcher

Wired to the full universe, the FX pair and the reference

DataFetcher

data, ready to hand to an IndexCalculator or a BacktestEngine.

sectors

sectors() -> dict[str, list[str]]

Constituents grouped by sector, for group-constraint tests.

equal_weights

equal_weights() -> dict[str, float]

An equally weighted portfolio over the universe.

weights

A minimal valid IndexResult built from a raw weight schedule.

The backtest engine takes its schedule from an index_result. Tests, examples and notebooks often want the cheap construction instead ("these weights on these dates, nothing else"), so this helper turns that dict into an :class:IndexResult the engine accepts, with every field a consumer reads filled consistently:

  • weight_snapshots and constituent_snapshots carry the schedule verbatim, so the engine trades exactly the weights that were written down.
  • index_levels is a flat series at base_value over the schedule's business-day span: a stand-in level, not a claim about performance.
  • divisor_history is 1.0 throughout, and the daily weights panel prices every constituent at 1.0, so the divisor identity holds trivially: Σ amount × price ÷ divisor = base_value on every day.

Core-only: pandas, nothing optional.

index_result_from_weights

index_result_from_weights(
    schedule: dict[Timestamp, dict[str, float]],
    index_id: str = "TEST",
    base_value: float = 1000.0,
) -> IndexResult

Build a minimal valid :class:IndexResult from a weight schedule.

Parameters:

Name Type Description Default
schedule dict[Timestamp, dict[str, float]]

Mapping of rebalance date (anything pd.Timestamp accepts) to {asset_id: weight}. An empty inner dict is a rebalance into cash.

required
index_id str

Identifier stamped on the result.

'TEST'
base_value float

The flat level the synthesised series holds.

1000.0

Returns:

Name Type Description
IndexResult IndexResult

Snapshots straight from schedule, a flat level series

IndexResult

over its business-day span, divisor 1.0, and a daily weights panel

IndexResult

that forward-fills each snapshot until the next.

Raises:

Type Description
ValueError

If schedule is empty: an engine with no rebalance schedule at all has nothing to simulate.