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 ¶
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 ¶
An equally weighted portfolio over the universe.
market_frame ¶
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 ¶
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_frame ¶
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.
sectors ¶
Constituents grouped by sector, for group-constraint tests.
trading_days ¶
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 |
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
Generatorproduces 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. expandlogare 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
GBPUSDpair 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 ¶
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 ¶
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. |
market_frame ¶
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 ¶
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.
data_fetcher ¶
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 ¶
Constituents grouped by sector, for group-constraint tests.
equal_weights ¶
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_snapshotsandconstituent_snapshotscarry the schedule verbatim, so the engine trades exactly the weights that were written down.index_levelsis a flat series at base_value over the schedule's business-day span: a stand-in level, not a claim about performance.divisor_historyis 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 |
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. |