Skip to content

Server

The local API server: create_app, ServerConfig and the command line. The endpoints themselves are documented in the Server API reference.

server

The local Beacon API server.

Requires the server extra:

pip install "py-beacon-kit[server]"

Importing this subpackage pulls in FastAPI, so the guard fires here and names the extra rather than letting a bare ImportError surface. The rest of Beacon stays importable without it.

ServerConfig dataclass

ServerConfig(
    auth_token: str,
    host: str = "127.0.0.1",
    port: int = 0,
    data_fetcher: DataFetcher | None = None,
    market_downloader: Downloader | None = None,
    cors_origins: tuple[str, ...] = DEFAULT_CORS_ORIGINS,
    storage_root: Path | None = None,
    data_store_id: str | None = None,
    data_store_name: str | None = None,
    startup_store: dict[str, Any] | None = None,
)

Settings for a single server process.

Attributes:

Name Type Description
auth_token str

Bearer token required on every request. Never empty: a server with no token would be open to any process on the machine.

host str

Interface to bind. Defaults to loopback and should stay there: the server has no transport security and trusts its bearer token alone.

port int

Port to bind. 0 asks the OS for a free one, which the launcher then reads back and prints.

data_fetcher DataFetcher | None

The data source to serve, or None to run without one.

cors_origins tuple[str, ...]

Exact origins allowed, in addition to the localhost pattern.

market_downloader Downloader | None

Where a store refresh fetches market data from, for a folder store set to refresh from Yahoo Finance. None builds the yfinance-backed downloader on first use, which is the real deployment; tests and offline runs inject their own so the download path is exercisable without a network.

storage_root Path | None

Base directory for persisted documents. None uses the platform app-data location; tests point it at a temporary path.

data_store_id str | None

The registered data store data_fetcher came from, or None.

data_store_name str | None

A name to show for the data being served: the store's name, or where unregistered data came from.

startup_store dict[str, Any] | None

A registered store's record to load in the background once the engine is serving, with /health reporting loading meanwhile, or None.

from_environment classmethod

from_environment(
    token: str | None = None,
    host: str = "127.0.0.1",
    port: int = 0,
    data_fetcher: DataFetcher | None = None,
    cors_origins: tuple[str, ...] | None = None,
    storage_root: Path | None = None,
    data_store_id: str | None = None,
    data_store_name: str | None = None,
    startup_store: dict[str, Any] | None = None,
) -> ServerConfig

Build a config, taking the token from the environment if not given.

Parameters:

Name Type Description Default
token str | None

Explicit token, typically from the command line. When None, the value of BEACON_API_TOKEN is used.

None
host str

Interface to bind.

'127.0.0.1'
port int

Port to bind; 0 asks the OS for a free one.

0
data_fetcher DataFetcher | None

Data source to serve, or None.

None
cors_origins tuple[str, ...] | None

Exact origins to allow. None resolves them from the environment and the defaults.

None
storage_root Path | None

Where saved documents live. None uses the platform app-data location.

None
data_store_id str | None

The registered store the data came from, if any.

None
data_store_name str | None

A name to show for the data being served.

None
startup_store dict[str, Any] | None

A registered store to load once serving.

None

Returns:

Name Type Description
ServerConfig ServerConfig

The assembled configuration.

Raises:

Type Description
ValueError

If no token is available from either source.

BacktestResultSummary

Bases: BaseModel

Serialised view of a BacktestResult, in the shape of its books.

The nested shape mirrors the library object, one home per fact. Books the run did not have (no benchmark given, no index calculated) are null rather than empty, so a client can tell "not measured" from "measured and empty". index is a container of two books, {target, optimised}, matching the library's IndexBooks.

from_result classmethod

from_result(
    result: BacktestResult,
    cap: float | None = None,
    settings: dict[str, Any] | None = None,
    weights: bool = True,
) -> BacktestResultSummary

Build from a library BacktestResult.

Parameters:

Name Type Description Default
result BacktestResult

The finished run.

required
cap float | None

The maximum constituent weight declared by the definition whose rules produced the target book: the document's own on a passive run, its parent's on an optimised one. Stamped on that book's rebalance snapshots only: a solved index has no cap of its own, since its constraints are what shaped its weights.

None
settings dict[str, Any] | None

What ran, resolved, to keep with the record.

None
weights bool

Whether each book's daily weights are filled in. Left empty, weights_dates_total still counts them, for a record whose weights are served from the run's own tables.

True

ErrorEnvelope

Bases: BaseModel

Every non-2xx response uses this shape.

HealthResponse

Bases: BaseModel

Response of GET /health.

IndexResultSummary

Bases: BaseModel

Serialised view of an IndexResult.

from_result classmethod

from_result(result: IndexResult) -> IndexResultSummary

Build from a library IndexResult.

Money

Bases: BaseModel

An amount with its denomination.

A bare float would leave the currency implicit, which breaks as soon as a response mixes denominations.

SeriesPayload

Bases: BaseModel

A pandas Series on the wire.

from_series classmethod

from_series(series: Series) -> SeriesPayload

Build from a pandas Series.

TableFrame

Bases: BaseModel

A pandas DataFrame on the wire.

Row-oriented so column order is preserved and the payload stays compact.

from_dataframe classmethod

from_dataframe(frame: DataFrame) -> TableFrame

Build from a pandas DataFrame.

create_app

create_app(config: ServerConfig) -> FastAPI

Build the ASGI application for a given configuration.

Parameters:

Name Type Description Default
config ServerConfig

Settings for this process, including the bearer token every route will require and the data source to serve.

required

Returns:

Name Type Description
FastAPI FastAPI

The configured application. Nothing is bound or started

FastAPI

here; see beacon.server.main for the launcher.

classify

classify(exc: BeaconError) -> tuple[int, str]

Map a library exception to its HTTP status and stable code.

Parameters:

Name Type Description Default
exc BeaconError

The raised library exception.

required

Returns:

Name Type Description
tuple int

(http_status, code). Unregistered BeaconError subclasses

str

fall through to the catch-all rather than escaping as a bare 500.

register_exception_handlers

register_exception_handlers(app: FastAPI) -> None

Attach the handlers that put every error into the envelope.

Parameters:

Name Type Description Default
app FastAPI

The application to register on.

required

dataframe_to_payload

dataframe_to_payload(frame: DataFrame) -> dict[str, Any]

Serialise a DataFrame as {index, columns, data}.

Row-oriented data keeps the payload compact and preserves column order, which a dict-of-columns would not guarantee.

Parameters:

Name Type Description Default
frame DataFrame

The frame to serialise. An empty frame yields empty lists.

required

Returns:

Name Type Description
dict dict[str, Any]

index (list of row labels), columns (list of column

dict[str, Any]

names), and data (list of rows, each a list of cell values).

dict[str, Any]

NaN, NaT and infinities all become None.

series_to_payload

series_to_payload(series: Series) -> dict[str, Any]

Serialise a Series as {index, name, data}.

Parameters:

Name Type Description Default
series Series

The series to serialise.

required

Returns:

Name Type Description
dict dict[str, Any]

index (list of labels), name (the series name, or None),

dict[str, Any]

and data (list of values, with NaN/NaT/infinities as None).

active_data

The data the engine is serving right now, and the one rule for needing it.

A data store can be loaded, or switched, while the engine runs, so the current data lives here, on app state, and every reader asks this module.

A route that needs data and finds none answers 409: no data being loaded is a state the caller can change by loading a store, not a server fault.

ActiveData dataclass

ActiveData(
    fetcher: DataFetcher | None = None,
    store_id: str | None = None,
    store_name: str | None = None,
    loading: bool = False,
    data_version: str = new_data_version(),
)

What the engine serves: the loaded data and the store it came from.

Replaced whole when a store is loaded, never edited in place, so a request that has already read fetcher keeps a consistent dataset for its whole run even if another store is loaded meanwhile.

Attributes:

Name Type Description
fetcher DataFetcher | None

The loaded data, or None when nothing is loaded.

store_id str | None

The registered store it came from. None for data given on the command line or by BEACON_DATA_PATH, which is not a registered store, and when nothing is loaded.

store_name str | None

A name to show for it: the store's name, or where the unregistered data came from.

loading bool

Whether a store is being loaded right now. One load at a time: a second would race the first to replace the data.

data_version str

An opaque token that changes whenever the data being served changes: at startup, and on every load (the same store loaded again included, since its files may have changed, and a refresh of the served store, which reloads it). A client compares it for equality only, to know whether what it cached is still current. Random rather than a counter, so an engine restart can never bring an old value back.

data_changed
data_changed() -> str

Mint a new data_version after the data changed in place.

new_data_version

new_data_version() -> str

A token no earlier data, in this process or any before it, can have.

active_data

active_data(request: Request) -> ActiveData

The engine's current data holder.

current_data

current_data(request: Request) -> DataFetcher | None

The loaded data, or None, for a reader that works either way.

require_data

require_data(request: Request, purpose: str) -> DataFetcher

The loaded data, or a refusal saying what could not be done.

Parameters:

Name Type Description Default
request Request

The incoming request.

required
purpose str

What needs the data, completing "No data is loaded, so ...", e.g. "a backtest cannot be run".

required

Raises:

Type Description
NoDataLoadedError

When nothing is loaded. Maps to 409 NO_DATA_LOADED.

analysis

A run's analysis on the wire: a backtest request's analysis built into the library's request, a stored run's analytics block kept and read beside its record, the catalogue of everything an analysis can be asked for, and a run's large tables by column or as Arrow.

The server works out no figure here: the library does, and this turns its answers into JSON or Arrow.

build_request

build_request(spec: Any) -> AnalysisRequest

The library's request from a backtest request's analysis.

Raises:

Type Description
InvalidArgumentError

If a report, level, statistic, view, section or setting is unknown, or a setting's value is refused.

keep_analytics

keep_analytics(folder: Path, block: dict[str, Any]) -> None

Keep a run's analytics block beside its tables.

read_analytics

read_analytics(folder: Path) -> dict[str, Any] | None

A stored run's analytics block, or None when it was made without one.

build_rerun_job

build_rerun_job(
    strategy_id: str,
    folder: Path,
    record: dict[str, Any] | None,
    request: AnalysisRequest,
    source: DataFetcher | None,
    save: str | None = None,
) -> Callable[
    [ProgressReporter], Awaitable[dict[str, Any]]
]

The job that gives request's analysis of a strategy's latest run.

With the choices the run's own analysis was made with, the run's block gains what was asked for and is kept; with other choices the answer is kept only under save. The run's factor work is read from its folder and kept there.

keep_saved

keep_saved(
    folder: Path, name: str, block: dict[str, Any]
) -> None

Keep block with the run under name, replacing one kept so.

saved_analyses

saved_analyses(folder: Path) -> list[dict[str, Any]]

Every analysis kept by name with the run, newest first, without their blocks.

read_saved

read_saved(folder: Path, name: str) -> dict[str, Any]

An analysis kept by name with the run.

Raises:

Type Description
DataNotFoundError

If none is kept under name.

delete_saved

delete_saved(folder: Path, name: str) -> None

Remove the analysis kept under name.

Raises:

Type Description
DataNotFoundError

If none is kept under it.

with_tables

with_tables(
    block: dict[str, Any],
    strategy_id: str,
    tables: Mapping[str, DataFrame],
) -> dict[str, Any]

block naming the run's large tables and where to read each.

catalogue

catalogue() -> dict[str, Any]

Everything an analysis can be asked for.

wire_table

wire_table(name: str, table: DataFrame) -> pd.DataFrame

A run's large table with its columns as they are sent, numbers rounded.

Raises:

Type Description
DataNotFoundError

If the run gives no such table.

columnar

columnar(name: str, table: DataFrame) -> dict[str, Any]

A large table by column: text as its distinct values and a code a row, a column the same on every row sent once.

arrow

arrow(name: str, table: DataFrame) -> bytes

A large table as an Arrow IPC stream, its text columns dictionary-encoded.

analysis_schemas

The wire shapes of a run's analysis: the request a backtest takes, the analytics block a run gives, the catalogue of what can be asked for, and a large table by column.

AnalysisRequestSpec

Bases: BaseModel

What a run's analysis should give: the reports wanted, each at a level, with statistics added or taken out, their time views, and every choice the analysis makes. GET /analysis/options lists the reports, statistics and settings.

StatisticRef

Bases: BaseModel

One statistic a report shows, and how.

ReportSelection

Bases: BaseModel

What a report shows, in order.

RollingValue

Bases: BaseModel

A statistic over each trailing window.

StatisticValue

Bases: BaseModel

One statistic's value.

UnavailableReason

Bases: BaseModel

Why a statistic was not worked out for the run.

TableRef

Bases: BaseModel

A large table of the run, read on its own.

AnalyticsBlock

Bases: BaseModel

A run's analysis: each statistic once, and each report as a list of statistic ids.

AnalyticsView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/analytics.

AnalyticsRerun

Bases: BaseModel

Body of POST /strategies/{strategy_id}/analytics: the analysis to give of the strategy's latest run, and a name to keep it under.

SavedAnalysis

Bases: BaseModel

One analysis kept by name with a strategy's latest run.

SavedAnalyses

Bases: BaseModel

Response of GET /strategies/{strategy_id}/analyses.

SavedAnalysisView

Bases: SavedAnalysis

Response of GET /strategies/{strategy_id}/analyses/{name}.

CodedColumn

Bases: BaseModel

A column of text sent once per distinct value.

ColumnarTable

Bases: BaseModel

A large table by column: each column a list of values, or, for repeated text, its distinct values and a code a row.

StatisticSpec

Bases: BaseModel

One statistic, as the catalogue lists it.

ReportEntrySpec

Bases: BaseModel

One statistic in a report level.

ReportSectionSpec

Bases: BaseModel

A level's statistics of one category, in order.

ReportLevelSpec

Bases: BaseModel

A report at one level.

ReportSpec

Bases: BaseModel

One report, at both levels.

TableSpec

Bases: BaseModel

A large table of a run.

UnavailableSpec

Bases: BaseModel

One reason a statistic can be unavailable.

AnalysisCatalogue

Bases: BaseModel

Response of GET /analysis/options: everything an analysis can be asked for, so a client hard-codes none of it.

app

Application factory for the Beacon API server.

The server is a local process owned by a desktop client: it binds loopback, authenticates every route with a bearer token the client generated, and holds no state of its own beyond the data source it was handed.

build_router

build_router() -> APIRouter

Build the router carrying the engine's own routes.

Returns:

Name Type Description
APIRouter APIRouter

Router with /health and /changelog, guarded by the bearer

APIRouter

dependency.

create_app

create_app(config: ServerConfig) -> FastAPI

Build the ASGI application for a given configuration.

Parameters:

Name Type Description Default
config ServerConfig

Settings for this process, including the bearer token every route will require and the data source to serve.

required

Returns:

Name Type Description
FastAPI FastAPI

The configured application. Nothing is bound or started

FastAPI

here; see beacon.server.main for the launcher.

backtest_options

GET /backtest/options: everything a backtest form needs, from the engine.

Every family's types come from the catalogue in the shape /indices/rule-types uses, so one form component renders them all. The presets carry their resolved settings, the vehicle's and the market model's settings and the implementation's plain settings are described field by field (with applies_to marking the exchange-traded ones), and the modelling assumptions are listed with the engine's defaults. Nothing here reads data.

backtest_options

backtest_options() -> BacktestOptions

The response of GET /backtest/options.

engine_defaults

engine_defaults() -> dict[str, Any]

The modelling assumptions a run makes when none are set: the library's conventions, and the data treatment a data source has by default (no staleness limit is 0 here).

backtest_settings

A backtest request's settings, built into the library's objects, and read back out fully resolved.

Each part of the request (strategy, implementation, flows, vehicle and modelling assumptions) is built from its {type, params} objects through the catalogue, so the server accepts exactly the types GET /backtest/options lists. Every problem is collected as a finding with the path of the field that caused it, such as vehicle.settings.pricing.params.threshold, and a request with any is refused whole.

A vehicle's settings over its preset

A vehicle is a preset (generic when none is named) with settings laid over it: a typed object (pricing) or a list (limits) replaces the preset's whole, a plain object (market) merges key by key, an omitted key keeps the preset's value, and null means none, refused on a setting that cannot be none. Settings for exchange-traded vehicles are refused on any other.

Findings dataclass

Findings(found: list[Finding] = list())

The problems found in a request, each at its field's path.

BuiltSettings dataclass

BuiltSettings(
    currency: str | None,
    dividends: str,
    reinvest_into: str,
    modelling_assumptions: ModellingAssumptions | None,
    strategy: Callable[[AnyIndexDefinition], Strategy],
    implementation: Implementation | None,
    flows: list[Flows] | dict[str, list[Flows]],
    vehicle: Vehicle | None,
    resolved: dict[str, Any],
    share_classes: list[ShareClass] = list(),
    initial_capital: float | dict[str, float] = 1000000.0,
)

A request's settings as the library's objects.

Attributes:

Name Type Description
strategy Callable[[AnyIndexDefinition], Strategy]

Builds the strategy for the path's index definition.

resolved dict[str, Any]

The settings, fully resolved, as the run reports them.

build

build(
    request: BacktestRequest,
    fetcher: DataFetcher | None = None,
) -> tuple[BuiltSettings | None, list[Finding]]

Build a request's settings, or say everything wrong with them.

Parameters:

Name Type Description Default
request BacktestRequest

The backtest request.

required
fetcher DataFetcher | None

The data the run would read, to resolve the modelling assumptions' data treatment; the library defaults when None.

None

Returns:

Name Type Description
tuple BuiltSettings | None

The built settings, or None when any finding was made, and

list[Finding]

the findings.

typed

typed(
    kind: str,
    spec: TypedSpec,
    path: str,
    findings: Findings,
) -> Any

One {type, params} object, built through the catalogue; None, with findings, when it cannot be.

typed_payload

typed_payload(kind: str, value: Any) -> dict[str, Any]

An object as its {type, params} form, read off its attributes.

vehicle_settings

vehicle_settings(vehicle: Vehicle) -> dict[str, Any]

Every setting of vehicle, in the shape vehicle.settings takes.

market_settings

market_settings(market: EtfMarket) -> dict[str, Any]

An ETF market model's settings, by name.

preset_vehicle

preset_vehicle(key: str) -> Vehicle

The vehicle a preset key names; the generic one for generic.

archetype_of

archetype_of(key: str) -> str

Which archetype a preset belongs to.

preset_keys

preset_keys() -> list[str]

Every preset key, the generic vehicle's first.

backtests

Backtest job body and result assembly.

Everything reported here derives from a single canonical series (the portfolio NAV from its initial capital), so the payload is internally consistent by construction rather than by coincidence. A client that recomputes drawdown from the level series, or compounds the annual returns, must land back on the numbers the server sent; if those were computed independently they would drift apart at the last decimal and nobody would know which to trust.

ClassContext dataclass

ClassContext(
    share_classes: Sequence[ShareClass],
    index_currency: str,
    fetcher: DataFetcher,
)

What measuring a run's share classes needs: the classes, for their hedge ratios, the index's currency and the data the rates come from.

assemble_result

assemble_result(
    result: BacktestResult,
    index_result: IndexResult,
    benchmark: RelativeMetricsPayload | None = None,
    cap: float | None = None,
    settings: dict[str, Any] | None = None,
    classes: ClassContext | None = None,
    comparators: Comparators | None = None,
) -> BacktestRunResult

Build the wire payload from a completed backtest.

Parameters:

Name Type Description Default
result BacktestResult

The finished backtest.

required
index_result IndexResult

The index it tracked, reported alongside as the replication reference.

required
benchmark RelativeMetricsPayload | None

Optional comparison against an external benchmark.

None
classes ClassContext | None

What measuring its share classes needs; None without them.

None
comparators Comparators | None

The index and benchmark it is compared against; None leaves the comparisons empty.

None

Returns:

Name Type Description
BacktestRunResult BacktestRunResult

Level, returns, drawdown, annual returns, the

BacktestRunResult

tracked index and metrics, all derived from the same NAV series.

target_cap

target_cap(definition: AnyIndexDefinition) -> float | None

The cap that applies to the run's index.target book, if any.

