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,
)

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.

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,
) -> 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

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

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.

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.

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.

annual_returns

annual_returns(level: Series) -> dict[str, float]

Calendar-year returns that compound exactly to the total.

Each year runs from the previous year's closing level to its own, so the product of (1 + r) telescopes to last / first - 1. Defining them any other way (from the first observation within each year, say) leaves a gap over each year boundary and the compounded total no longer matches. The first observation is the starting point, not a year of its own: a run that starts on 2 January begins from the capital held on the eve, which may fall in the year before.

Parameters:

Name Type Description Default
level Series

The level series, indexed by date.

required

Returns:

Name Type Description
dict dict[str, float]

Year (as a string) -> return for that year.

assemble_result

assemble_result(
    result: BacktestResult,
    index_result: IndexResult,
    benchmark: RelativeMetricsPayload | None = None,
    cap: float | 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

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,
    fetcher: DataFetcher,
    index_store: DocumentStore,
    start: str,
    end: str,
) -> RelativeMetricsPayload

Resolve a benchmark and measure the portfolio against it.

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 to compare against.

required
fetcher DataFetcher

Data source.

required
index_store DocumentStore

Where stored index definitions live.

required
start str

Window start, YYYY-MM-DD.

required
end str

Window end, YYYY-MM-DD.

required

Returns:

Name Type Description
RelativeMetricsPayload RelativeMetricsPayload

The comparison and the rebased benchmark.

build_backtest_job

build_backtest_job(
    document: IndexDocument,
    fetcher: DataFetcher,
    request: BacktestRequest,
    index_store: DocumentStore,
    record_store: DocumentStore | None = None,
) -> JobBody

Build the job body that runs one backtest.

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
document IndexDocument

The index definition 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.

required

Returns:

Name Type Description
JobBody JobBody

A coroutine function taking a progress reporter.

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.

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,
)

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.

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,
) -> 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

Returns:

Name Type Description
ServerConfig ServerConfig

The assembled configuration.

Raises:

Type Description
ValueError

If no token is available from either source.

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,
)

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

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.

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

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.

stream_events async

stream_events(
    queue: Queue[dict[str, Any]],
) -> AsyncIterator[dict[str, Any]]

Yield events from a subscriber queue until cancelled.

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.

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.

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.

constituent_prices

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

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

Raises:

Type Description
DataNotFoundError

If none of them can be priced.

expected_returns_from

expected_returns_from(
    prices: DataFrame,
) -> dict[str, float]

Annualised historical mean returns.

A poor forecast, and deliberately the honest one: it is what the data supports. Anything more sophisticated invented here would look like a view the server does not have.

factor_exposures

factor_exposures(
    prices: DataFrame,
    fetcher: DataFetcher,
    as_of: Timestamp,
) -> pd.DataFrame

Size, momentum and volatility loadings, standardised.

Parameters:

Name Type Description Default
prices DataFrame

Constituent prices.

required
fetcher DataFetcher

For share counts.

required
as_of Timestamp

Date the loadings are measured at.

required

Returns:

Type Description
DataFrame

pd.DataFrame: z-scored exposures, names on the index.

build_optimisation_job

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

Build the coroutine that runs an optimisation.

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_weights: dict[str, float],
    label_for: dict[str, str],
    prices: DataFrame,
    risk_model: Any,
    fetcher: DataFetcher,
) -> 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.

target_weights_from

target_weights_from(
    run: dict[str, Any], as_of: str | None
) -> dict[str, float]

The index weights an optimisation is measured against.

Reads the rebalance in force on as_of from a stored backtest, so the optimiser and the weights pane agree about what the index held.

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,
) -> 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

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.

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,
) -> pd.DataFrame

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

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.

build_estimation_job

build_estimation_job(
    model_id: str,
    request: RiskModelRequest,
    identifiers: list[str],
    fetcher: DataFetcher,
) -> 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
) -> 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_beacon_router

build_beacon_router() -> APIRouter

Build the /beacon router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest submission endpoint.

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

beacon

Beacon View: running an index's backtest, and reading the result.

A backtest calculates the whole index and then simulates a tracking 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 are the other half. They serve the panes of the view (overview, weights, attribution, a single name, and a comparison across indices), and each answers from the most recent successful run of that index rather than recalculating anything. Job results are persisted and the run payload carries composition, so a client switching tabs does not wait on a recalculation, and two panes read a moment apart describe the same run.

Every read is a 404 until a backtest has been run, which is the honest answer: there is no view of an index nobody has calculated.

build_beacon_router
build_beacon_router() -> APIRouter

Build the /beacon router.

Returns:

Name Type Description
APIRouter APIRouter

Router carrying the backtest submission endpoint.

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.

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.

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.

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.

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

A buy 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
) -> 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

BacktestRecordRow

Bases: BaseModel

One stored backtest record, as a listing knows it.

The row is deliberately thin: the id to fetch the record by, and when it was captured. Names come from the index catalogue the client already holds, and everything else from /beacon/{index_id}/record.

BacktestRecordCollection

Bases: TolerantCollection

Response of GET /beacon/backtests.

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.

WeightRow

Bases: BaseModel

One name's index, 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.

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.

BacktestRequest

Bases: BaseModel

Body of POST /beacon/{index_id}/backtest.

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 /beacon/{index_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 /beacon/{index_id}/weights.

ContributionPayload

Bases: BaseModel

One constituent's share of the index return.

AttributionView

Bases: BaseModel

Response of GET /beacon/{index_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 /beacon/{index_id}/assets/{identifier}.

CompareEntry

Bases: BaseModel

One index within a comparison, on the shared window.

CompareView

Bases: BaseModel

Response of GET /beacon/compare.

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:{index_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.

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.

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

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

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)

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

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.

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.

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.

specs_for

specs_for(kind: str) -> list[TypeSpec]

Every registered type of one kind, ready to serve.

Parameters:

Name Type Description Default
kind str

catalogue.SELECTION, WEIGHTING or CONSTRAINT.

required

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(
    index_id: str, name: str, run: dict[str, Any]
) -> OverviewView

Headline view of a completed run.

build_attribution

build_attribution(
    index_id: str,
    run: dict[str, Any],
    fetcher: DataFetcher,
    start: str | None,
    end: str | None,
) -> AttributionView

Per-constituent contributions over a window, with the two drags.

build_asset_view

build_asset_view(
    index_id: str,
    identifier: str,
    run: dict[str, Any],
    fetcher: DataFetcher,
) -> AssetView

One constituent: its weight history and how it fared against the index.

build_compare

build_compare(
    runs: dict[str, dict[str, Any]],
) -> CompareView

Several indices 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(
    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,
) -> WeightsView

Composition at a date, with per-constituent rows, drift and cap flags.

Parameters:

Name Type Description Default
index_id str

The index being read.

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

Returns:

Name Type Description
WeightsView WeightsView

The pane's whole payload.

build_rows

build_rows(
    snapshot: RebalanceSnapshot,
    held: 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
held dict[str, float] | None

Drifted weights, 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,
) -> pd.DataFrame

Close prices for a set of names over a window, names on the columns.