Read off the built definition rather than the document because an optimised document has no pipeline of its own to read one from, while its target book is its parent's calculation and the parent's cap is the one that shaped those weights. A chain whose immediate parent is itself derived has no cap at that level, and says so.

Parameters:

Name Type Description Default
definition AnyIndexDefinition

The definition the run calculated.

required

compare_against_benchmark

compare_against_benchmark(
    nav: Series, reference: BenchmarkRef, levels: Series
) -> RelativeMetricsPayload

Measure the portfolio against a resolved benchmark.

The benchmark series is rebased on the aligned window rather than its own full history, so both lines start at 100 on the same date and can be read off one axis. Rebasing before alignment would leave the benchmark starting somewhere other than 100 once trimmed.

Parameters:

Name Type Description Default
nav Series

Portfolio NAV series.

required
reference BenchmarkRef

What it is compared against.

required
levels Series

The benchmark's levels, resolved.

required

Returns:

Name Type Description
RelativeMetricsPayload RelativeMetricsPayload

The comparison and the rebased benchmark.

build_backtest_job

build_backtest_job(
    strategy: StrategyDocument,
    document: IndexDocument,
    fetcher: DataFetcher,
    request: BacktestRequest,
    index_store: DocumentStore,
    record_store: DocumentStore | None = None,
    built: BuiltSettings | None = None,
    store: Mapping[str, Any] | None = None,
) -> JobBody

Build the job body that runs one backtest of a stored strategy.

Returned as a closure rather than run inline: the caller submits it to the job registry, which owns scheduling and progress publication.

Parameters:

Name Type Description Default
strategy StrategyDocument

The stored strategy run. Its id and name are stamped on the run's settings, and its record is filed under its id.

required
document IndexDocument

The index it holds, to calculate and then track.

required
fetcher DataFetcher

Data source for both the index and the simulation.

required
request BacktestRequest

Period, capital and cost settings for this run, with the strategy's own strategy and implementation.

required
built BuiltSettings | None

Its strategy, implementation, flows, vehicle and modelling assumptions, already built; built here from request when None.

None

Returns:

Name Type Description
JobBody JobBody

A coroutine function taking a progress reporter.

strategy_and_vehicle_payloads

strategy_and_vehicle_payloads(
    result: BacktestResult,
) -> dict[str, Any]

What a strategy, flows, a vehicle and the implementation's stages add to the run payload; each empty or null when unused.

benchmarks

Resolving a benchmark reference to a level series.

A benchmark arrives as a reference, not data: either the id of a stored index definition (which has to be calculated before it can be compared against) or a market-data identifier whose price series is the benchmark directly.

A stored index of either face qualifies: a benchmark needs only a level series to compare against, and an optimised index has one exactly as a rule-driven one does. It is calculated through whichever path its definition requires, which is the only difference the two faces make here.

resolve_benchmark

resolve_benchmark(
    reference: BenchmarkRef,
    fetcher: DataFetcher,
    index_store: DocumentStore,
    start: str,
    end: str,
) -> pd.Series

Turn a benchmark reference into a date-indexed level series.

Parameters:

Name Type Description Default
reference BenchmarkRef

What to compare against.

required
fetcher DataFetcher

Data source.

required
index_store DocumentStore

Where stored index definitions live.

required
start str

Start date, YYYY-MM-DD.

required
end str

End date, YYYY-MM-DD.

required

Returns:

Type Description
Series

pd.Series: Levels indexed by date. Not rebased: the caller decides,

Series

and returns are scale-invariant anyway.

Raises:

Type Description
DataNotFoundError

If the referenced index or identifier does not exist, or carries no data over the window.

resolve_benchmark_book

resolve_benchmark_book(
    reference: BenchmarkRef,
    fetcher: DataFetcher,
    index_store: DocumentStore,
    start: str,
    end: str,
) -> IndexResult | pd.Series

A benchmark reference as a backtest takes it: a stored index's calculation, which carries its holdings as well as its levels, or a market-data series's levels.

Raises:

Type Description
DataNotFoundError

As resolve_benchmark.

comparisons

A run against its comparators, on the wire: its index and its benchmark, each with the comparator's own figures over the run's window and the run's figures relative to it.

The run is given by its daily returns, the ones its metrics use, so the comparison with its own index agrees with its tracking error and tracking difference.

Comparators dataclass

Comparators(
    index_id: str,
    benchmark: BenchmarkRef | None = None,
    benchmark_levels: Series | None = None,
)

What a run is compared against: its index, by id, and an external benchmark with its resolved levels.

comparison_payloads

comparison_payloads(
    result: BacktestResult,
    index_result: IndexResult,
    comparators: Comparators,
) -> ComparisonsPayload

The run against its index and its benchmark.

compression

Compressing large responses for a client on another machine, and not for one on the same machine, where compressing costs more time than the transfer it saves.

RemoteGZip

RemoteGZip(app: ASGIApp, minimum_size: int = MINIMUM_SIZE)

Gzip responses of at least MINIMUM_SIZE bytes, for clients on another machine only.

config

Configuration for the Beacon API server.

The server is spawned and owned by a desktop client, so its configuration arrives from the command line and the environment rather than from a file.

ServerConfig dataclass

ServerConfig(
    auth_token: str,
    host: str = "127.0.0.1",
    port: int = 0,
    data_fetcher: DataFetcher | None = None,
    market_downloader: Downloader | None = None,
    cors_origins: tuple[str, ...] = DEFAULT_CORS_ORIGINS,
    storage_root: Path | None = None,
    data_store_id: str | None = None,
    data_store_name: str | None = None,
    startup_store: dict[str, Any] | None = None,
)

Settings for a single server process.

Attributes:

Name Type Description
auth_token str

Bearer token required on every request. Never empty: a server with no token would be open to any process on the machine.

host str

Interface to bind. Defaults to loopback and should stay there: the server has no transport security and trusts its bearer token alone.

port int

Port to bind. 0 asks the OS for a free one, which the launcher then reads back and prints.

data_fetcher DataFetcher | None

The data source to serve, or None to run without one.

cors_origins tuple[str, ...]

Exact origins allowed, in addition to the localhost pattern.

market_downloader Downloader | None

Where a store refresh fetches market data from, for a folder store set to refresh from Yahoo Finance. None builds the yfinance-backed downloader on first use, which is the real deployment; tests and offline runs inject their own so the download path is exercisable without a network.

storage_root Path | None

Base directory for persisted documents. None uses the platform app-data location; tests point it at a temporary path.

data_store_id str | None

The registered data store data_fetcher came from, or None.

data_store_name str | None

A name to show for the data being served: the store's name, or where unregistered data came from.

startup_store dict[str, Any] | None

A registered store's record to load in the background once the engine is serving, with /health reporting loading meanwhile, or None.

from_environment classmethod
from_environment(
    token: str | None = None,
    host: str = "127.0.0.1",
    port: int = 0,
    data_fetcher: DataFetcher | None = None,
    cors_origins: tuple[str, ...] | None = None,
    storage_root: Path | None = None,
    data_store_id: str | None = None,
    data_store_name: str | None = None,
    startup_store: dict[str, Any] | None = None,
) -> ServerConfig

Build a config, taking the token from the environment if not given.

Parameters:

Name Type Description Default
token str | None

Explicit token, typically from the command line. When None, the value of BEACON_API_TOKEN is used.

None
host str

Interface to bind.

'127.0.0.1'
port int

Port to bind; 0 asks the OS for a free one.

0
data_fetcher DataFetcher | None

Data source to serve, or None.

None
cors_origins tuple[str, ...] | None

Exact origins to allow. None resolves them from the environment and the defaults.

None
storage_root Path | None

Where saved documents live. None uses the platform app-data location.

None
data_store_id str | None

The registered store the data came from, if any.

None
data_store_name str | None

A name to show for the data being served.

None
startup_store dict[str, Any] | None

A registered store to load once serving.

None

Returns:

Name Type Description
ServerConfig ServerConfig

The assembled configuration.

Raises:

Type Description
ValueError

If no token is available from either source.

resolve_token

resolve_token(token: str | None = None) -> str

The bearer token: token if given, else BEACON_API_TOKEN.

Raises:

Type Description
ValueError

If neither supplies one.

resolve_cors_origins

resolve_cors_origins(
    explicit: list[str] | None = None,
) -> tuple[str, ...]

Find the exact origins this server should allow.

In order: --cors-origin (repeatable), then $BEACON_CORS_ORIGINS as a comma-separated list, then the defaults.

Explicit origins replace the defaults rather than adding to them. An operator narrowing what may call the server should not find two extra origins still permitted: that is the opposite of what configuring it means. The localhost pattern is applied separately by the middleware and is unaffected either way.

Parameters:

Name Type Description Default
explicit list[str] | None

Origins from the command line, or None.

None

Returns:

Name Type Description
tuple tuple[str, ...]

Origins, in the order given, with duplicates removed.

resolve_data_source

resolve_data_source(
    explicit: Path | None = None,
) -> tuple[DataFetcher | None, str]

Find the data source a spawned server should serve.

In order:

  1. --data <path>, passed here as explicit
  2. $BEACON_DATA_PATH
  3. the app-data store, if one has been written there
  4. nothing: the server starts data-less

The two explicit branches fail loudly: asking for a store that cannot be read is a mistake worth stopping for, and starting data-less instead would turn it into a puzzle about why every endpoint that needs data answers 409 NO_DATA_LOADED. The auto-load branch does the opposite and only warns, because a corrupt app-data store must not leave the client unable to start the server that would let it write a new one.

Parameters:

Name Type Description Default
explicit Path | None

Path from the command line, or None.

None

Returns:

Name Type Description
tuple DataFetcher | None

The fetcher (or None), and a sentence naming the branch that

str

ran, for the caller to log. Which branch ran is the first thing anyone

tuple[DataFetcher | None, str]

debugging an empty client will want to know.

constraints

Constraint sets: the stored form of what a portfolio is allowed to be.

One stored row maps to exactly one class in beacon.optimise.constraints. That correspondence is the whole design: a client's constraint editor, the JSON it saves, and the objects the solver receives are the same list in three representations, so there is no translation layer where a rule can quietly change meaning.

Validation happens before the job, not inside it

A malformed constraint set is a bad request, and the client should learn that from the save or the submission rather than from a job that fails a moment later. So the document is validated up front and reports every problem it finds, addressed to the row that caused it. A user fixing a constraint editor needs all the errors, not the first one.

What cannot be checked here is feasibility: whether a set of individually valid constraints can be satisfied together depends on the universe and the data, and the optimiser answers that when it runs. It refuses rather than fudging, so the failure still reaches the client with a message naming what is impossible.

constraint_types_registered

constraint_types_registered() -> set[str]

The constraint types a row may name.

Read off the catalogue rather than a hand-kept table: importing beacon.optimise above is what registers the classes, and adding a constraint means decorating it there and nowhere else.

constraint_params

constraint_params() -> dict[str, set[str]]

Constraint type -> the parameters it accepts.

Read off the constraint classes themselves, so a parameter a class accepts is always accepted here too.

validate_constraint_set

validate_constraint_set(
    document: ConstraintSet,
) -> list[Finding]

Check a constraint set, reporting everything wrong with it.

Parameters:

Name Type Description Default
document ConstraintSet

The set to check.

required

Returns:

Name Type Description
list list[Finding]

Findings, addressed to the row that caused each. Empty when the

list[Finding]

set is well formed, which is not the same as feasible.

validate_constraint_rows

validate_constraint_rows(
    constraints: Sequence[ConstraintRow],
    prefix: str = "constraints",
) -> list[Finding]

Check a list of constraint rows wherever it is carried.

The same rows appear in two documents (a stored constraint set, and an optimised index's derivation), and a client editing either needs identical answers, so there is one checker and the caller only says where the rows live.

Parameters:

Name Type Description Default
constraints Sequence[ConstraintRow]

The rows to check.

required
prefix str

Dotted path the findings are addressed under, e.g. "derivation.constraints".

'constraints'

Returns:

Name Type Description
list list[Finding]

Findings, addressed to the row that caused each.

build_constraints

build_constraints(
    document: ConstraintSet,
) -> list[Constraint]

Turn a stored set into optimiser constraint objects.

Parameters:

Name Type Description Default
document ConstraintSet

A set that has already been validated.

required

Returns:

Name Type Description
list list[Constraint]

The constraints, in the order the rows carry them. Order matters

list[Constraint]

only for reporting, since the solver applies them all at once.

Raises:

Type Description
CalculationError

If a row's type is unknown, which validation would have caught. Reaching here means the set was never validated.

build_constraint_rows

build_constraint_rows(
    constraints: Sequence[ConstraintRow],
) -> list[Constraint]

Turn constraint rows into optimiser constraint objects.

Parameters:

Name Type Description Default
constraints Sequence[ConstraintRow]

Rows that have already been validated, from a stored set or from an optimised index's derivation.

required

Returns:

Name Type Description
list list[Constraint]

The constraints, in the order the rows carry them.

has_errors

has_errors(findings: list[Finding]) -> bool

Whether any finding blocks saving or running.

label_map

label_map(document: ConstraintSet) -> dict[str, str]

Constraint label to the row id that produced it.

The optimiser reports binding constraints by their own generated labels, such as "maximum weight 10.0000% on AAA", which say what bound but not which row of the editor to highlight. Building the same objects in the same order and reading their labels back gives the mapping, without the optimiser needing to know that a stored document exists.

constraint_types

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

Every constraint type and the parameters it accepts.

Served so a client can build its editor from the same source the solver reads, rather than from a copy that drifts.

data_stores

Named data stores: which exist, which one the engine serves, and loading it.

A data store holds one dataset. Users name their stores ("Synthetic data", "My data"), one is active at a time, and the engine remembers which, so the next start serves the same data. A store is a py-beacon data folder or a Postgres database, and each can be refreshed from its own source (refresh_plan).

The registry is saved with the engine's other documents (indices, universes), so it lives wherever --documents points.

What the engine serves at startup

In order:

  1. --data <path>: a folder named on the command line.
  2. $BEACON_DATA_PATH.
  3. The active registered store.
  4. If nothing is registered yet but a store exists in the default app-data folder, it is registered as "Synthetic data" (or by its source) and made active, so an existing app-data store is served without any setup.
  5. Nothing: the engine starts empty, and a store can be loaded later.

The first two fail loudly, because naming data that cannot be read is a mistake worth stopping for. The active store only warns and starts empty, so a store that was moved or damaged never stops the engine from starting and offering another.

StoreRecord

Bases: BaseModel

A registered store, as saved. Which store is active is saved here too, as a flag on its record, so the registry is one collection with nothing beside it to fall out of step.

StoreRegistry

StoreRegistry(root: Path | None = None)

The registered data stores and which one is active.

Parameters:

Name Type Description Default
root Path | None

Where the engine keeps its documents. None uses the platform app-data location, as every other collection does.

None
listing
listing() -> tuple[list[dict[str, Any]], SkipCounts]

Every readable store in name order, and what was skipped and why.

records
records() -> list[dict[str, Any]]

Every readable store, in name order.

get
get(store_id: str) -> dict[str, Any] | None

One store's record, or None when there is none or it can't be read.

find_by_path
find_by_path(
    path: Path | str, kind: str = "folder"
) -> dict[str, Any] | None

The store registered for this folder or database, if any.

new_id
new_id(name: str) -> str

An unused id derived from name.

A second store with the same name gets a numbered id ("my-data-2"), so names never have to be unique, only ids.

create
create(
    name: str,
    path: Path | str,
    kind: str = "folder",
    managed: bool = False,
    store_id: str | None = None,
    connection: dict[str, Any] | None = None,
    refresh_from: str = "source",
) -> dict[str, Any]

Register a store, with an id derived from its name unless given.

path is a folder for a folder store; for a database it is the connection described without its password.

update
update(
    store_id: str,
    name: str | None = None,
    refresh_from: str | None = None,
) -> dict[str, Any] | None

Change a store's display name or refresh source. Its id stays.

delete
delete(store_id: str) -> None

Forget a store. Its folder is left exactly as it is.

active_id
active_id() -> str | None

The store the engine last served, or None.

If more than one record is flagged, as a crash between two writes in set_active could leave it, the most recently loaded one wins.

set_active
set_active(store_id: str | None) -> None

Remember which store to serve, including across restarts.

The new store is flagged before the old one is cleared, so a crash in between leaves two flagged rather than none, and active_id resolves that.

mark_loaded
mark_loaded(store_id: str) -> None

Record that a store was just loaded.

StartupData dataclass

StartupData(
    fetcher: DataFetcher | None,
    store_id: str | None,
    store_name: str | None,
    origin: str,
    pending: dict[str, Any] | None = None,
)

What the engine serves when it starts, and a line saying why.

load

load(
    record: dict[str, Any], **settings: Any
) -> DataFetcher

Read a registered store into a fetcher.

Parameters:

Name Type Description Default
record dict[str, Any]

The store's registry record.

required
**settings Any

Passed to the loader: fx_policy, max_price_staleness_days, free_float_backfill_days.

{}

Raises:

Type Description
ConfigurationError

If the store cannot be read.

postgres_source

postgres_source(
    record: dict[str, Any],
) -> postgres.PostgresSource

The database a Postgres store's record points at.

refresh_plan

refresh_plan(
    record: dict[str, Any],
) -> tuple[RefreshAction | None, str]

What refreshing a store would do, and if nothing, why not.

Returns:

Name Type Description
tuple RefreshAction | None

The action ("extend", "reread" or "download"), or None with a

str

sentence saying why the store has nothing to refresh.

available

available(record: dict[str, Any]) -> bool

Whether a store can be loaded now, as far as can be told cheaply.

A folder must hold a store. A database is only checked for its password variable: connecting on every listing would make the list as slow as the slowest database in it, so a database that is down is found when it is loaded.

describe

describe(
    record: dict[str, Any], active_id: str | None
) -> DataStore

A store's listing row, read without loading it.

resolve_startup

resolve_startup(
    explicit: Path | None,
    root: Path | None,
    load_now: bool = True,
) -> StartupData

Find the data to serve at startup. See the module docstring for the order.

Parameters:

Name Type Description Default
explicit Path | None

--data from the command line, or None.

required
root Path | None

Where the engine keeps its documents (--documents).

required
load_now bool

Whether to load a registered store here. False leaves it in pending, for the engine to load once it is serving. Explicit data is loaded here either way, so a path that cannot be read still stops the engine at once.

True

Raises:

Type Description
ConfigurationError

If --data or $BEACON_DATA_PATH names data that cannot be read.

definitions

Index definition documents: validation and materialisation.

A stored definition is a JSON document describing a rule pipeline. It is not an IndexDefinition: the library object takes constructed rule and scheme instances, which JSON cannot carry. This module owns both directions: checking a document and turning a valid one into the library object.

Validation collects findings rather than raising at the first problem. A user editing a pipeline needs every issue at once, each addressable to the rule that caused it, not a single exception naming whichever one failed first.

selection_rules

selection_rules() -> dict[str, set[str]]

Selection rule name -> the parameters it accepts.

weighting_schemes

weighting_schemes() -> dict[str, set[str]]

Weighting scheme name -> the parameters it accepts.

validate_document

validate_document(document: IndexDocument) -> list[Finding]

Collect every finding for a definition document.

Parameters:

Name Type Description Default
document IndexDocument

The definition to check, of either face. Which checks run is decided by derivation, the same discriminator a client branches on.

required

Returns:

Type Description
list[Finding]

list[Finding]: Every problem found, each carrying the path and, where

list[Finding]

applicable, the id of the rule responsible. Empty when the definition

list[Finding]

is valid and unremarkable; warnings alone do not block saving.

has_errors

has_errors(findings: list[Finding]) -> bool

Whether any finding blocks saving.

build_index_definition

build_index_definition(
    document: IndexDocument,
) -> IndexDefinition

Materialise a valid rule-driven document into an IndexDefinition.

Parameters:

Name Type Description Default
document IndexDocument

A document that has already passed validate_document() without errors, carrying a rule pipeline.

required

Returns:

Name Type Description
IndexDefinition IndexDefinition

The library object, ready for IndexCalculator.

Raises:

Type Description
InvalidRuleError

If the document is optimiser-derived. An OptimisedIndexDefinition is not an IndexDefinition (the calculator must never receive one by accident), so callers that can handle either use :func:build_definition.

ValueError

If the document is invalid after all. The library's own constructor validation is the final word.

build_definition

build_definition(
    document: IndexDocument, documents: DocumentStore
) -> AnyIndexDefinition

Materialise a document of either face into a library definition.

A rule-driven document builds an :class:IndexDefinition; an optimiser-derived one builds an :class:~beacon.index.derived.OptimisedIndexDefinition over its source, resolved through the store. This is recursive, so a chain of derivations builds a chain of definitions and the whole thing calculates through one path.

Parameters:

Name Type Description Default
document IndexDocument

The stored definition to materialise.

required
documents DocumentStore

Where the sources of a derivation are read from.

required

Returns:

Name Type Description
AnyIndexDefinition AnyIndexDefinition

The library object, ready for

AnyIndexDefinition

class:~beacon.backtest.main.Backtest.

Raises:

Type Description
DataNotFoundError

If a derivation names a source that is not stored.

InvalidRuleError

If a derivation chain returns to itself, or runs deeper than :data:MAX_DERIVATION_DEPTH.

derivative_records

What a fund's swap, index futures and dividends owed add to a run's result.

A synthetic fund's swap: its value and the counterparty exposure it makes each day, and each reset. A fund's index futures, whether it holds the index through them or equitises its cash buffer: their exposure each day, in money and as a share of NAV, and each roll. Every series is on the run's days, and everything is empty or null for a fund without them, so a run without them answers as before.

derivative_payloads

derivative_payloads(
    result: BacktestResult, days: Index
) -> dict[str, Any]

The swap and futures records a run's payload carries.

derivatives

Stateless derivatives pricing.

Nothing here reads a stored document or writes one. A request carries every input it needs and the response is a pure function of it, which is what makes these endpoints safe to call repeatedly from a form as someone types, and what lets a test assert that the storage directory is untouched afterwards.

The term-structure and roll reads are the exception only in that they resolve a price from the data source. They still write nothing.

A futures fair value is S·e^((r − q + c)·T). The response splits the carry into its financing, dividend and borrow components, each expressed as the price effect it contributes rather than as a rate: "financing adds 1.24" is a sentence about this contract, while "r is 5%" is a sentence about the world.

price_futures

price_futures(
    request: FuturesPriceRequest,
) -> FuturesPriceResponse

Value a futures contract and decompose its carry.

Parameters:

Name Type Description Default
request FuturesPriceRequest

Every input the calculation needs.

required

Returns:

Name Type Description
FuturesPriceResponse FuturesPriceResponse

Fair value, the carry split into parts, contract

FuturesPriceResponse

value, and a tenor x rate grid.

price_trs

price_trs(request: TrsPriceRequest) -> TrsPriceResponse

Value a total return swap and schedule its financing.

Parameters:

Name Type Description Default
request TrsPriceRequest

Trade terms, legs and valuation inputs.

required

Returns:

Name Type Description
TrsPriceResponse TrsPriceResponse

Financing schedule, present value, fair spread,

TrsPriceResponse

breakeven table and DV01.

build_term_structure

build_term_structure(
    index_id: str,
    fetcher: DataFetcher,
    expiries: list[str],
    as_of: str | None,
    risk_free_rate: float,
    dividend_yield: float,
) -> TermStructureResponse

Price a strip of futures on an index, off its own spot.

Raises:

Type Description
DataNotFoundError

If the index cannot be priced.

build_roll

build_roll(
    index_id: str,
    fetcher: DataFetcher,
    front_expiry: str,
    back_expiry: str,
    as_of: str | None,
    risk_free_rate: float,
    dividend_yield: float,
) -> RollResponse

The cost or gain of rolling from one contract to the next.

Both legs are priced theoretically off the same spot and curve, so the roll reported here is the carry roll rather than a market one. With a flat curve it is positive in backwardation and negative in contango, which is the sign convention a desk expects.

documents

Reading stored documents: tolerant listings, strict details.

A listing answers "what is there". A document the server cannot parse or validate is skipped, with a WARNING naming the id and the fault, and the rest of the collection is served; the response counts what was left out and why.

A detail route answers "give me this one". A document the server cannot read gets the same 404, envelope and pointer as an absent one, and the underlying fault is logged at WARNING rather than returned, because it is about this server's storage and not about the request. Listings and detail routes share one definition of "unreadable", so they cannot disagree about what exists.

Write paths use stored, which keeps "is something stored here" and "can it be read" apart, so a corrupt document can still be deleted or replaced.

SkipCounts dataclass

SkipCounts(
    unparseable: int = 0,
    from_newer_build: int = 0,
    unrecognised: int = 0,
)

How many documents a listing left out, and why.

The three causes call for different responses, so they are counted separately.

Attributes:

Name Type Description
unparseable int

Not valid JSON, or a schema version nothing can migrate. The file itself is damaged; restore or remove it.

from_newer_build int

Written by a newer py-beacon than the one reading it. Nothing is wrong with the file; upgrade the engine.

unrecognised int

Valid JSON that this build's model does not accept. The document and the engine disagree about its shape -- usually a version gap the schema migrations do not cover.

total property
total: int

Every document left out, whatever the cause.

Stored dataclass

Stored(
    document: T | None = None,
    fault: Exception | None = None,
)

Bases: Generic[T]

What a collection holds under one id: both questions, answered once.

Three states, and every write path branches on some pair of them: absent (nothing stored), present-but-unreadable (a file is there and the server cannot interpret it), and readable (here is the document). A bool alone cannot express the middle one, which is precisely the state that made a corrupt document undeletable.

Built from a single store.read, so present cannot contradict the document the same call returned -- an exists followed by a read can.

Attributes:

Name Type Description
document T | None

The built document, or None when absent or unreadable.

fault Exception | None

Why it could not be read; None when it read, and also None when nothing is stored. fault is not None is "present but unreadable".

present property
present: bool

Whether something is stored under this id, readable or not.

readable property
readable: bool

Whether the stored document could be read and built.

warn_guard_skipped
warn_guard_skipped(guard: str, describe: str) -> None

Log a guard that this document's fault made impossible to run.

A no-op when the document is absent or readable: there is no guard to skip in either case.

The log line is the only record, and it has to name both halves -- which check did not run, and why -- because the request succeeds and the response says nothing about it. Skipping is the lesser evil (a guard that cannot run must not make a document permanently unfixable), but it is still a guard that did not run.

Parameters:

Name Type Description Default
guard str

The check that was skipped, as a noun phrase.

required
describe str

The document it would have guarded, e.g. "universe 'tech'".

required

validated

validated(
    model: type[ModelT],
) -> Callable[[str, dict[str, Any]], ModelT]

A build function that validates a document against model.

The common case: a document that carries its own identity, so the id the file was found under adds nothing and is ignored.

Parameters:

Name Type Description Default
model type[ModelT]

The response model the stored document must satisfy.

required

Returns:

Name Type Description
Callable Callable[[str, dict[str, Any]], ModelT]

(document_id, document) -> model instance.

raw

raw(
    document_id: str, document: dict[str, Any]
) -> dict[str, Any]

A build function that hands back the stored dict unchanged.

For a caller that wants the document as stored rather than as a model: the delete cascade reads derivations off raw dicts, and a guard that only reads one key has no reason to hold the whole document to a model.

stored

stored(
    store: DocumentStore,
    document_id: str,
    build: Callable[[str, dict[str, Any]], T],
) -> Stored[T]

Ask what a collection holds under an id, without conflating the answers.

The write-path counterpart to load_document: where a read turns an unreadable document into a not-found, a write needs the distinction kept, so this one refuses nothing and reports.

Parameters:

Name Type Description Default
store DocumentStore

The collection to look in.

required
document_id str

Identifier of the document.

required
build Callable[[str, dict[str, Any]], T]

Turns (document_id, document) into whatever the caller needs -- raw for the stored dict, validated(Model) for a model.

required

Returns:

Name Type Description
Stored Stored[T]

The document, or the fault that stopped it being read.

read_collection

read_collection(
    store: DocumentStore,
    build: Callable[[str, dict[str, Any]], T],
    describe: str,
) -> tuple[list[T], SkipCounts]

Read every document in a collection, skipping the unreadable ones.

Parameters:

Name Type Description Default
store DocumentStore

The collection to read.

required
build Callable[[str, dict[str, Any]], T]

Turns (document_id, document) into the listing's row. validated covers the usual case.

required
describe str

What these documents are, for the log line, singular: "universe", "index definition".

required

Returns:

Name Type Description
tuple list[T]

(rows, skipped). The counts are published by the response

SkipCounts

model rather than only logged: a picker silently short by three is

tuple[list[T], SkipCounts]

indistinguishable from a correct one, and the server is the only side

tuple[list[T], SkipCounts]

that knows. Broken down by cause, because this side also knows why,

tuple[list[T], SkipCounts]

and the client cannot work it out.

load_document

load_document(
    store: DocumentStore,
    document_id: str,
    build: Callable[[str, dict[str, Any]], T],
    describe: str,
    source: str = "DocumentStore",
) -> T

Read one document, answering not-found when it cannot be read.

Parameters:

Name Type Description Default
store DocumentStore

The collection to read from.

required
document_id str

Identifier of the document.

required
build Callable[[str, dict[str, Any]], T]

Turns (document_id, document) into the response model.

required
describe str

The subject of the not-found message, e.g. "universe 'tech'".

required
source str

The pointer the not-found message carries. Defaults to the store; a route with somewhere better to send the client (running a backtest, say) passes its own.

'DocumentStore'

Returns:

Type Description
T

Whatever build returns.

Raises:

Type Description
DataNotFoundError

If the document is absent, is not valid JSON, or does not satisfy its model. One answer for all three, because a client can act on none of them differently.

slug

slug(name: str) -> str

Derive a document id from a display name.

Shared by universes and data stores, so both turn a name into an id the same way.

Lower-cased, runs of non-alphanumerics collapsed to one dash, trimmed: "My Tech Names!" becomes "my-tech-names", which is what appears in the URL.

Returns an empty string when nothing survives. A name of pure punctuation has no identifier, and the caller refuses it rather than inventing one.

errors

Exception to HTTP mapping.

Every library exception is registered here, in one place, so a new one cannot reach a client as an unlabelled 500. Codes are part of the API contract: clients branch on them, so they must stay stable even if the message changes.

FindingsError

FindingsError(
    subject: str, reason: str, findings: list[Finding]
)

Bases: InvalidRuleError

A refusal that carries every finding, not one message for the form.

Subclasses InvalidRuleError for its 422 / INVALID_RULE mapping, with no registration of its own. The findings ride along as an attribute, which _beacon_detail puts into the envelope's detail, so a client can point at each bad rule, member or row. Index pipelines, universes, feature imports and constraint sets all refuse through this one class.

Parameters:

Name Type Description Default
subject str

What was refused, e.g. "universe" or "index definition 'X'". Published as rule_description.

required
reason str

Why, in a phrase. Published as reason.

required
findings list[Finding]

Every problem found.

required

classify

classify(exc: BeaconError) -> tuple[int, str]

Map a library exception to its HTTP status and stable code.

Parameters:

Name Type Description Default
exc BeaconError

The raised library exception.

required

Returns:

Name Type Description
tuple int

(http_status, code). Unregistered BeaconError subclasses

str

fall through to the catch-all rather than escaping as a bare 500.

failure_envelope

failure_envelope(
    exc: BaseException, calculation: str
) -> dict[str, Any]

The {code, message, detail} an exception publishes, without a response.

For a caller that has to record a failure rather than answer a request: a background job, whose caller received 202 long before anything went wrong. A deliberate refusal and a crash therefore reach the client with different codes, exactly as they would over HTTP.

The ladder matches register_exception_handlers deliberately, so the same exception carries the same code whichever way it travels. That includes ValueError -> INVALID_ARGUMENT, with the tension noted on ARGUMENT_CODE above and one wrinkle of its own: inside an accepted job a ValueError is less certainly the caller's fault than it is at the request boundary, because the request was already validated. It is still the likelier reading (the job body is where a stored document becomes a definition, and that is exactly where a bad stored value surfaces), and one code for one exception is worth more than a second judgement call in a second place.

Anything else is wrapped as an UnexpectedCalculationError rather than given a code of its own. A fault that escaped every guard IS that case, so it publishes UNEXPECTED_CALCULATION_FAILURE and carries original_type exactly as one raised in the library would. Reaching for the BEACON_ERROR catch-all instead would put a crash under the code an unregistered refusal gets, so a client could not tell the two apart.

Parameters:

Name Type Description Default
exc BaseException

Whatever the job raised.

required
calculation str

What was being computed, for the wrap's calculation name. A job's kind is the honest answer: it is what the work was.

required

Returns:

Name Type Description
dict dict[str, Any]

The ErrorDetail body: code, message and optional detail.

dict[str, Any]

Not the outer envelope: a job records the detail, not a response.

register_exception_handlers

register_exception_handlers(app: FastAPI) -> None

Attach the handlers that put every error into the envelope.

Parameters:

Name Type Description Default
app FastAPI

The application to register on.

required

holding_reference

Reference columns on the rows that name a holding, as of each row's date.

The caller chooses the columns with reference_fields, a comma-separated list of reference columns. Without it a row carries a default set: the name, the asset class, the sector and the dataset's country column. Each row's columns are read as they stood on that row's date (see beacon.data.reference_as_of), so a past holding shows the classification of its time, not today's.

A column the dataset lacks is not a mistake when the API documents it: the columns that say what a series is answer as the engine reads them (a missing ASSET_CLASS is Equity), and the descriptive columns answer null. Any other column the dataset lacks is refused, naming the ones it has.

HoldingReference

HoldingReference(
    fetcher: DataFetcher | None,
    identifiers: Iterable[str],
    columns: Sequence[str],
)

Reference columns for the names a response names, on their dates.

Parameters:

Name Type Description Default
fetcher DataFetcher | None

The loaded data; None puts no columns on the rows.

required
identifiers Iterable[str]

Every name the response will ask about.

required
columns Sequence[str]

The chosen columns; empty puts none on the rows.

required
of
of(identifier: str, date: str) -> dict[str, Any] | None

One name's columns on date; None when none were chosen or the name had no reference row then.

windows_of
windows_of(
    identifiers: Iterable[str], start: str, end: str
) -> dict[str, list[dict[str, Any]]] | None

Each name's windows over a span; None when none were chosen.

map_of
map_of(
    identifiers: Iterable[str], date: str
) -> dict[str, dict[str, Any] | None] | None

Several names' columns on one date; None when none were chosen.

chosen_columns

chosen_columns(
    fetcher: DataFetcher, reference_fields: str | None
) -> list[str]

The reference columns a request asks for, as stored (upper case).

Raises:

Type Description
InvalidRuleError

If a column is neither in the dataset nor one the API documents.

holding_reference

holding_reference(
    fetcher: DataFetcher | None,
    reference_fields: str | None,
    identifiers: Iterable[str],
) -> HoldingReference

The reference columns a request asks for, for the names it will name.

A response that can be read without data loaded, such as a saved run, carries no columns then, unless some were asked for by name.

Raises:

Type Description
NoDataLoadedError

If columns were asked for by name and no data is loaded to read them from.

InvalidRuleError

If a column is unknown (see chosen_columns).

holdings

A strategy's holdings workflow at a date, one row per name, from the universe to the shares held.

What a run decided at each rebalance is saved with it, inside the strategy's record under a key of its own: how each rebalance's universe narrowed (the index's selection), each name's weight after the implementation's screens, and its target and final weights, and the book's units held, recorded only on the days they change, with its daily NAV. The view joins that to the run's own index and strategy weights: the index weights before and after the index's cap, the strategy's choice, each implementation stage, the weight traded to, and, as of the date asked about, the weight held and its drift.

A held weight is the units held times the day's price in the book's currency, over the day's NAV: what the book's own record says, without storing every day's weights.

holdings_document

holdings_document(
    result: BacktestResult, index_result: IndexResult
) -> dict[str, Any]

What a run decided at each rebalance, for its holdings workflow.

build_holdings

build_holdings(
    strategy_id: str,
    index: IndexDocument,
    run: dict[str, Any],
    document: dict[str, Any] | None,
    fetcher: DataFetcher,
    date: str | None,
    reference: HoldingReference,
    benchmark_on: Callable[[str], dict[str, float] | None],
    universe_on: Callable[
        [list[str], str], dict[str, float | None]
    ]
    | None = None,
    universe_name: str | None = None,
) -> HoldingsView

The workflow in force on date, the run's last day when None.

benchmark_on gives the run's benchmark index's weights on a rebalance date, or None for a run without one; universe_on the universe's weights of some names on a date.

Raises:

Type Description
DataNotFoundError

If date is before the run's first rebalance.

holdings_schemas

Response models for a strategy's holdings workflow at a date.

HoldingStage

Bases: BaseModel

One step of the workflow, in order.

HoldingRow

Bases: BaseModel

One name's way through the workflow at the rebalance in force.

HoldingsView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/holdings.

index_level

An index on its own: GET /indices/{index_id}/level.

The index calculated alone, with no portfolio, costs or vehicle: its level, the headline figures of that level, and its weights at each rebalance. Through the same cache a backtest uses, so a backtest over the same dates reuses the calculation, and the other way round.

IndexLevelView

Bases: BaseModel

Response of GET /indices/{index_id}/level.

calculate_index

calculate_index(
    document: IndexDocument,
    index_store: DocumentStore,
    fetcher: DataFetcher,
    start: str | None = None,
    end: str | None = None,
) -> IndexResult

A stored index calculated alone, over the loaded data.

Parameters:

Name Type Description Default
document IndexDocument

The stored definition; a derived one's source is resolved through index_store.

required
index_store DocumentStore

Where stored definitions live.

required
fetcher DataFetcher

The loaded data.

required
start str | None

First date; the definition's base date when None.

None
end str | None

Last date; the last date the data covers when None.

None

build_index_level

build_index_level(
    document: IndexDocument, result: IndexResult
) -> IndexLevelView

The view of a calculated index.

job_threads

Running a job's work in a worker thread, so the event loop stays free.

A job body is a coroutine the registry runs as a task on the event loop. A backtest, an optimisation or a risk model is minutes of synchronous pandas and numpy work, and run inside that coroutine it holds the loop for all of it: no request is answered, /health included, and the submit's own 202 waits too, because the task starts before the response goes out.

run_in_thread hands the work to a worker thread with a plain progress function. A report from the thread is passed to the loop to publish, and once the job is cancelled the next report stops the work, so a cancelled run does not go on to write its results.

JobCancelledError

Bases: Exception

Raised in the worker thread at its next report after a cancellation.

run_in_thread async

run_in_thread(
    report: ProgressReporter,
    work: Callable[[Report], Result],
) -> Result

Run work in a worker thread, publishing its reports on the loop.

Parameters:

Name Type Description Default
report ProgressReporter

The job's progress reporter.

required
work Callable[[Report], Result]

The synchronous work, called with a progress function.

required

Returns:

Type Description
Result

What work returns.

Raises:

Type Description
CancelledError

When the job is cancelled. The thread itself cannot be interrupted, so it stops at its next report instead.

jobs

In-process job registry for long-running work.

A backtest or an optimisation takes long enough that holding an HTTP connection open for it is the wrong shape: the client wants to submit, get an id back, and either poll or listen. Jobs run as asyncio tasks in the server process. There is no queue, no broker and no persistence, which suits a single local process owned by one desktop client. Restarting the server loses in-flight jobs, and that is the correct trade for this deployment.

Every state change is published to subscribers, so the WebSocket feed and polling see the same thing.

Job dataclass

Job(
    id: str,
    kind: str,
    status: str = PENDING,
    progress: float = 0.0,
    message: str = "",
    result: Any = None,
    error: dict[str, Any] | None = None,
    _task: Task[Any] | None = None,
)

A unit of background work and its observable state.

is_terminal property
is_terminal: bool

Whether this job has finished, failed or been cancelled.

snapshot
snapshot() -> dict[str, Any]

The public view of this job.

The result is only carried once the job has succeeded: sending a half-built result would invite a client to use it.

JobRegistry

JobRegistry(result_store: DocumentStore | None = None)

Owns running jobs and the subscribers watching them.

Parameters:

Name Type Description Default
result_store DocumentStore | None

Where completed results are persisted. None keeps everything in memory, which is what the unit tests want and what a process with nowhere to write falls back to.

None
results property
results: DocumentStore | None

Where completed results are persisted, or None when nowhere is.

Read-only, and exposed for exactly one caller: GET /jobs reads this collection through documents.read_collection the way every other listing reads its own. The model half of "unreadable" has to be applied where the model is known, and JobStatus is a wire model. The registry is deliberately free of the wire layer, so the listing cannot be assembled in here without dragging schemas down with it.

What stays in here is the bookkeeping the registry owns and no route can express: retention, the cascade delete, and the latest-result queries. Those decide skip-or-see for themselves; see _prune.

subscribe
subscribe() -> asyncio.Queue[dict[str, Any]]

Register a subscriber and return its event queue.

unsubscribe
unsubscribe(queue: Queue[dict[str, Any]]) -> None

Remove a subscriber.

publish
publish(event: dict[str, Any]) -> None

Send an event to every subscriber, dropping the oldest if full.

Deliberately synchronous and non-blocking: a job reporting progress must never await a slow reader.

publish_data_loaded
publish_data_loaded(
    store_id: str, name: str, data_version: str
) -> None

Announce that the engine now serves a different data store.

A client holding anything derived from the data (lists of names, coverage, previews) should refetch it: every dataset may have changed at once.

publish_data_freshness
publish_data_freshness(
    dataset: str, detail: dict[str, Any] | None = None
) -> None

Announce that a dataset's contents may have changed.

get
get(job_id: str) -> Job | None

Return an in-memory job by id, or None.

Only jobs this process ran. Use :meth:snapshot to include results persisted by an earlier process.

snapshot
snapshot(job_id: str) -> dict[str, Any] | None

Return a job's state, from memory or from disk.

A job this process ran is authoritative; otherwise the persisted result of an earlier process is served, which is what lets a completed backtest survive a restart and still be readable.

Parameters:

Name Type Description Default
job_id str

Identifier of the job.

required

Returns:

Type Description
dict[str, Any] | None

dict or None: The snapshot, or None if the job is unknown to both.

latest_result
latest_result(kind: str) -> dict[str, Any] | None

The result of the most recent successful job of a kind.

Read from the store rather than from memory. Every terminal job is persisted, so the store is the complete record, and it is the only one of the two that survives a restart, which is the case this exists to serve.

Parameters:

Name Type Description Default
kind str

The job kind, e.g. "backtest:my-index".

required

Returns:

Type Description
dict[str, Any] | None

dict or None: The stored result, or None when nothing of that kind

dict[str, Any] | None

has succeeded.

forget
forget(kind: str) -> int

Drop every job and persisted result of one kind.

The cascade half of deleting the thing a kind is keyed to: an index's backtest results go with its definition, rather than surviving under an id that does not resolve.

Parameters:

Name Type Description Default
kind str

The exact kind, e.g. "backtest:my-index". Exact rather than a prefix, so backtest:core cannot take backtest:core-hedged with it.

required

Returns:

Name Type Description
int int

How many records went (in-memory jobs plus persisted

int

results), so the caller can log what the delete cost.

latest_results_by_kind
latest_results_by_kind(
    prefix: str,
) -> dict[str, dict[str, Any]]

The newest successful result for every kind under a prefix.

Reads the store, which holds every terminal job including the ones this process ran. The /jobs listing deliberately excludes those so a job does not appear twice, and filtering the same way here would hide every model the running process had just estimated, which is most of them.

Parameters:

Name Type Description Default
prefix str

Kind prefix including its separator, e.g. "risk:".

required

Returns:

Name Type Description
dict dict[str, dict[str, Any]]

Kind to its newest result.

list_jobs
list_jobs() -> list[Job]

Every job this process knows about.

submit
submit(kind: str, body: JobBody) -> Job

Start a job and return it immediately.

Parameters:

Name Type Description Default
kind str

Label for what this job is, e.g. "backtest".

required
body JobBody

Coroutine function taking a progress reporter.

required

Returns:

Name Type Description
Job Job

The registered job, already scheduled.

Raises:

Type Description
RuntimeError

If called off the event loop. FastAPI runs a sync endpoint in a worker thread, where there is no loop to attach a task to, so a submitting endpoint must be async def. The bare failure is an opaque "no running event loop", hence the explicit check.

cancel
cancel(job_id: str) -> bool

Request cancellation of a job.

Parameters:

Name Type Description Default
job_id str

Identifier of the job.

required

Returns:

Name Type Description
bool bool

True if cancellation was requested, False if the job is

bool

unknown or already finished.

drain async
drain() -> None

Await every outstanding task. For shutdown and for tests.

market_caps

Each name's market cap at a date, local and converted, and how old it is.

A point-in-time cap is a price times a share count, both of which move, so it is a market-data join rather than a stored attribute. /data/reference publishes it as a derived field and the index preview beside each constituent; both call market_caps, so the two never disagree.

market_caps

market_caps(
    fetcher: DataFetcher,
    identifiers: list[str],
    requested: set[str],
    end: Timestamp,
    currency: str,
) -> dict[str, dict[str, Any]]

Market capitalisation per name, local and converted into currency.

A point-in-time cap is a price times a share count, both of which move, so this is a market-data join rather than a stored attribute -- which is why it is derived and has to be asked for by name.

uses_free_float

uses_free_float(
    scheme: str | None, params: dict[str, Any] | None
) -> bool

Whether an index weights by free-float market cap: cap-weighted with free float applied.

universe_weights

universe_weights(
    caps: dict[str, dict[str, Any]],
    names: list[str],
    free_float: bool,
) -> dict[str, float | None]

Each name's share of the universe's total market cap.

A universe has no weighting of its own, so its weights are its names' market caps over their total: free-float caps where the index weights by them, full caps otherwise. None for a name with no cap that day.

market_inputs

Each data store's market inputs, kept once for every run on it.

A backtest's run keeps the market inputs it read in its data store's shared folder (beacon.analysis.shared_inputs), under market_inputs/<store id> in the server's storage, and only the list of pieces it used beside its record. A run on data from no registered store keeps its inputs itself, as before. The bookkeeping is beacon.analysis.shared_runs'; this gives it the stored runs.

  • Merging. A folder holding more than 20 files is merged into one file per table, dropping the pieces no run uses.
  • Deleting a store. The runs on it keep their pieces, and the folder is marked as kept for a deleted store, which such a run says when it opens; or, with cascade, the runs are deleted, and the folder with them.
  • Runs stored before. A run that kept its inputs itself is moved to the shared layout when the server starts, under unattributed, and reads the same.

root_of

root_of(records: DocumentStore) -> Path

The storage root the records are kept under, where the shared folders are.

stored_runs

stored_runs(records: DocumentStore) -> dict[str, Path]

Each stored strategy's run folder.

shared_for

shared_for(
    records: DocumentStore, store: Mapping[str, Any] | None
) -> SharedInputs | None

The shared folder of the data store store (its id), or None for data from no registered store.

merge_if_due

merge_if_due(
    records: DocumentStore,
    shared: SharedInputs,
    store: Mapping[str, Any],
) -> bool

Merge shared when it holds too many files; whether it did.

forget_store

forget_store(
    records: DocumentStore,
    store: Mapping[str, Any],
    cascade: bool = False,
) -> dict[str, Any]

Deleting the data store store: the runs on it keep their market inputs, or with cascade are deleted.

Returns:

Name Type Description
dict dict[str, Any]

kept and deleted, the strategies whose runs were kept

dict[str, Any]

and deleted, and the warning said when runs were kept.

warnings_of

warnings_of(folder: Path) -> list[str]

What a stored run says as it opens.

migrate

migrate(records: DocumentStore) -> list[str]

Move the stored runs that keep their market inputs themselves to the shared layout; the strategies moved.

methods

Answer 405 for a method a fixed path does not support.

A fixed path such as /indices/validate sits beside a parameterised one such as /indices/{index_id}. The router alone would match PUT /indices/validate against the parameterised path with index_id="validate", and the client would be told its request was malformed when the truth is that the verb does not exist there. The published spec lists every method each fixed path supports, so this reads the spec once and, for a fixed path, refuses with 405 any method the spec does not list for it. A fixed path with no parameterised sibling gets the same 405 as always.

A plain OPTIONS request, one that is not a CORS preflight, gets the same 405 on every path in the spec, parameterised ones included, with Allow listing every method the path supports. The router alone listed only the methods of the first route it matched.

LiteralPathMethods

LiteralPathMethods(app: ASGIApp, api: FastAPI)

ASGI middleware refusing undocumented methods on fixed paths.

allowed
allowed() -> dict[str, frozenset[str]]

Methods per fixed path, read from the spec on first use.

Lazily, because routers are mounted after middleware is added, and the spec is complete only once they are.

templated
templated(path: str) -> frozenset[str] | None

Methods of the parameterised spec path path matches, if any.

optimisation

Running an optimisation, and reading the frontier and exposures off it.

A solve is fast; estimating the risk model it needs is not, because that means pulling a price history for every name and building a covariance. So a run is a job like a backtest, and the result carries enough that the frontier and exposures panes read it rather than re-solving.

Where the inputs come from
  • Target weights: the index's latest completed run, via its rebalance snapshots. Optimising against an index nobody has calculated is not a thing that can be done, so it is a 404 rather than a silent default.
  • Risk model: estimated from the constituents' own price history over the run's window. Shrunk toward constant correlation, because a covariance estimated on a few hundred observations across a similar number of names is badly conditioned and an optimiser inverts it.
  • Expected returns: the historical mean, annualised, and this is a modelling choice worth stating plainly: historical mean returns are a poor forecast. They are used because they are the only return estimate derivable from the data the server holds, and because a frontier has to be drawn against something. A caller with a real forecast should supply it. The field says so.
Factor exposures without a factor file

Exposures need loadings, and there is no fundamentals data (that is the features layer, still to be designed). So the factors here are the ones that are derivable from price and share count:

  • size: log market capitalisation
  • momentum: trailing return, excluding the most recent month
  • volatility: trailing standard deviation of returns

Value and quality are absent rather than approximated. A momentum factor built from prices is the real thing; a value factor faked without book values would not be, and labelling one as such would be worse than not having it.

OptimisationTarget dataclass

OptimisationTarget(
    index_id: str, date: str, weights: dict[str, float]
)

The weights an optimisation solves for, and where they came from.

Attributes:

Name Type Description
index_id str

The index targeted, or the one the targeted strategy holds.

date str

The rebalance the weights were in force from, YYYY-MM-DD.

weights dict[str, float]

The target's weights.

constituent_prices

constituent_prices(
    fetcher: DataFetcher,
    identifiers: list[str],
    start: str | None = None,
    end: str | None = None,
    currency: str | None = None,
) -> pd.DataFrame

Close prices for a set of names in currency, names on the columns.

Converted into the index's currency, so the risk model behind a solve carries exchange-rate risk, and a size factor compares caps in one money. None leaves each name in its own currency (a run saved before runs recorded their currency).

Raises:

Type Description
DataNotFoundError

If none of them can be priced.

CalculationError

If a name's currency has no rate into currency.

build_optimisation_job

build_optimisation_job(
    run_id: str,
    request: OptimisationRunRequest,
    constraint_set: ConstraintSet,
    constraints: list[Any],
    target: OptimisationTarget,
    label_for: dict[str, str],
    fetcher: DataFetcher,
    currency: str,
) -> Callable[
    [ProgressReporter], Awaitable[dict[str, Any]]
]

Build the coroutine that runs an optimisation.

Parameters:

Name Type Description Default
target OptimisationTarget

The weights solved for, and where they came from.

required

Returns:

Type Description
Callable[[ProgressReporter], Awaitable[dict[str, Any]]]

A coroutine function suitable for JobRegistry.submit.

assemble_optimisation

assemble_optimisation(
    run_id: str,
    request: OptimisationRunRequest,
    constraint_set: ConstraintSet,
    result: Any,
    target: OptimisationTarget,
    label_for: dict[str, str],
    prices: DataFrame,
    risk_model: Any,
    fetcher: DataFetcher,
    currency: str | None = None,
) -> OptimisationRunResult

Build the wire payload from a completed solve.

Carries the prices' own summary rather than the frames themselves: the frontier and exposures panes re-derive what they need from the identifiers and the window, which keeps a stored run small.

build_frontier

build_frontier(
    run: dict[str, Any],
    constraints: list[Any],
    fetcher: DataFetcher,
    risk_free_rate: float,
) -> FrontierView

Trace the frontier over the run's universe and window.

build_exposures

build_exposures(
    run: dict[str, Any], fetcher: DataFetcher
) -> ExposuresView

Factor exposures of the active position, and its risk decomposition.

parameter_schemas

The shapes a structured backtest parameter takes, published for its editor.

A parameter whose display type is json names one of these in its ref, so a client renders rows or a picker for it rather than a JSON box. No route returns them, so publish adds them to the OpenAPI document's components.schemas beside the schemas the routes reach, where every ref is resolved.

IdentifierList

Bases: RootModel[list[str]]

Instrument identifiers, such as those GET /data/identifiers finds.

ReferenceValueList

Bases: RootModel[list[str]]

Values of one reference column, matched exactly. The parameter's column names the column, and GET /data/reference-values lists what it holds.

IdentifierValues

Bases: RootModel[dict[str, float]]

A number for each identifier. The parameter's help says what the numbers are, and whether they are scaled to add up to one.

DatedAmounts

Bases: RootModel[dict[date, float]]

An amount for each date (YYYY-MM-DD), in the book's currency, positive in and negative out.

ConstraintRows

Bases: RootModel[list[ConstraintRow]]

Optimiser constraints, one row each. The parameter's options names the family whose types the rows take.

TypedSpecs

Bases: RootModel[list[TypedSpec]]

Configurable objects, each a type and its parameters. The parameter's options names the family their types come from.

CountryRates

Bases: RootModel[dict[str, float]]

A rate for each two-letter country code, such as {"US": 0.15}, as a fraction.

FactorSignals

Bases: RootModel[dict[str, TypedSpec]]

Each factor by a name of your choosing, as a signal whose scores are the names' exposures to it. The parameter's options names the family the signals' types come from.

FactorTargets

Bases: RootModel[dict[str, float]]

A target active exposure, in scores, for each factor named in the construction's factors. A factor with no target is aimed at zero.

SignalWeights

Bases: RootModel[list[float]]

A weight for each of the factor's signals, in their order: zero or more, not all zero. Leave out for equal weights.

GroupingFields

Bases: RootModel[list[str]]

Reference fields to group names by, such as ["region", "sector"]: each name is centred within the group its values put it in.

parameter_schemas

parameter_schemas() -> dict[str, Any]

The schemas above, and any they refer to, by name.

publish

publish(app: FastAPI) -> None

Add the parameter schemas to app's OpenAPI document.

A schema a route already publishes, such as ConstraintRow, is kept as the route publishes it.

periods

A run's figures by calendar period: GET /strategies/{id}/periods.

Read from the saved run, so nothing is recalculated: its daily returns, the first from the run's starting point, and its comparators' levels. Each period runs from the previous period's close and the first from the run's start, so the periods compound to the whole run's figures.

build_periods

build_periods(
    strategy_id: str,
    index_id: str,
    run: dict[str, Any],
    frequency: str,
) -> PeriodsView

A saved run's figures in each period of frequency.

preview

What an index definition resolves to at a date, in whichever face it has.

For a rule pipeline, that is the constituent derivation waterfall: how a universe narrows to an index, one rung per selection rule, each naming what it removed, then weighting and capping. The point is attributability: every excluded asset reports the rule that excluded it, so a methodology author can see why a name is missing rather than only that it is.

For a derivation there is no waterfall to show. An optimised index eliminates nothing: it reallocates exactly the names its parent published, and the solve moves every weight at once. Answering with an empty funnel would be a worse lie than refusing, so the answer is a different shape: the parent's weights beside the solved ones, and which constraints cost something. Both faces come back through one endpoint and one response model, with steps and solve mutually exclusive, exactly as pipeline and derivation are on the document being previewed.

Neither face computes anything itself. The pipeline walk is beacon.index.calculation.selection's, and the solve is beacon.index.derived.solve_snapshot, the same call calculate_derived_index makes at every rebalance. A preview and the run it previews cannot disagree, because they are the same code.

build_preview

build_preview(
    document: IndexDocument,
    fetcher: DataFetcher,
    documents: DocumentStore,
    as_of: str | None = None,
    universe_name: str | None = None,
) -> PreviewResponse

Resolve an index definition at a date, in whichever face it has.

Parameters:

Name Type Description Default
document IndexDocument

A validated index definition, rule-driven or derived.

required
fetcher DataFetcher

Data source the rules, weighting scheme and solve read from.

required
documents DocumentStore

Where a derivation's source is resolved from. Unused for a rule-driven document, and required all the same: the draft route previews a document that was never saved, so the store is the only way its parent can be found.

required
as_of str | None

Date to evaluate at, YYYY-MM-DD. Defaults to the base date.

None
universe_name str | None

The name of the stored universe the document names, for the universe step's description.

None

Returns:

Name Type Description
PreviewResponse PreviewResponse

Per-asset outcomes and final weights, plus either the

PreviewResponse

waterfall (steps) or the optimisation (solve).

Raises:

Type Description
DataNotFoundError

If a derivation names a source that is not stored.

InvalidRuleError

If a derivation's parent published no snapshot on or before as_of.

CalculationError

If a derivation's solve is infeasible. The solver's own message names the binding conflict.

query_guard

A query parameter that takes one value is refused when it is given twice.

Without this, ?limit=10&limit=500 quietly answers with the last value, so a client that built a URL wrong gets an answer to a question it did not ask. A list parameter, such as ?fields=NAME&fields=SECTOR, may repeat. Which is which is read from the published OpenAPI document, so the rule is the contract's rather than a second list kept beside it.

single_valued_query

single_valued_query(request: Request) -> None

Refuse a repeated query parameter that takes a single value.

Raises:

Type Description
InvalidArgumentError

Naming each parameter given more than once.

record_tables

A backtest record's large parts, kept as tables beside it.

Almost all of a stored record is a handful of large parts: each book's daily weights, the fund's positions and transactions, each index's rebalance snapshots and the holdings document. The record keeps its own shape with those parts left empty, and says under TABLES_KEY where each one is kept:

  • a book's daily weights, in the run's own weights tables, which the run's analysis keeps anyway;
  • the fund's units in the holdings document, in the fund's holdings, which the run keeps for the same reason;
  • the positions and transactions, as a table each;
  • the rebalance snapshots and the rest of the holdings document, as a table each of every value in them, which gives back the same JSON.

A part goes into a table only when what it reads back as is the part exactly, checked as it is written; one that would not come back exactly stays in the JSON. whole_record puts the parts back, so every response is the one the whole record gave.

slim_record

slim_record(
    document: dict[str, Any], run: RunData, folder: Path
) -> dict[str, Any]

document with its large parts left empty and kept as tables in folder, the run's tables folder, where whole_record finds them.

A book's daily weights are served from run's weights tables, so the document may carry them empty already: what is kept is how many of the latest days it serves, weights_dates_total of them up to the record's bound. Every other part is moved only when what it reads back as is the part exactly.

Parameters:

Name Type Description Default
document dict[str, Any]

The record, as it is stored whole.

required
run RunData

The run's analysis data, as written to folder.

required
folder Path

The run's tables folder.

required

whole_record

whole_record(
    document: dict[str, Any],
    folder: Path,
    wanted: Collection[str] | None = None,
) -> dict[str, Any]

document with the parts slim_record moved put back, or as it is when nothing was moved, as a record stored before.

Parameters:

Name Type Description Default
document dict[str, Any]

The record as it is stored.

required
folder Path

The run's tables folder.

required
wanted Collection[str] | None

The record's top-level keys to put back, every one when None; the rest stay empty.

None

Raises:

Type Description
ConfigurationError

If a part's table is missing or cannot be read.

transactions_from

transactions_from(trades: Any) -> dict[str, Any]

The run's trades as the record's transactions give them.

units_from

units_from(changes: Any) -> dict[str, list[list[Any]]]

Each name's units on the days they changed, 0 from the day it left, from the fund's holdings, as the holdings document gives them.

frame_table

frame_table(frame: dict[str, Any]) -> pa.Table | None

A frame's index and columns as a table, or None when a column holds values of more than one kind (a number beside text), which a table column cannot.

frame_from

frame_from(table: Table) -> dict[str, Any]

The frame frame_table kept.

tree_table

tree_table(tree: Any) -> pa.Table

Every value in tree, a row each in the order they come, beneath a row for each list and dict that holds them.

tree_from

tree_from(table: Table) -> Any

The tree tree_table kept.

reference

Assembling a batch reference response.

Reference data for many identifiers in one request, so a universe table does not need one call per name.

Order is the request's order. A table renders rows in the order it asked for them, and a response sorted by identifier or by whatever the store happened to hold would force the client to re-sort against its own request. Every requested identifier gets exactly one entry at its requested position.

A miss is an entry, not a failure. One unknown ticker in five hundred must not fail the batch: the table should render 499 rows and mark one unknown. Entries carry found, so "we have no data for this" and "this name has no value for that field" stay distinguishable.

Derived fields are requested by name alongside stored ones. adv_3m sits in the same fields list as NAME and SECTOR, so a client asks for what it wants to display in one place and reads the answer out of one mapping. It is opt-in because computing it means slicing the price history for every identifier in the batch, which is work nobody should pay for by default.

A money field is published twice, in two named currencies. Each money field carries its local figure (market_cap_local, in local_currency) beside the converted one (market_cap, in market_cap_currency), and the optional currency parameter names what the converted one is converted into, USD when the caller says nothing. The local number is a fact about the company, the converted one is what compares to a weight, and publishing both means no client has to choose and none can be misled by the choice. A missing rate nulls the converted figure only: the local one is knowable whatever the FX situation, and nulling it would hide something the server holds.

parse_list

parse_list(raw: list[str] | None) -> list[str]

Split a repeatable query parameter into a clean, ordered list.

Accepts both repetition (?fields=A&fields=B) and the comma-separated form (?fields=A,B), because both are natural to write and a client should not have to know which one this server prefers. Applied to every list parameter here rather than only to identifiers: handling one and not the other is how ?fields=NAME,SECTOR ends up rejected as a single column literally named "NAME,SECTOR".

Parameters:

Name Type Description Default
raw list[str] | None

Values as FastAPI parsed them.

required

Returns:

Name Type Description
list list[str]

Entries in request order, duplicates removed.

parse_identifiers

parse_identifiers(raw: list[str] | None) -> list[str]

The identifiers parameter, validated against the batch limit.

Parameters:

Name Type Description Default
raw list[str] | None

Values as FastAPI parsed them.

required

Returns:

Name Type Description
list list[str]

Identifiers, in request order, with duplicates removed.

Raises:

Type Description
InvalidRuleError

If none were supplied, or more than MAX_BATCH.

parse_currency

parse_currency(raw: str | None) -> str

The currency parameter, validated, or the default when absent.

Raises:

Type Description
InvalidRuleError

If it is not a three-letter code. An unknown-but- well-formed code converts to nothing and reports null, which is a data answer; "dollars" is a request that cannot be honoured, and silently nulling every converted figure for it would look like missing FX data rather than a typo.

build_entries

build_entries(
    fetcher: DataFetcher,
    identifiers: list[str],
    date: str | None = None,
    fields: list[str] | None = None,
    currency: str = DEFAULT_CURRENCY,
) -> list[ReferenceEntry]

Assemble one entry per requested identifier, in request order.

Parameters:

Name Type Description Default
fetcher DataFetcher

The data source.

required
identifiers list[str]

What to look up, already validated.

required
date str | None

Point-in-time date for reference validity.

None
fields list[str] | None

Stored columns and derived field names to return. None returns every stored column and no derived field: computing ADV for a batch nobody asked it for would be the endpoint's whole cost paid by every caller.

None
currency str

What the converted money fields are converted into, defaulting to USD. The local figures come back in the instrument's own currency whatever this says, so a caller that names nothing still gets both numbers and both labels.

DEFAULT_CURRENCY

Returns:

Name Type Description
list list[ReferenceEntry]

One ReferenceEntry per requested identifier, in order.

Raises:

Type Description
InvalidRuleError

If a requested stored column is not in the dataset. A silently absent column would show as an empty table row and be read as missing data rather than as a misspelled request.

reports

Rendering reports: the built-in factsheet, and rendering a stored template.

Two kinds of template, and the distinction matters:

  • A stored template is a block list a user built. It is rendered exactly as saved, because that is what "I designed this page" means. Nothing is substituted into it.
  • A built-in template is generated from a completed run. FACTSHEET-A4 is the first: it reads an index's latest backtest and lays out the headline figures, the constituents and the contribution chart.

Rendering runs as a job. Building a factsheet reads a stored run and derives contributions from it, and the result is a PDF, which does not belong in a JSON job payload. So the render writes a file under the storage root, the job result carries its id, and GET /reports/renders/{render_id} streams it.

is_built_in

is_built_in(template_id: str) -> bool

Whether a template is generated rather than stored.

build_factsheet

build_factsheet(
    index_name: str, run: dict[str, Any]
) -> ReportTemplate

Lay out a one-page factsheet from a completed backtest.

Parameters:

Name Type Description Default
index_name str

Display name for the header.

required
run dict[str, Any]

A stored backtest result.

required

Returns:

Name Type Description
ReportTemplate ReportTemplate

The blocks, in reading order.

Raises:

Type Description
DataNotFoundError

If the run carries no composition (an older stored result may have a level but no constituents), since a factsheet without holdings is not a factsheet.

render_path

render_path(directory: Path, render_id: str) -> Path

Where a rendered PDF lives.

build_render_job

build_render_job(
    render_id: str,
    request: RenderRequest,
    template: ReportTemplate,
    directory: Path,
) -> Callable[
    [ProgressReporter], Awaitable[dict[str, Any]]
]

Build the coroutine that renders a template to a PDF.

Returns:

Type Description
Callable[[ProgressReporter], Awaitable[dict[str, Any]]]

A coroutine function suitable for JobRegistry.submit.

new_render_id

new_render_id() -> str

An identifier for one rendered document.

ensure_renderable

ensure_renderable(template: ReportTemplate) -> None

Raise if a template obviously cannot produce a page.

Only the checks that are cheap and certain. Whether the blocks fit is the renderer's answer, and it gives a better one (naming the block that overflowed), so it is left to say it.

risk

Estimating a risk model, and serving what it says about itself.

Estimation is a job because it means pulling a price history for every name in the universe before any matrix arithmetic happens. The read is cheap and serves the stored result.

The diagnostics are the interesting part

A correlation matrix looks equally plausible whether or not it can be trusted, so the endpoint reports how it was made and how well conditioned it is rather than only the numbers:

  • intensity: how much weight went on the structured target. Zero means the raw sample covariance, which on a short history across many names is mostly noise.
  • condition number: largest eigenvalue over smallest. An optimiser inverts this matrix, and a large condition number means the inverse amplifies estimation error rather than reflecting it.
  • positive semi-definite: computed from the eigenvalues, not asserted. A matrix that fails this can produce a negative portfolio variance, and a caller about to invert it needs to know rather than be reassured.

average_correlation is reported alongside because it is the sanity check a person can actually do: a diversified equity universe sits somewhere around 0.3–0.6, and a figure far outside that says the window or the universe is not what someone thought.

constituent_returns

constituent_returns(
    fetcher: DataFetcher,
    identifiers: list[str],
    start: str | None,
    end: str | None,
    currency: str,
) -> pd.DataFrame

Daily returns for a set of names in currency, names on the columns.

Prices are converted into currency before the returns are taken, so a name quoted elsewhere carries its exchange-rate moves as part of its risk.

Raises:

Type Description
DataNotFoundError

If fewer than two names can be priced. A covariance over one asset is a variance, and the endpoint promises a matrix.

CalculationError

If a name's currency has no rate into currency.

build_estimation_job

build_estimation_job(
    model_id: str,
    request: RiskModelRequest,
    identifiers: list[str],
    fetcher: DataFetcher,
    currency: str,
) -> Callable[
    [ProgressReporter], Awaitable[dict[str, Any]]
]

Build the coroutine that estimates a risk model.

Returns:

Type Description
Callable[[ProgressReporter], Awaitable[dict[str, Any]]]

A coroutine function suitable for JobRegistry.submit.

assemble_risk_model

assemble_risk_model(
    model_id: str,
    request: RiskModelRequest,
    model: Any,
    currency: str | None = None,
) -> RiskModelView

Build the wire payload from an estimated model.

routers

HTTP routers, one module per resource group.

Each module exposes a build_router() that returns an APIRouter; the app factory mounts them. Routers never construct their own data source: they read it from application state, so a request cannot outlive or contradict the process configuration.

build_analysis_router

build_analysis_router() -> APIRouter

Build the router for reading a run's analysis.

build_backtest_router

build_backtest_router() -> APIRouter

Build the /backtest router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest options.

build_coverage_router

build_coverage_router() -> APIRouter

Build the /data/coverage router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying coverage reporting and the sync endpoint.

build_data_router

build_data_router() -> APIRouter

Build the /data router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the prices and reference endpoints.

build_derivatives_router

build_derivatives_router() -> APIRouter

Build the /derivatives router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the two pricing endpoints and the two

APIRouter

index-level reads.

build_importing_router

build_importing_router() -> APIRouter

Build the /data/import router.

build_indices_router

build_indices_router() -> APIRouter

Build the /indices router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying index list, read, validate, create and

APIRouter

update.

build_events_router

build_events_router() -> APIRouter

Build the /ws event feed.

Kept separate from the HTTP router because the bearer dependency that guards every other route takes a Request and cannot run on a WebSocket handshake. This router is mounted unguarded and authorises itself.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the WebSocket endpoint.

build_jobs_router

build_jobs_router() -> APIRouter

Build the /jobs router and the /ws event feed.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying job polling, cancellation and the socket.

build_optimise_router

build_optimise_router() -> APIRouter

Build the /optimise router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying constraint-set CRUD, run submission, and the

APIRouter

frontier and exposures reads.

build_reference_values_router

build_reference_values_router() -> APIRouter

The reference-values route, under /data.

build_refresh_router

build_refresh_router() -> APIRouter

Build the /data/stores/{store_id}/refresh route.

build_reports_router

build_reports_router() -> APIRouter

Build the /reports router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying template CRUD, the render job, and the

APIRouter

rendered-document download.

build_risk_router

build_risk_router() -> APIRouter

Build the /risk-models router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying model reads and the estimation job.

build_stores_router

build_stores_router() -> APIRouter

Build the /data/stores router.

build_strategies_router

build_strategies_router() -> APIRouter

Build the /strategies router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying strategy list, read, validate, create,

APIRouter

replace and delete.

build_strategy_runs_router

build_strategy_runs_router() -> APIRouter

Build the router for backtesting stored strategies and reading their runs, under /strategies.

Mounted before the strategies router, so /strategies/compare and /strategies/runs are not read as strategy ids.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest submission, its validation,

APIRouter

and the result views.

build_synthetic_router

build_synthetic_router() -> APIRouter

Build the /data/synthetic router.

build_universes_router

build_universes_router() -> APIRouter

Build the /universes router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying universe list, read, members, upsert and

APIRouter

delete.

build_watchlists_router

build_watchlists_router() -> APIRouter

Build the /data/watchlists router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying watchlist list, read, upsert and delete.

analysis

Reading a run's analysis: the catalogue of what an analysis can be asked for, a strategy's latest run's analytics block, and its large tables, by column in JSON or as Arrow.

build_analysis_router
build_analysis_router() -> APIRouter

Build the router for reading a run's analysis.

backtest

The backtest's catalogue: GET /backtest/options.

Backtests themselves are submitted for a stored strategy, at POST /strategies/{strategy_id}/backtest; this lists what such a request, and the strategy it runs, may carry.

build_backtest_router
build_backtest_router() -> APIRouter

Build the /backtest router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest options.

coverage

Data-coverage reporting and the sync job.

Coverage reports whether each dataset is loaded, how many identifiers it holds, the span of dates it covers, and when it was last refreshed. A null age means the dataset is not loaded at all, which is a different statement from "loaded and never refreshed"; see decisions/0002-caching-and-data-freshness.md.

POST /{dataset}/sync is deprecated. It refreshes the active store from its own source, the same as POST /data/stores/{store_id}/refresh.

build_coverage_router
build_coverage_router() -> APIRouter

Build the /data/coverage router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying coverage reporting and the sync endpoint.

data

Data router: prices and reference data.

There is no /data/fundamentals endpoint: fundamentals will be served by a general features endpoint covering any per-instrument datapoint that is neither reference data nor a corporate action and that can drive a backtest or an index rule. That endpoint is not designed yet.

/data/corporate-actions returns the raw actions plus the aggregates that need the whole series (a trailing dividend, its yield, and the compounded split ratio), so a client does not reimplement the trailing window and get its boundary wrong.

build_data_router
build_data_router() -> APIRouter

Build the /data router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the prices and reference endpoints.

derivatives

Derivatives pricing endpoints.

Stateless by contract. The two pricing endpoints read nothing and write nothing: a request carries every input, and the response is a pure function of it. That is what makes them safe to call from a form on every keystroke, and a test asserts the storage directory is untouched afterwards rather than trusting the claim.

The term-structure and roll reads resolve a spot price from the data source. They still write nothing.

build_derivatives_router
build_derivatives_router() -> APIRouter

Build the /derivatives router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the two pricing endpoints and the two

APIRouter

index-level reads.

importing

Load a user's own data from CSV files or an Excel workbook into a new store.

The engine runs on the user's own machine, so it takes the files by path, the way a data folder is registered: nothing is copied through the request, and large files cost nothing extra to send.

Every row is checked before anything is saved. If any is wrong, the answer is 422 INVALID_RULE with one finding per problem, each naming its sheet, row and column. Otherwise the data is saved as a new store the engine owns (managed), and loaded straight away unless activate is false.

build_importing_router
build_importing_router() -> APIRouter

Build the /data/import router.

indices

Index definition CRUD with structured validation.

A rejected save returns findings, not a bare 422. A user editing a pipeline needs every problem at once, each addressable to the rule that caused it, so the client can mark the offending row rather than showing one message for the whole form.

load_index
load_index(
    request: Request, index_id: Identifier
) -> IndexDocument

Read an index definition, or answer not-found.

Not-found covers a definition that is absent, one that is not valid JSON, and one that does not satisfy IndexDocument (the third being what every stored document hits the day a required field is added to the model). The listing skips exactly the same documents, so the two cannot disagree about what exists.

Parameters:

Name Type Description Default
request Request

The incoming request.

required
index_id Identifier

Identifier of the definition.

required

Returns:

Name Type Description
IndexDocument IndexDocument

The stored definition.

Raises:

Type Description
DataNotFoundError

If it cannot be served.

build_schedule
build_schedule(
    document: IndexDocument,
    as_of: str | None = None,
    limit: int = SCHEDULE_HORIZON_PERIODS,
) -> ScheduleView

Derive an index's rebalance schedule around a date.

Derived rather than stored: the next rebalance is a function of the schedule, the calendar and today, so storing it would leave a date that silently expires.

Parameters:

Name Type Description Default
document IndexDocument

The index definition.

required
as_of str | None

The date to answer from; defaults to today.

None
limit int

Most dates to serve in each of recent and upcoming; the untrimmed lengths are published beside them either way.

SCHEDULE_HORIZON_PERIODS

Returns:

Name Type Description
ScheduleView ScheduleView

The next rebalance, days until, and a strip of dates

ScheduleView

either side with the counts they were trimmed from.

build_indices_router
build_indices_router() -> APIRouter

Build the /indices router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying index list, read, validate, create and

APIRouter

update.

jobs

Job polling and the WebSocket event feed.

Polling and the socket report the same state: the socket is a latency optimisation, not a separate source of truth. A client that misses a frame can always fall back to GET /jobs/{id}, and one that cannot hold a socket open loses nothing but immediacy.

There is no endpoint here that creates a job. Jobs are submitted by the endpoints that own the work (backtests, optimisation, risk, reports, data loads and refreshes), so a bare "start a job" route would be dead weight and an easy way to spawn work with no purpose.

build_jobs_router
build_jobs_router() -> APIRouter

Build the /jobs router and the /ws event feed.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying job polling, cancellation and the socket.

build_events_router
build_events_router() -> APIRouter

Build the /ws event feed.

Kept separate from the HTTP router because the bearer dependency that guards every other route takes a Request and cannot run on a WebSocket handshake. This router is mounted unguarded and authorises itself.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the WebSocket endpoint.

optimise

Optimiser endpoints: constraint sets, runs, frontier and exposures.

Constraint sets are stored documents like indices and watchlists. A run is a job, because the solve itself is fast but the risk model it needs is not: that means a price history for every constituent and a covariance built from it.

The frontier and exposures panes read a completed run rather than re-solving, which is the same arrangement as the Beacon View endpoints and for the same reason: a client switching tabs should not wait on a recalculation.

Validation happens before the job. A malformed constraint set is a bad request and the client should learn that from the submission, not from a job that fails a moment later. It reports every problem it finds, addressed to the row that caused it, because someone fixing a constraint editor needs all the errors rather than the first.

build_optimise_router
build_optimise_router() -> APIRouter

Build the /optimise router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying constraint-set CRUD, run submission, and the

APIRouter

frontier and exposures reads.

reference_values

GET /data/reference-values: the values a reference column holds.

What a picker for a list of reference values offers, such as the sectors an exclusion screen may exclude: a parameter's column names the column.

ReferenceValues

Bases: BaseModel

Response of GET /data/reference-values.

build_reference_values_router
build_reference_values_router() -> APIRouter

The reference-values route, under /data.

refresh

Refresh a data store from its own source.

What a refresh does depends on the store:

  • Synthetic data is extended to today, keeping every day it holds. This runs python -m beacon.synthetic --extend in a child process, as generation does.
  • A folder is read again, picking up files changed outside the engine.
  • A database has its tables read again.
  • Imported files have nothing to refresh; importing again makes a new store. The request is refused.
  • A folder set to refresh from Yahoo Finance downloads new prices for its instruments and saves them into the folder. Yahoo is never the default: a store uses it only when the user chose it for that store.

Whatever a refresh changes is saved, so it survives a restart. If the store is the one being served, the engine then serves the refreshed data, with a new data_version.

build_refresh_router
build_refresh_router() -> APIRouter

Build the /data/stores/{store_id}/refresh route.

submit_refresh
submit_refresh(
    request: Request,
    record: dict[str, Any],
    end: str | None = None,
) -> RefreshJobStatus

Check a refresh can run, and start it as a job.

Shared with the deprecated POST /data/coverage/{dataset}/sync, which refreshes the store being served.

download_into
download_into(
    path: Path,
    downloader: Downloader,
    on_progress: Callable[[int, int, str], None]
    | None = None,
) -> tuple[int, str | None]

Download prices after a folder store's last date, and save them into it.

The instruments are the ones its reference data lists, or, without reference data, every identifier it prices.

Returns:

Name Type Description
tuple int

Market rows added, and the last date the store now holds.

str | None

Nothing is written when no rows were added.

reports

Report templates and rendering.

Templates are stored documents. Rendering is a job, because the result is bytes, and bytes do not belong in a JSON job payload, so the render writes a file and the job result carries its id for GET /reports/renders/{id} to stream.

Two kinds of template. A stored one is rendered exactly as saved, because that is what "I designed this page" means. A built-in one (FACTSHEET-A4) is generated from an index's latest run. See beacon.server.reports for why there is no templating language in between.

build_reports_router
build_reports_router() -> APIRouter

Build the /reports router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying template CRUD, the render job, and the

APIRouter

rendered-document download.

risk

Risk-model endpoints.

Estimation is a job: it means pulling a price history for every name in the universe before any matrix arithmetic happens. Reading a finished model is cheap and serves the stored result, the same arrangement as backtests and optimisation runs.

The model id is chosen by the caller rather than generated, so a client can re-estimate "the one I use for tech" over a new window and keep referring to it by the same name. Each estimate supersedes the last under that id.

build_risk_router
build_risk_router() -> APIRouter

Build the /risk-models router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying model reads and the estimation job.

stores

Named data stores: list, register, rename, forget, and load one.

Loading runs as a job, like a backtest: reading a large store takes seconds, and the client watches its progress on the event socket. When it finishes, the engine serves the new store's data, remembers it for the next start, and publishes a data.loaded event.

A request already running when a load finishes keeps the data it started with: the swap replaces the engine's data whole, it never changes it in place.

record_or_404
record_or_404(
    request: Request, store_id: str
) -> dict[str, Any]

A store's record, or 404 naming it.

managed_root
managed_root(app: FastAPI) -> Path

The folder the engine creates its own stores in.

remove_managed_folder
remove_managed_folder(app: FastAPI, path: Path) -> None

Delete a folder the engine created, and nothing outside its own root.

The check is the point: a registry record could have been edited by hand, and deleting whatever path it names would make one bad file a way to remove anything on the disk.

refuse_if_refreshing
refuse_if_refreshing(
    request: Request, record: dict[str, Any]
) -> None

Refuse to load or forget a store while a refresh is rewriting it (409).

build_stores_router
build_stores_router() -> APIRouter

Build the /data/stores router.

serve_store async
serve_store(
    app: FastAPI,
    record: dict[str, Any],
    report: ProgressReporter,
) -> LoadResult

Load a store and start serving it: the one path for every load.

Used by activation and by generation, so a store the engine just wrote is served exactly as one the user picked. The caller has set active_data.loading; this clears it however it ends. A failed load leaves the data already served in place.

claim_loading
claim_loading(request: Request) -> None

Mark a load as started, or refuse when one already is (409).

Set before the job is scheduled, so a second request arriving before the job starts is refused rather than racing it.

load_store_job
load_store_job(app: FastAPI, record: dict[str, Any]) -> Any

The coroutine that loads a store and starts serving it.

strategies

Stored strategies: /strategies.

A strategy is what a backtest holds (an index in full, through a replication, or an active strategy measured against it) and how it trades. It is stored beside the index definitions, so it outlives the process and travels with them; everything else a backtest needs stays on the backtest.

build_strategies_router
build_strategies_router() -> APIRouter

Build the /strategies router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying strategy list, read, validate, create,

APIRouter

replace and delete.

strategy_runs

Backtesting a stored strategy, and reading its runs.

A backtest calculates the strategy's index and then simulates the portfolio day by day, which is far too slow to hold an HTTP connection open for. The submission endpoint therefore returns a job id; the client polls GET /jobs/{id} or listens on /ws.

The read endpoints serve the panes of Beacon View (overview, weights, attribution, a single name, and a comparison across strategies), and each answers from the strategy's most recent successful run rather than recalculating anything, so two panes read a moment apart describe the same run. Every read is a 404 until the strategy has been backtested.

build_strategy_runs_router
build_strategy_runs_router() -> APIRouter

Build the router for backtesting stored strategies and reading their runs, under /strategies.

Mounted before the strategies router, so /strategies/compare and /strategies/runs are not read as strategy ids.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest submission, its validation,

APIRouter

and the result views.

synthetic

Generate synthetic data into a new store, as a job.

The engine generates on request, so an app can start the engine first and offer "Generate synthetic data" when nothing is loaded.

The job runs python -m beacon.synthetic in a child process rather than the generator in this process, for three reasons:

  • One path. The engine runs the same command a person would, so the same settings always give the same data, whichever way it was made.
  • Memory. The default size peaks at about 2.5 GB. A child process gives it all back when it exits; a long-running engine might not.
  • Failure. If generation runs out of memory or crashes, the child dies and the engine carries on. Cancelling the job kills the child.

The child prints a progress line per stage (--progress), which the job reports on the event socket.

interpreter
interpreter(flags: Any = sys.flags) -> list[str]

This Python, with the isolation flags the engine itself runs under.

An engine started isolated (-I, as the Beacon app starts its bundled one) ignores the user's own site-packages and PYTHON* variables. A child started without the same flags would not, and could load a different numpy or pandas from the user's site-packages than the engine uses.

Parameters:

Name Type Description Default
flags Any

The interpreter's flags; sys.flags unless a test passes others.

flags
command
command(config: SyntheticConfig, out: Path) -> list[str]

The command line that generates config into out.

Every setting is passed explicitly, dates included, so the child generates exactly what was validated, even if it runs past midnight.

build_synthetic_router
build_synthetic_router() -> APIRouter

Build the /data/synthetic router.

extend_command
extend_command(path: Path, end: str | None) -> list[str]

The command line that extends the synthetic store at path.

run_synthetic async
run_synthetic(
    arguments: list[str], report: ProgressReporter
) -> None

Run python -m beacon.synthetic in a child process, reporting progress.

Generates a store or extends one, for the reasons in the module docstring.

Raises:

Type Description
CalculationError

If the command exits unsuccessfully, carrying the end of its own output.

universes

Universes: named sets of instrument identifiers.

A universe is a server-side concept. The library has no universe object (an IndexDefinition carries a plain list of identifiers), so these documents exist to let several definitions share one curated list rather than each repeating it.

Members are checked against the loaded data. A universe naming an instrument the server has no data for is a universe that produces an empty index and no explanation. Both POST and PUT resolve every member against the loaded data and refuse the ones that are not there, as findings naming each missing identifier, in the shape the index editor already renders. A bare 422 would tell somebody a list of five hundred tickers was wrong without saying which one.

Seeded universes are read-only. The synthetic generator writes a GLOBAL universe covering everything it produced, so a fresh workspace has something to select. It is marked source: "seeded" and refuses edits: it derives from the dataset, so regenerating would discard whatever had been changed. Refusing now beats losing it later.

load_universe
load_universe(
    request: Request, universe_id: Identifier
) -> Universe

Read a universe or raise the mapped not-found error.

Shared with the indices router, which resolves a universe reference when saving a definition.

Parameters:

Name Type Description Default
request Request

The incoming request.

required
universe_id Identifier

Identifier of the universe.

required

Returns:

Name Type Description
Universe Universe

The stored universe.

Raises:

Type Description
DataNotFoundError

If no such universe exists, or the stored document cannot be parsed or validated. The listing skips exactly those, so the two surfaces agree by construction.

seed_global_universe
seed_global_universe(
    store: DocumentStore, fetcher: Any
) -> bool

Write the GLOBAL universe for a loaded dataset.

Idempotent, and deterministic: the members are the dataset's identifiers in sorted order, so regenerating with the same seed reproduces the same document byte for byte.

Rewritten when the dataset's membership changes -- a store swapped for a larger one should not leave GLOBAL describing the old one -- but left alone otherwise, so the file's mtime does not churn on every boot.

Parameters:

Name Type Description Default
store DocumentStore

The universe document store.

required
fetcher Any

The loaded data source.

required

Returns:

Name Type Description
bool bool

Whether anything was written.

build_universes_router
build_universes_router() -> APIRouter

Build the /universes router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying universe list, read, members, upsert and

APIRouter

delete.

watchlists

Watchlist CRUD, persisted through the DocumentStore.

Watchlists are user-authored and must outlive the process, so they go to disk rather than to application state.

build_watchlists_router
build_watchlists_router() -> APIRouter

Build the /data/watchlists router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying watchlist list, read, upsert and delete.

runs

Reading a stored run's composition.

Three small readers, in their own module because both views and weights need them and neither should have to import the other to get at them. Every view of a completed backtest starts by turning the stored payload back into snapshots, so this is the shared floor rather than a utility drawer.

snapshots_from

snapshots_from(
    run: dict[str, Any],
) -> list[RebalanceSnapshot]

Read the rebalance snapshots off a stored run.

Raises:

Type Description
DataNotFoundError

If the run carries none. An older stored result may have a level and metrics but no composition, and saying so is better than serving an empty index.

weight_map

weight_map(
    snapshots: list[RebalanceSnapshot],
    uncapped: bool = False,
) -> dict[pd.Timestamp, dict[str, float]]

Snapshots keyed by timestamp, as the analysis helpers expect.

snapshot_at

snapshot_at(
    snapshots: list[RebalanceSnapshot], as_of: str | None
) -> RebalanceSnapshot

The rebalance in force on a date.

The latest snapshot at or before as_of, because an index holds the weights set at its last rebalance until the next one. A date before the first rebalance has no answer and says so rather than returning the first, which would report weights that were not yet in force.

schemas

Wire schemas for the Beacon API.

Library result objects are dataclasses holding pandas structures. They are never exposed directly: their field names and types are internal and would otherwise become an API contract by accident. Everything crossing the wire is declared here, so OpenAPI describes it and a library refactor cannot silently reshape a response.

SkippedCauses

Bases: BaseModel

Why a listing left documents out, one count per cause.

Each cause has its own remedy, so each has its own count and a description that names it.

from_counts classmethod
from_counts(counts: SkipCounts) -> SkippedCauses

Publish a listing's SkipCounts.

TolerantCollection

Bases: BaseModel

A listing that leaves out what it cannot read, and says so.

The base of every collection the server lists from the store.

skips staticmethod
skips(counts: SkipCounts) -> dict[str, Any]

Both fields from one SkipCounts, so they cannot disagree.

Spread into a collection's constructor. Taking the total and the breakdown from the same object is what keeps skipped equal to the sum of its causes -- two arguments computed separately at seven call sites is seven places for them to drift.

Money

Bases: BaseModel

An amount with its denomination.

A bare float would leave the currency implicit, which breaks as soon as a response mixes denominations.

SeriesPayload

Bases: BaseModel

A pandas Series on the wire.

from_series classmethod
from_series(series: Series) -> SeriesPayload

Build from a pandas Series.

TableFrame

Bases: BaseModel

A pandas DataFrame on the wire.

Row-oriented so column order is preserved and the payload stays compact.

from_dataframe classmethod
from_dataframe(frame: DataFrame) -> TableFrame

Build from a pandas DataFrame.

DataSourceStatus

Bases: BaseModel

Whether data is loaded, how much, and from where.

ChangelogSectionView

Bases: BaseModel

One heading under a release and its items.

ChangelogEntryView

Bases: BaseModel

One release of the engine.

ChangelogResponse

Bases: BaseModel

Response of GET /changelog.

HealthResponse

Bases: BaseModel

Response of GET /health.

CalendarCoveragePayload

Bases: BaseModel

The window asked for beside the one the calendar could offer.

Present only when the calendar narrowed the run, so its presence is the signal. A window the calendar covers nothing of never reaches a result at all: that refuses, because an empty index is a failure wearing a success.

from_coverage classmethod
from_coverage(
    coverage: CalendarCoverage,
) -> CalendarCoveragePayload

Build from the library's CalendarCoverage.

Only ever called for a partial cover, where both covered ends are set. An empty one refuses before a result exists.

IndexResultSummary

Bases: BaseModel

Serialised view of an IndexResult.

from_result classmethod
from_result(result: IndexResult) -> IndexResultSummary

Build from a library IndexResult.

BacktestMetrics

Bases: BaseModel

Headline metrics from a backtest run.

Tracking figures are null when the run had no target index to compare to.

RebalanceSnapshot

Bases: BaseModel

The index's composition at one rebalance.

Both weight sets are carried. weights is what the index applied; uncapped_weights is what the weighting scheme produced before any cap. They are equal on an uncapped index, and the difference is the only way to answer what capping cost, a question that cannot be reconstructed from the applied weights alone.

The run payload publishes these snapshots as rebalances, and the stored backtest record publishes the same rows on each index book.

ReferenceWindow

Bases: BaseModel

A span over which a name's reference columns held.

PortfolioBookPayload

Bases: BaseModel

The portfolio's books on the wire.

No rebalances here, unlike the index books: a portfolio makes no rebalance decision of its own (it trades toward one), so decided weights would be a field with nothing honest to put in it.

BookPayload

Bases: BaseModel

One comparator's record on the wire.

Two different facts about weights, not two copies of one: weights is the daily panel (what the book actually HELD each day, drift included) and rebalances is what each rebalance DECIDED. They agree only on a rebalance date; everywhere else prices have moved the held weights away from the decided ones. A client wanting decided weights reads rebalances rather than resampling the panel, which cannot answer what capping cost whatever it is resampled onto.

IndexBooksPayload

Bases: BaseModel

The run's calculated indices on the wire.

Mirrors the library's IndexBooks: target is the index being aimed at, pre-optimisation; optimised is the solved index's own calculation, null until an optimised run fills it.

UnfilledOrderPayload

Bases: BaseModel

An order the simulation could not execute in full.

PriceGapPayload

Bases: BaseModel

A day a name had no bar on a session that should have had one.

RebalancePricingPayload

Bases: BaseModel

The session one rebalance's trades were priced from.

BacktestResultSummary

Bases: BaseModel

Serialised view of a BacktestResult, in the shape of its books.

The nested shape mirrors the library object, one home per fact. Books the run did not have (no benchmark given, no index calculated) are null rather than empty, so a client can tell "not measured" from "measured and empty". index is a container of two books, {target, optimised}, matching the library's IndexBooks.

from_result classmethod
from_result(
    result: BacktestResult,
    cap: float | None = None,
    settings: dict[str, Any] | None = None,
    weights: bool = True,
) -> BacktestResultSummary

Build from a library BacktestResult.

Parameters:

Name Type Description Default
result BacktestResult

The finished run.

required
cap float | None

The maximum constituent weight declared by the definition whose rules produced the target book: the document's own on a passive run, its parent's on an optimised one. Stamped on that book's rebalance snapshots only: a solved index has no cap of its own, since its constraints are what shaped its weights.

None
settings dict[str, Any] | None

What ran, resolved, to keep with the record.

None
weights bool

Whether each book's daily weights are filled in. Left empty, weights_dates_total still counts them, for a record whose weights are served from the run's own tables.

True

BacktestRecordRow

Bases: BaseModel

One stored backtest record, as a listing knows it.

The row is deliberately thin: the strategy to fetch the record by, what it holds, and when it was captured. Everything else comes from /strategies/{strategy_id}/record.

BacktestRecordCollection

Bases: TolerantCollection

Response of GET /strategies/runs.

An envelope rather than a bare array, so the listing can say how many stored records it left out.

PricesResponse

Bases: BaseModel

Response of GET /data/prices/{identifier}.

from_frame classmethod
from_frame(
    identifier: str, interval: str, frame: DataFrame
) -> PricesResponse

Build from a date-indexed market-data frame.

FeatureValue

Bases: BaseModel

One field and what it was worth.

FeatureResponse

Bases: BaseModel

Response of GET /data/features/{identifier}.

FeatureBatchEntry

Bases: BaseModel

One instrument in a batch feature response.

FeatureBatchResponse

Bases: BaseModel

Response of GET /data/features.

FeatureTypeCoverage

Bases: BaseModel

One feature dataset, and how much of it is present.

FeatureCatalogue

Bases: BaseModel

Response of GET /data/features/catalogue.

What a client populates its controls from. Derived from the loaded data rather than a fixed vocabulary, so a dataset somebody loads tomorrow becomes a filter without a code change.

FieldDescriptor

Bases: BaseModel

One datapoint a client can offer as a filter.

FieldCatalogue

Bases: BaseModel

Response of GET /data/fields.

Every datapoint an expression can name, from one place, so a client builds one field picker rather than one per dataset. Derived from the loaded store, so a column or dataset nobody declared still appears.

TablePage

Bases: BaseModel

Response of GET /data/tables/{dataset}.

The stored data as it is, before any view shapes it. Paged because the default synthetic store holds 11.8M market rows, and an unbounded dump is not something a client can render or an engine should assemble.

FeatureRow

Bases: BaseModel

One row of an import.

FeatureImport

Bases: BaseModel

Body of POST /data/features.

FeatureImportResult

Bases: BaseModel

What an import did.

UniverseMembership

Bases: BaseModel

One universe an instrument belongs to.

ReferenceResponse

Bases: BaseModel

Response of GET /data/reference/{identifier}.

Fields are whatever columns the loaded reference data carries (the library does not impose a schema on it), so they are returned as a mapping rather than as named attributes.

from_row classmethod
from_row(
    identifier: str,
    row: Series,
    universes: list[UniverseMembership] | None = None,
) -> ReferenceResponse

Build from a single reference-data row.

IdentifierMatch

Bases: BaseModel

One identifier a search or enumeration returned.

IdentifierSearchResponse

Bases: BaseModel

Response of GET /data/identifiers.

Search when q is given, enumeration when it is not.

Ranking is decided server-side and is part of the contract: exact identifier, identifier prefix, name prefix, identifier substring, name substring, alphabetical within each. Once limit is applied a client cannot re-rank what it was not sent.

ReferenceEntry

Bases: BaseModel

One identifier's row in a batch reference response.

BatchReferenceResponse

Bases: BaseModel

Response of GET /data/reference.

Entries are in the order the request named them, one per identifier, so a table can render straight down the list without re-sorting against what it asked for.

CorporateAction

Bases: BaseModel

One corporate action.

kind is the authoritative answer to what value means, and the reason a client needs no list of type strings. Reading type and inferring cash or ratio from a hardcoded list works until a type the client has never seen arrives, at which point it renders as whichever the list defaults to: confidently, and wrongly.

CorporateActionsResponse

Bases: BaseModel

Response of GET /data/corporate-actions/{identifier}.

Carries the raw history and the two aggregates that need the whole series to compute, so a client asking "what did this pay" does not have to reimplement the trailing window and get its boundary subtly wrong.

from_frame classmethod
from_frame(
    identifier: str,
    frame: DataFrame,
    trailing_dividend: float,
    trailing_dividend_yield: float | None,
    cumulative_split_ratio: float,
) -> CorporateActionsResponse

Build from a corporate-action history slice.

SyncRequest

Bases: BaseModel

Body of POST /data/coverage/{dataset}/sync.

Deprecated with the endpoint. Its fields are accepted and ignored: a sync refreshes the whole active store from its own source, as POST /data/stores/{id}/refresh does.

Watchlist

Bases: BaseModel

A named set of instrument identifiers.

WatchlistUpsert

Bases: BaseModel

Body of PUT /data/watchlists/{id}.

The id comes from the URL, so it is not repeated here: accepting it in both places invites the two to disagree.

WatchlistCollection

Bases: TolerantCollection

Response of GET /data/watchlists.

RuleSpec

Bases: BaseModel

One rule in the pipeline, addressable by its id.

WeightingSpec

Bases: BaseModel

The weighting group of the pipeline.

TreatmentSpec

Bases: BaseModel

The treatment group of the pipeline.

PipelineSpec

Bases: BaseModel

The grouped rule pipeline: Selection, Weighting, Treatment.

UniverseRef

Bases: BaseModel

Where an index's universe comes from.

Either a reference to a stored universe or a literal list. identifiers is always populated on read, so consumers never have to resolve it.

ConstraintRow

Bases: BaseModel

One constraint, in the shape a client's editor holds it.

Maps 1:1 to a class in beacon.optimise.constraints: the row a user edits, the JSON that is stored and the object the solver receives are the same thing in three representations, so a rule cannot change meaning in translation.

DerivationPayload

Bases: BaseModel

How an optimised index is derived from the index it was built on.

The whole of an optimised index's methodology: the source it reallocates, what the solve minimises, and what the answer must satisfy. No weights (neither the parent's nor the solved ones), because definitions are rules and weights are calculated.

IndexDocument

Bases: BaseModel

A stored index definition, in one of its two faces.

A document carries either a rule pipeline (pipeline and universe) or a derivation, never both and never neither. derivation is the discriminator: present, the index is optimiser-derived and its methodology is the derivation; absent, it is a rule pipeline over a universe.

Finding

Bases: BaseModel

One validation result, addressable to the rule that caused it.

ValidationReport

Bases: BaseModel

Response of the validation endpoint, and of a rejected save.

IndexCollection

Bases: TolerantCollection

Response of GET /indices.

OptimiseRequest

Bases: BaseModel

Body of POST /indices/{index_id}/optimise.

Everything the derived index needs that the parent cannot supply. The source is deliberately absent: provenance is server-truth, taken from the URL, so a client cannot assert a parentage the server did not create.

DeletedIndex

Bases: BaseModel

One index a delete removed, and what went with it.

IndexDeletion

Bases: BaseModel

Response of DELETE /indices/{index_id}.

Everything the delete removed, so a client reports the outcome from the response rather than from its own prediction of the blast radius. The named index comes first, then each optimised child in the order the cascade reached it.

ReportTemplateDocument

Bases: BaseModel

A stored report template, as JSON.

Blocks are kept as free-form mappings rather than a discriminated union so the wire shape stays exactly what beacon.report.blocks reads and writes. A second definition of the same thing here is a second definition to keep in step, and the block model already validates its own rows on the way in.

ReportTemplateCollection

Bases: TolerantCollection

Response of GET /reports/templates.

RenderRequest

Bases: BaseModel

Body of POST /reports/render.

RenderResult

Bases: BaseModel

Result payload of a completed render job.

FuturesPriceRequest

Bases: BaseModel

Body of POST /derivatives/futures/price.

Stateless: every input the calculation needs is here, and nothing is read from or written to storage.

CarryDecomposition

Bases: BaseModel

Carry split into the pieces a person can reason about.

Each part is the price effect of one rate acting alone. They do not sum to the total exactly, because carry compounds rather than adds; the residual is reported rather than spread across the parts, which would make each of them slightly wrong in order to hide that the split is approximate.

FuturesPriceResponse

Bases: BaseModel

Response of POST /derivatives/futures/price.

TrsPriceRequest

Bases: BaseModel

Body of POST /derivatives/trs/price.

TrsAccrual

Bases: BaseModel

One financing period.

TrsPriceResponse

Bases: BaseModel

Response of POST /derivatives/trs/price.

TermStructureEntry

Bases: BaseModel

One expiry in a term structure.

TermStructureResponse

Bases: BaseModel

Response of GET /derivatives/{index_id}/term-structure.

RollResponse

Bases: BaseModel

Response of GET /derivatives/{index_id}/roll.

Both legs are priced theoretically off the same spot and curve, so this is the carry roll rather than a market one.

RiskModelRequest

Bases: BaseModel

Body of POST /risk-models/{model_id}/estimate.

RiskDiagnosticsPayload

Bases: BaseModel

How an estimate was produced, and how far it can be trusted.

RiskModelView

Bases: BaseModel

Response of GET /risk-models/{model_id}.

RiskModelSummary

Bases: BaseModel

One entry in GET /risk-models.

RiskModelCollection

Bases: BaseModel

Response of GET /risk-models.

ConstraintSet

Bases: BaseModel

A named list of constraints.

ConstraintSetCollection

Bases: TolerantCollection

Response of GET /optimise/constraint-sets.

SavedConstraintSet

Bases: BaseModel

Response of a successful save: the set plus any warnings.

ParameterSpec

Bases: BaseModel

One parameter of a configurable type, described well enough to render.

Names, types, defaults and whether a parameter is required are read from the constructor, so they cannot drift from what the code accepts. Labels, ordering and choices are declared on the class, because a signature cannot carry them.

TypeSpec

Bases: BaseModel

One configurable type a client can offer.

RuleTypes

Bases: BaseModel

Response of GET /indices/rule-types.

Everything a methodology editor needs to render a real form: which rules and schemes exist, what each takes, and how to label and order the fields. Without it RuleSpec.type is a free-text box and params a list of key/value pairs, so a misspelled parameter is only discovered on submit.

CalendarOption

Bases: BaseModel

One selectable trading calendar, as GET /indices/calendars serves it.

Every field is derived from exchange_calendars at request time except name, which is curated and falls back to the code. Nothing here is a hand-kept table of the calendar set itself, so the options a client offers cannot drift from the calendars the schedule accepts.

CalendarList

Bases: BaseModel

Response of GET /indices/calendars.

IndexDocument.calendar is required, so this publishes the calendars the engine accepts, the way /indices/rule-types and /optimise/constraint-types publish theirs. It is read from exchange_calendars at request time, never a hand-kept copy, so the wire set cannot drift from the set the calculation schedules on.

ConstraintTypes

Bases: BaseModel

Response of GET /optimise/constraint-types.

Served so a client builds its editor from the same source the solver reads, rather than from a copy that drifts.

OptimisationRunRequest

Bases: BaseModel

Body of POST /optimise/runs: the weights a target held on a date, solved for under a constraint set.

The target is an index or a stored strategy, exactly one of the two. An index's weights are calculated, so no backtest is needed; a strategy's are what its latest run chose at that rebalance, so an active strategy's answers how close a portfolio can get to it under the constraints.

WeightRow

Bases: BaseModel

One name's target, optimal and active weight.

OptimisationRunResult

Bases: BaseModel

Result payload of a completed optimisation job.

FrontierPoint

Bases: BaseModel

One portfolio on the efficient frontier.

FrontierView

Bases: BaseModel

Response of GET /optimise/runs/{run_id}/frontier.

FactorExposure

Bases: BaseModel

One factor loading.

RiskDecomposition

Bases: BaseModel

Active risk split into factor and specific parts.

The two sum to the total exactly, because the covariance is defined as B F Bᵀ + D. Pair an arbitrary covariance with arbitrary loadings and there is a cross term; the identity belongs to this model and not to any pairing of a matrix with some exposures.

ExposuresView

Bases: BaseModel

Response of GET /optimise/runs/{run_id}/exposures.

Factors are the ones derivable from price and share count (size, momentum, volatility), plus a market intercept. Value and quality are absent rather than approximated: a momentum factor built from prices is the real thing, a value factor faked without book values would not be.

SavedIndex

Bases: BaseModel

Response of a successful save: the document plus any warnings.

FieldNode

Bases: BaseModel

A named datapoint: expressions.core.Field.

namespace is the surface the value comes from and dataset narrows a feature to one vendor's TYPE, so two sources can both ship a revenue without collision.

ComparisonNode

Bases: BaseModel

A field, an operator and a value: expressions.core.Comparison.

AllNode

Bases: BaseModel

Every operand must pass: expressions.core.All.

AnyNode

Bases: BaseModel

At least one operand must pass: expressions.core.Any_.

NotNode

Bases: BaseModel

The negation of an expression: expressions.core.Not.

ExpressionNode

Bases: RootModel[Annotated[FieldNode | ComparisonNode | AllNode | AnyNode | NotNode, Field(discriminator='node')]]

One node of a serialised expression, discriminated on node.

The grammar a screen is written in: a field, a comparison over one, or a boolean composition of either. Recursive (all, any and not carry nodes of this same union), so an arbitrarily nested screen is one type.

A RootModel rather than a bare union so the union is a named schema in this document: a recursive $ref needs a name to point at, and so does ParameterSpec.ref.

Universe

Bases: BaseModel

A named set of instrument identifiers.

UniverseUpsert

Bases: BaseModel

Body of PUT /universes/{id}.

UniverseCreate

Bases: BaseModel

Body of POST /universes.

No id: the server derives one from the name, so a client cannot create two universes whose ids differ only in punctuation and expect them to be distinct documents.

UniverseCollection

Bases: TolerantCollection

Response of GET /universes.

UniverseMembers

Bases: BaseModel

Response of GET /universes/{id}/members.

ScheduleView

Bases: BaseModel

Response of GET /indices/{index_id}/schedule.

Derived, not stored: the next rebalance is a function of the schedule, the calendar and today, and storing it would leave a date that silently expires.

PreviewRequest

Bases: BaseModel

Body of POST /indices/{id}/preview, which previews the saved index.

PreviewDocumentRequest

Bases: BaseModel

Body of POST /indices/preview, which previews a document as supplied.

The route for a draft. The by-id route reads what is stored, so while an editor holds unsaved changes its figures describe the old definition, with nothing on screen to say they are stale. This one previews exactly what was sent, so editing a rule updates the resolved figures without saving.

StageParam

Bases: BaseModel

One setting of a stage, as a reader sees it.

PreviewStep

Bases: BaseModel

One rung of the derivation waterfall.

There is one of these per selection rule, in pipeline order, plus a first entry for the universe itself so the funnel starts from a stated total.

PreviewAsset

Bases: BaseModel

Per-asset outcome of the derivation.

Two disjoint groups of fields, matching the two faces of a preview. The rule-provenance fields (excluded_by, excluded_at, uncapped_weight, capped) describe a walk down a pipeline and are null on a derived preview, which has no rules to attribute anything to. The derived fields (source_weight, solved_weight, weight_delta) describe a reallocation and are null on a rule-driven one. Neither group was overloaded to carry the other's meaning: a client reading excluded_by on a derived index would be reading an answer to a question nobody asked.

PreviewConstraint

Bases: BaseModel

One constraint at the solved point: whether it bound, and its room.

Every constraint appears, not only the binding ones. A binding constraint's slack is zero by definition, so a report of only those is a list of zeros; what a reader actually wants beside "this cap bound" is "and the turnover budget had four points of room left".

PreviewSolve

Bases: BaseModel

What the optimiser did at one rebalance: the derived face of a preview.

A derivation has no waterfall: the solve moves every weight at once rather than eliminating names in steps, so there are no rungs to show. This is the honest analogue: which parent snapshot was solved, under what, and which rules cost something.

PreviewResponse

Bases: BaseModel

Response of POST /indices/{id}/preview, in one of its two faces.

Exactly one of steps and solve is present, mirroring pipeline and derivation on the document the preview was built from: a rule-driven index answers with the waterfall, a derived one with the solve. solve is the discriminator, and it is the same discriminator the client already branches on one level up.

Everything outside the pair is common to both: the resolved weights, their total, and one row per name.

BenchmarkRef

Bases: BaseModel

What to compare a backtest against.

Distinct from the index being tracked. The tracked index measures replication accuracy; a benchmark measures relative performance against something the portfolio was never trying to replicate.

RelativeMetricsPayload

Bases: BaseModel

Performance against a benchmark, over their shared window.

TypedSpec

Bases: BaseModel

One configurable object: its type and its parameters.

The types and what each accepts are listed by GET /backtest/options.

StrategySpec

Bases: BaseModel

What the backtest holds.

The path's index is the strategy's index: the one tracked, or the benchmark an active strategy is measured against.

CashBandSpec

Bases: BaseModel

The shares of the book the cash may move between before it is brought back to the cash buffer.

ImplementationSpec

Bases: BaseModel

How the strategy is carried out at the fund's size.

VehicleSpec

Bases: BaseModel

How the money is held: a preset, with any settings laid over it.

Settings merge over the preset's: a typed object (pricing) or a list (limits) replaces the preset's whole, a plain object (market) merges key by key, an omitted key keeps the preset's value, and null means none (refused on a setting that cannot be none). Money settings are in the book's currency.

ModellingAssumptionsSpec

Bases: BaseModel

What the run takes as given about markets and data. Every field is optional; an unset one takes the engine's default, which /backtest/options lists.

ShareClassSpec

Bases: BaseModel

One share class of a backtest's fund: its own currency, fee and distribution policy, sharing the book with the other classes.

StrategyBacktestRequest

Bases: BaseModel

Body of POST /strategies/{strategy_id}/backtest: everything a backtest needs that the stored strategy does not hold.

Every field past the first five is optional, and a request without them runs exactly as one with only the dates, capital and cost does. The strategy and its implementation come from the stored strategy, so a body carrying either is refused.

BacktestRequest

Bases: StrategyBacktestRequest

A backtest's settings with its strategy and implementation: what a stored strategy's run is built from. Not a request body of its own.

VehiclePreset

Bases: BaseModel

One vehicle preset, with every setting resolved.

BacktestOptions

Bases: BaseModel

Response of GET /backtest/options.

Everything a backtest form needs, from the engine itself: each family's types with their fields (in the shape /indices/rule-types uses), the vehicle presets with their resolved settings, the settings' descriptors, the modelling assumptions with their defaults, and the closed choices.

BacktestValidation

Bases: BaseModel

Response of POST /strategies/{strategy_id}/backtest/validate.

ConcentrationPayload

Bases: BaseModel

How concentrated a weight vector is.

DriftPayload

Bases: BaseModel

How far weights moved between two rebalances.

OverviewView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/overview.

ConstituentRow

Bases: BaseModel

One constituent's row in the weights table.

Everything a row needs is here, so the table renders from one response rather than joining three. The two weights are the point: raw_weight is what the weighting scheme produced, weight is what survived the cap, and the difference is what capping moved.

RiskPayload

Bases: BaseModel

How the index's volatility divides among its holdings.

Contributions sum to volatility exactly rather than approximately: the decomposition is an identity, so a client can show the parts and the whole without them disagreeing.

ActiveRiskPayload

Bases: BaseModel

How tracking error against a benchmark divides among active positions.

Contributions sum to tracking_error exactly, the same identity the total decomposition satisfies, on active weights rather than holdings.

WeightsView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/weights.

ContributionPayload

Bases: BaseModel

One constituent's share of the index return.

AttributionView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/attribution.

Contributions are Carino-linked, so they sum to the compounded total return rather than approximately to it. residual is reported regardless and should sit at machine epsilon; anything larger means an assumption broke upstream, which is worth surfacing rather than rounding away.

AssetView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/assets/{identifier}.

CompareEntry

Bases: BaseModel

One strategy's latest run within a comparison, on the shared window.

CompareView

Bases: BaseModel

Response of GET /strategies/compare.

FlowPayload

Bases: BaseModel

One day's flow, as the run dealt it.

HedgePayload

Bases: BaseModel

One trade of a hedged class's forward.

ShareClassMetrics

Bases: BaseModel

A class's figures against the index in its currency: converted for an unhedged class, hedged the same way for a hedged one.

ShareClassPayload

Bases: BaseModel

One share class's record of a run. Its money is in its own currency, and its series share level's dates.

MarketPayload

Bases: BaseModel

An ETF's quotes on the exchange, on level's dates.

ComparatorFigures

Bases: BaseModel

A comparator's own figures over a run's window, defined as a run's are.

RelativeFigures

Bases: BaseModel

A run against a comparator over the same window.

ComparisonPayload

Bases: BaseModel

A run against one comparator: the comparator's own figures and the run's relative to it, over the days both have.

ComparisonsPayload

Bases: BaseModel

A run against each of its comparators; null where it has none.

PeriodFigures

Bases: BaseModel

A run's figures over one calendar period.

PeriodComparison

Bases: BaseModel

A comparator over one period: its own figures and the run's against it.

PeriodRow

Bases: BaseModel

One calendar period of a run.

PeriodsView

Bases: BaseModel

Response of GET /strategies/{strategy_id}/periods.

SwapResetPayload

Bases: BaseModel

One reset of a synthetic fund's swap.

FuturesRollPayload

Bases: BaseModel

One roll of a fund's index futures to the next contract.

ReplicationPayload

Bases: BaseModel

What a replication did at one rebalance.

ActiveStepPayload

Bases: BaseModel

What an active strategy did at one rebalance.

RebalanceStepPayload

Bases: BaseModel

What the implementation's stages did at one rebalance.

BacktestRunResult

Bases: BaseModel

Result payload of a completed backtest job.

Every series here derives from the same NAV, rebased to 100: returns is the level's percentage change, drawdown is the level against its running peak, and annual_returns compound back to the total. A client that recomputes any of them lands on these numbers exactly.

JobStatusOf

Bases: BaseModel, Generic[ResultT]

State of one background job, generic over its result payload.

Generic so that every kind of job can publish the shape of the thing it returns.

Not used as a response model directly: the parametrisations below are, and JobStatus is the untyped one.

JobStatus

Bases: JobStatusOf[Any]

State of one background job, with an untyped result.

Used for a listing, which mixes kinds, and as the fallback arm of AnyJobStatus for a kind nothing models yet.

BacktestJobStatus

Bases: JobStatusOf[BacktestRunResult]

A backtest:{strategy_id} job. result is the run payload.

OptimisationJobStatus

Bases: JobStatusOf[OptimisationRunResult]

An optimise:{run_id} job. result is the solved portfolio.

RenderJobStatus

Bases: JobStatusOf[RenderResult]

A render:{render_id} job. result describes the rendered document.

RiskModelJobStatus

Bases: JobStatusOf[RiskModelView]

A risk:{model_id} job. result is the estimated model.

AnalyticsJobStatus

Bases: JobStatusOf[AnalyticsBlock]

An analytics:{strategy_id} job. result is the latest run's analysis, whole: what was kept with it and what was worked out now.

LoadResult

Bases: BaseModel

Result payload of a completed load:{store_id} job.

LoadJobStatus

Bases: JobStatusOf[LoadResult]

A load:{store_id} job. result describes the store now served.

GenerateResult

Bases: BaseModel

Result payload of a completed generate:{store_id} job.

GenerateJobStatus

Bases: JobStatusOf[GenerateResult]

A generate:{store_id} job. result describes the new store.

RefreshResult

Bases: BaseModel

Result payload of a completed refresh:{store_id} job.

RefreshJobStatus

Bases: JobStatusOf[RefreshResult]

A refresh:{store_id} job. result says what changed.

JobCollection

Bases: TolerantCollection

Response of GET /jobs.

Untyped results on purpose: a listing spans every kind at once, so the per-kind arms buy a client nothing it can use without reading kind anyway. GET /jobs/{job_id} is where the typed result lives.

DatasetCoverage

Bases: BaseModel

What the loaded data actually spans, for one dataset.

CoverageResponse

Bases: BaseModel

Response of GET /data/coverage.

ErrorDetail

Bases: BaseModel

The body of an error envelope.

ErrorEnvelope

Bases: BaseModel

Every non-2xx response uses this shape.

metrics_from

metrics_from(
    summary: dict[str, float | None],
) -> BacktestMetrics

The payload's metrics from BacktestResult.summary(): the headline five, which must be there, and whichever others the run has.

headline_metric

headline_metric(
    summary: dict[str, float | None], key: str
) -> float

Read a core metric, which BacktestResult.summary() always populates.

The summary's value type is float | None because the tracking figures are optional; the five headline metrics are not. A missing one is a break in the mirror between summary() and BacktestMetrics, not a degraded run, so it raises CalculationError naming the key rather than reporting a value that was never measured.

rebalance_snapshots

rebalance_snapshots(
    index_result: IndexResult, cap: float | None = None
) -> list[RebalanceSnapshot]

Composition at each rebalance, in date order.

Carries the uncapped weights alongside the applied ones. On an uncapped index the two are identical and the duplication costs a little space; on a capped one the difference is the only record of what the cap did, and it cannot be recovered from the applied weights afterwards.

The cap itself comes from the definition rather than from the cap report, because the calculator only files a report on dates where the cap actually bound. "A 20% cap applies and nothing reached it" and "no cap applies" are different statements about a methodology, and a client asking what the rules are should get the same answer on both dates.

Parameters:

Name Type Description Default
index_result IndexResult

The calculated index.

required
cap float | None

The definition's maximum constituent weight, if it has one.

None

unfilled_payloads

unfilled_payloads(
    result: BacktestResult,
) -> list[UnfilledOrderPayload]

The run's unfilled orders, for whichever payload publishes them.

price_gap_payloads

price_gap_payloads(
    result: BacktestResult,
) -> list[PriceGapPayload]

The run's carried-forward marks, for whichever payload publishes them.

Shared by the record and the run payload rather than written twice: the same fact crossing the wire in two shapes is how the two drift.

rebalance_pricing_payloads

rebalance_pricing_payloads(
    result: BacktestResult,
) -> list[RebalancePricingPayload]

The session each rebalance priced from, for either payload.

security

Bearer-token authentication.

The server binds to loopback but that is not a security boundary: any process on the machine can reach it. Every route therefore requires the token the launcher generated, including /health.

verify_bearer_token

verify_bearer_token(request: Request) -> None

Reject the request unless it carries the configured bearer token.

Wired in as a router-level dependency, so it runs before any handler.

Parameters:

Name Type Description Default
request Request

The incoming request; the expected token is read from application state, where create_app() put it.

required

Raises:

Type Description
HTTPException

401 when the Authorization header is missing, malformed, or carries the wrong token.

serialisation

Wire formats shared by every router.

pandas objects do not survive a plain JSON encoder intact (timestamps, NaN and numpy scalars all need handling), so frames and series are converted here into an explicit, stable shape rather than left to a default encoder.

dataframe_to_payload

dataframe_to_payload(frame: DataFrame) -> dict[str, Any]

Serialise a DataFrame as {index, columns, data}.

Row-oriented data keeps the payload compact and preserves column order, which a dict-of-columns would not guarantee.

Parameters:

Name Type Description Default
frame DataFrame

The frame to serialise. An empty frame yields empty lists.

required

Returns:

Name Type Description
dict dict[str, Any]

index (list of row labels), columns (list of column

dict[str, Any]

names), and data (list of rows, each a list of cell values).

dict[str, Any]

NaN, NaT and infinities all become None.

series_to_payload

series_to_payload(series: Series) -> dict[str, Any]

Serialise a Series as {index, name, data}.

Parameters:

Name Type Description Default
series Series

The series to serialise.

required

Returns:

Name Type Description
dict dict[str, Any]

index (list of labels), name (the series name, or None),

dict[str, Any]

and data (list of values, with NaN/NaT/infinities as None).

share_class_results

What a run's share classes add to its result.

Each class's record is in its own currency, its series on the run's days, and its figures are measured against the index in its currency: converted for an unhedged class, hedged the same way for a hedged one (see beacon.backtest.class_benchmark).

flow_payloads

flow_payloads(
    flows: Sequence[FlowRecord],
) -> list[FlowPayload]

Flows as dealt, on the wire.

share_class_payloads

share_class_payloads(
    result: BacktestResult,
    share_classes: Sequence[ShareClass],
    days: Index,
    index_levels: Series,
    index_currency: str,
    fetcher: DataFetcher,
) -> dict[str, Any]

The share_classes and hedge_value a run's payload carries; empty and null without share classes.

Parameters:

Name Type Description Default
result BacktestResult

The finished run.

required
share_classes Sequence[ShareClass]

The classes it ran, for their hedge ratios.

required
days Index

The run's days, which the classes' series are put on.

required
index_levels Series

The index's levels, in index_currency.

required
index_currency str

The index's currency.

required
fetcher DataFetcher

The data the exchange and short rates are read from.

required

share_class_settings

A backtest request's share classes, and its capital and flows by class.

Without share classes a request's capital is one amount and its flows one list, as before. With them, each class can have its own capital and flows, keyed by its name, in its own currency, and a single amount or list is taken for a fund of one class only, as Backtest takes them. Every problem is a finding at its field's path, such as share_classes.1.hedge_band or flows.EUR H.0.

FundTerms dataclass

FundTerms(
    share_classes: list[ShareClass],
    initial_capital: float | dict[str, float],
    flows: list[Flows] | dict[str, list[Flows]],
)

A request's share classes, capital and flows as Backtest takes them.

Attributes:

Name Type Description
share_classes list[ShareClass]

The classes; empty for a fund of one.

initial_capital float | dict[str, float]

One amount, or one per class name.

flows list[Flows] | dict[str, list[Flows]]

One list of scenarios, or one per class name.

resolved
resolved() -> dict[str, Any]

The share classes as the run reports them.

fund_terms

fund_terms(
    request: StrategyBacktestRequest,
    report: Report,
    build_flow: BuildFlow,
) -> FundTerms

The request's share classes, capital and flows, each problem reported.

stage_descriptions

What each stage of a strategy's workflow is and does, as a reader sees it.

Each stage of a constituent preview or a holdings workflow carries its type, one plain sentence of what it does with its settings, and its settings as labelled values formatted with their units: money in the index's currency, fractions as percentages, basis points and days as such. The labels and units are the catalogue's own, so a stage reads the way its editor does.

Described dataclass

Described(
    type: str,
    description: str,
    params: list[dict[str, str]] = list(),
)

A stage as a reader sees it.

Attributes:

Name Type Description
type str

Its class, or the kind for the steps that have none.

description str

One plain sentence of what it does with its settings.

params list[dict[str, str]]

Its settings, each {label, value} formatted for reading.

Values

Values(
    kind: str,
    type_name: str,
    params: dict[str, Any] | None,
    currency: str,
)

A typed object's settings, each with its catalogue label and unit.

Parameters:

Name Type Description Default
kind str

Its catalogue family, such as catalogue.SCREEN.

required
type_name str

Its class name.

required
params dict[str, Any] | None

The settings given; the rest take their defaults.

required
currency str

What money is in.

required
text
text(name: str) -> str

One setting's value as a sentence writes it.

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

The settings that are set, labelled, in the editor's order.

formatted

formatted(
    value: Any, unit: str | None, currency: str
) -> str

A setting's value as a reader reads it.

rule

rule(
    type_name: str,
    params: dict[str, Any] | None,
    currency: str,
) -> Described

An index's selection rule.

screen

screen(
    type_name: str,
    params: dict[str, Any] | None,
    currency: str,
) -> Described

An implementation's screen.

stale_price

stale_price() -> Described

The stale-price screen every run applies.

universe

universe(
    universe_id: str | None, name: str | None, count: int
) -> Described

The universe a rebalance starts from.

index_cap

index_cap(max_weight: float) -> Described

An index's cap on any one weight.

strategy

strategy(
    settings: dict[str, Any], currency: str
) -> Described

What a tracking or active strategy chose.

cash_buffer

cash_buffer(share: float) -> Described

An implementation's cash buffer.

capacity

capacity(
    settings: dict[str, Any], currency: str
) -> Described

An implementation's caps and minimum position, as one stage.

store

Versioned JSON document storage on the platform's app-data directory.

The server is a local process with no database. User-authored artefacts, such as watchlists, index definitions and constraint sets, are small JSON documents that must survive a restart and, more importantly, must survive a schema change without the user losing them. Every document therefore carries a schema_version, and reads run it forward through the migration chain before it reaches the caller.

DocumentStore

DocumentStore(
    collection: str,
    root: Path | None = None,
    compact: bool = False,
)

A namespaced directory of versioned JSON documents.

Parameters:

Name Type Description Default
collection str

Subdirectory name, e.g. "watchlists". Documents from different collections never collide.

required
root Path | None

Base directory. Defaults to the platform app-data location. Tests pass a temporary path.

None
compact bool

Write documents without the space after each comma and colon, for a collection whose documents are large.

False
panels_directory
panels_directory(document_id: str) -> Path

The folder beside a document that holds its large tables, as a run's record keeps them; the same identifier rules apply.

exists
exists(document_id: str) -> bool

Whether a document with this id is stored.

read
read(document_id: str) -> dict[str, Any] | None

Read a document, migrating it forward to the current schema.

Parameters:

Name Type Description Default
document_id str

Identifier of the document.

required

Returns:

Type Description
dict[str, Any] | None

dict or None: The document at the current schema version, or None

dict[str, Any] | None

if it does not exist.

Raises:

Type Description
ConfigurationError

If the stored file is not valid JSON, or was written by a newer version of the application than this one understands.

write
write(
    document_id: str, document: dict[str, Any]
) -> dict[str, Any]

Write a document, stamping it with the current schema version.

The write goes to a temporary file in the same directory and is then moved into place, so a crash mid-write leaves the previous document intact rather than a truncated one.

Parameters:

Name Type Description Default
document_id str

Identifier of the document.

required
document dict[str, Any]

Payload to store.

required

Returns:

Name Type Description
dict dict[str, Any]

The stored document, including its schema_version.

delete
delete(document_id: str) -> bool

Delete a document.

Parameters:

Name Type Description Default
document_id str

Identifier of the document.

required

Returns:

Name Type Description
bool bool

True if a document was removed, False if none existed.

list_ids
list_ids() -> list[str]

Return every stored document id, sorted.

read_all
read_all() -> list[dict[str, Any]]

Read every document in the collection, migrating each forward.

store_schemas

Request and response models for named data stores.

PostgresConnection

Bases: BaseModel

Where a Postgres store's tables or views are. Read-only.

DataStoreCreate

Bases: BaseModel

Body of POST /data/stores: register an existing store.

StoreForgotten

Bases: BaseModel

Response of DELETE /data/stores/{store_id}: what became of the stored runs that read the store's data.

DataStoreUpdate

Bases: BaseModel

Body of PATCH /data/stores/{store_id}. Omitted fields are unchanged.

RefreshRequest

Bases: BaseModel

Body of POST /data/stores/{store_id}/refresh. Optional.

StoreProblem

Bases: BaseModel

Why a store cannot be read, when the engine can tell without loading it.

DataStore

Bases: BaseModel

One registered data store.

DataStoreCollection

Bases: TolerantCollection

Response of GET /data/stores.

GenerateSyntheticRequest

Bases: BaseModel

Body of POST /data/synthetic: generate a synthetic data store.

Every field is optional. Anything left out takes the same default as python -m beacon.synthetic, because the engine runs that command: the same settings always give the same data, whichever way it was made.

ImportRequest

Bases: BaseModel

Body of POST /data/import: load CSV files or an Excel workbook.

ImportResult

Bases: BaseModel

Response of POST /data/import.

strategies

Stored strategies: what makes one valid, what stops one running, and what uses an index.

A strategy is checked by the same code that builds a backtest's settings, so a strategy that saves is one a backtest accepts, and the two cannot disagree about what is valid.

read_strategy

read_strategy(
    document_id: str, document: dict[str, Any]
) -> StrategyDocument

A stored strategy as its model: the build function for reading one.

The model refuses fields it does not know, so a client sending a backtest's vehicle or period on a strategy hears about it, and the store's own version stamp is set aside before it is checked.

load_strategy

load_strategy(
    strategies: DocumentStore, strategy_id: str
) -> StrategyDocument

A stored strategy, or not-found: absent and unreadable alike.

problem_of

problem_of(
    document: StrategyDocument, indices: DocumentStore
) -> StrategyProblem | None

Why a strategy cannot be run as it stands, or None when it can.

Its index can go after it is saved: deleted through a store the refusal never saw, or not copied to this machine with the strategy.

with_problem

with_problem(
    document: StrategyDocument, indices: DocumentStore
) -> StoredStrategy

A strategy as listed and read, with any problem it has.

validate_strategy

validate_strategy(
    document: StrategyDocument, indices: DocumentStore
) -> list[Finding]

Every problem with a strategy, each at its field's path.

The strategy and implementation are checked as a backtest checks them, so their findings sit at the same paths (strategy.replication, implementation.caps.0). Its index must be stored and readable.

strategies_using

strategies_using(
    strategies: DocumentStore, index_ids: set[str]
) -> list[str]

The ids of the stored strategies that hold any of these indices.

A strategy that cannot be read is not counted: what it holds cannot be known, and refusing a delete over it would leave the index undeletable until the strategy was repaired.

strategy_runs

A stored strategy's runs, and the weights a target stands for.

Runs are kept per strategy: a backtest of a stored strategy is the job kind backtest:{strategy_id}, and its saved record is filed under the strategy's id. A run is served only when it carries its strategy's id, so a run saved before runs were kept per strategy, filed under an index's id, is never taken for a strategy of the same name.

A target for the optimiser or a risk model is an index or a strategy. An index's weights are calculated, so no run is needed; a strategy's are what its latest run chose at each rebalance: the index's for a plain index strategy, its replication's for a tracker, its construction's for an active strategy.

backtest_kind

backtest_kind(strategy_id: str) -> str

The job kind a strategy's backtests run under.

latest_run

latest_run(
    registry: JobRegistry, strategy_id: str
) -> dict[str, Any] | None

The most recent successful run of a strategy, or None.

A run of this kind that does not carry the strategy's id was saved before runs were kept per strategy, under an index of the same name, and is not this strategy's. Runs since are always newer, so only the newest needs the check.

require_run

require_run(
    registry: JobRegistry, strategy_id: str
) -> dict[str, Any]

A strategy's latest run, or not-found pointing at how to make one.

strategy_steps

strategy_steps(run: dict[str, Any]) -> list[dict[str, Any]]

What a run's strategy chose at each rebalance, as {date, weights} rows in date order.

strategy_weights

strategy_weights(
    run: dict[str, Any], as_of: str | None
) -> tuple[str, dict[str, float]]

The rebalance date and the weights a run's strategy chose in force on as_of; the last when None.

index_weights

index_weights(
    document: IndexDocument,
    index_store: DocumentStore,
    fetcher: DataFetcher,
    as_of: str | None,
) -> tuple[str, dict[str, float]]

The rebalance date and an index's weights in force on as_of, from its calculation up to that date; the latest when None.

strategy_schemas

Request and response models for stored strategies.

StrategyDocument

Bases: BaseModel

A stored strategy: what a backtest holds and how it trades.

Everything else a backtest needs (the period, capital, currency, dividends, costs, flows, vehicle, share classes and modelling assumptions) belongs to the backtest, not the strategy.

StrategyProblem

Bases: BaseModel

Why a stored strategy cannot be run as it stands.

StoredStrategy

Bases: StrategyDocument

A stored strategy as listed and read, with any problem it has.

StrategyCollection

Bases: TolerantCollection

Response of GET /strategies.

SavedStrategy

Bases: BaseModel

Response of a strategy save: the document as stored, and any warnings.

StrategyDeletion

Bases: BaseModel

Response of DELETE /strategies/{strategy_id}.

types

Serving the catalogue: turning registered classes into renderable type specs.

One adapter for both editors. The methodology editor and the optimiser's constraint editor ask the same question (what types exist, and what does each take), so they get the same answer shape and a client can render both with one component.

parameter_spec

parameter_spec(
    parameter: Parameter,
    schemas: Mapping[str, str],
    columns: Mapping[str, str] | None = None,
    options: Mapping[str, str] | None = None,
) -> ParameterSpec

One catalogue parameter as its API shape.

Parameters:

Name Type Description Default
parameter Parameter

The introspected parameter.

required
schemas Mapping[str, str]

Parameter name -> component schema, as the class declared it.

required
columns Mapping[str, str] | None

Parameter name -> the reference column its values are of.

None
options Mapping[str, str] | None

Parameter name -> the family its entries' types come from.

None

specs_for

specs_for(
    kind: str, families: Mapping[str, str] | None = None
) -> list[TypeSpec]

Every registered type of one kind, ready to serve.

Parameters:

Name Type Description Default
kind str

catalogue.SELECTION, WEIGHTING or CONSTRAINT, or one of a backtest's families.

required
families Mapping[str, str] | None

Catalogue kind -> the name it is served under, for a parameter whose entries are themselves configurable types.

None

Returns:

Name Type Description
list list[TypeSpec]

Type specs, name-ordered, each carrying its parameters in the

list[TypeSpec]

order a form should show them, and, for a constraint, the unit its

list[TypeSpec]

slack is reported in.

views

Reading a completed run: overview, weights, attribution, per-asset, compare.

A backtest is a job because it is slow. These are the panes that read what the job produced, and they must be fast: a client switching tabs should not be waiting on a recalculation. So they derive everything from the stored run rather than recomputing the index.

The stored run carries its level and metrics, plus two things that cannot be recovered from a NAV series:

  • rebalance snapshots: the weights at each rebalance, both as applied and as they would have been uncapped. Everything about composition comes from these: the weights pane reads one, attribution drifts them forward day by day, the per-asset pane reads a name's history across them, and cap drag needs the uncapped set to compare against.
  • costs and starting capital: two scalars, from which the cost drag falls out.

Daily weights are not stored. drifted_weights() reconstructs them from the snapshots and the prices.

attribute() uses Carino linking, so the contributions sum to the compounded total return rather than approximately to it. The endpoint reports the residual regardless: it should sit at machine epsilon, and one that does not means an assumption has broken somewhere upstream, which is worth surfacing rather than rounding away.

build_overview

build_overview(
    strategy_id: str,
    index_id: str,
    name: str,
    run: dict[str, Any],
) -> OverviewView

Headline view of a completed run.

build_attribution

build_attribution(
    strategy_id: str,
    index_id: str,
    run: dict[str, Any],
    fetcher: DataFetcher,
    start: str | None,
    end: str | None,
    currency: str | None = None,
) -> AttributionView

Per-constituent contributions over a window, with the two drags.

Prices are converted into currency, the index's, so each contribution includes its name's exchange-rate return and the contributions add up to the index's return.

build_asset_view

build_asset_view(
    strategy_id: str,
    index_id: str,
    identifier: str,
    run: dict[str, Any],
    fetcher: DataFetcher,
    currency: str | None = None,
) -> AssetView

One constituent: its weight history and how it fared against the index.

The comparison with the index is made in currency, the index's; the price series returned stays in the name's own currency, as a price chart should.

build_compare

build_compare(
    runs: dict[str, dict[str, Any]],
) -> CompareView

Several strategies' latest runs on one axis, over the window they all share.

Aligned rather than concatenated: two indices with different start dates would otherwise be compared over different periods, and the one with the shorter history would look better or worse for no reason but its span. Every level is rebased to 100 on the first shared date, so the lines start together and the comparison is of shape rather than of scale.

weights

The weights pane: composition at a date, per constituent.

It answers one question, what does the index hold, and how did it get there, with everything the table needs to answer it.

Each row carries two weights. raw_weight is what the weighting scheme produced; weight is what survived the cap. The pair is what makes a capped index legible: without the raw figure a reader sees several names sitting at exactly 20% and cannot tell whether the cap was binding hard on one and barely on another. Raw weights sum to 1 and applied weights sum to 1, and the weight moved between them is cap_redistributed.

The aggregate DriftPayload and every row's delta_since_rebalance come from the same held-weight vector, so the total always matches the rows it is a total of. Drift is measured against the targets the last rebalance set, not against the previous rebalance. An equal-weighted index resets to 1/n every time, so comparing consecutive rebalances would report zero drift forever; the question worth answering is how far prices have moved the index since it was last reset.

shares_outstanding is the company's shares outstanding, not shares held per index unit (the usual fact-sheet "shares" column, which needs a divisor and a notional this endpoint has neither of).

concentration_of

concentration_of(
    weights: dict[str, float],
) -> ConcentrationPayload

Concentration measures for one weight vector.

build_weights

build_weights(
    strategy_id: str,
    index_id: str,
    run: dict[str, Any],
    as_of: str | None,
    fetcher: DataFetcher,
    with_risk: bool = False,
    benchmark: dict[str, float] | None = None,
    benchmark_id: str | None = None,
    currency: str | None = None,
) -> WeightsView

Composition at a date, with per-constituent rows, drift and cap flags.

Parameters:

Name Type Description Default
strategy_id str

The strategy whose run is read.

required
index_id str

The index it holds.

required
run dict[str, Any]

The stored run, for context the snapshot does not carry.

required
as_of str | None

Date asked about; None means the latest rebalance.

required
fetcher DataFetcher

Data source, for prices and shares outstanding.

required
with_risk bool

Decompose the index's volatility across its constituents. Off by default because estimating a covariance over every name is the pane's whole cost.

False
benchmark dict[str, float] | None

Weights to measure tracking error against, if any.

None
benchmark_id str | None

What to call it in the response.

None
currency str | None

The index's currency. Prices are converted into it before weights drift or risk is estimated, so a name quoted elsewhere carries its exchange-rate moves.

None

Returns:

Name Type Description
WeightsView WeightsView

The pane's whole payload.

build_rows

build_rows(
    snapshot: RebalanceSnapshot,
    drift: dict[str, float] | None,
    as_of: str | None,
    fetcher: DataFetcher,
    contributions: RiskContributions | None = None,
    active_by_name: dict[str, float] | None = None,
    benchmark: dict[str, float] | None = None,
) -> list[ConstituentRow]

One row per constituent, heaviest first.

Parameters:

Name Type Description Default
snapshot RebalanceSnapshot

The rebalance in force.

required
drift dict[str, float] | None

Each name's held weight less its target, or None when nothing has drifted yet.

required
as_of str | None

The date shares outstanding are read at.

required
fetcher DataFetcher

Data source.

required

Returns:

Name Type Description
list list[ConstituentRow]

Rows ordered by applied weight, descending. A weights table is

list[ConstituentRow]

read from the top, so the order that matters is the one the reader

list[ConstituentRow]

cares about rather than the order the store happened to hold.

prices_for

prices_for(
    fetcher: DataFetcher,
    identifiers: list[str],
    start: str | None,
    end: str | None,
    currency: str | None = None,
) -> pd.DataFrame

Close prices for a set of names over a window, names on the columns, converted into currency (None leaves each in its own).

Raises:

Type Description
CalculationError

If a name's currency has no rate into currency.