Skip to content

Finance

The sekai.finance toolkit. Classes are exported from sekai.finance; functions from their modules (sekai.finance.features, sekai.finance.metrics, ...). See the Finance toolkit pages for explanations and examples.

Market data

sekai.finance.data. See Market data and backtesting.

MarketData

MarketData(prices: ArrayLike | None = None, *, fields: Mapping[str, ArrayLike] | None = None, timestamps: ArrayLike | None = None, assets: Sequence[Any] | None = None, exogenous: Mapping[str, ArrayLike] | None = None, price_field: str = 'close', warmup: int = 0)

An immutable panel of market data aligned on one time axis.

Parameters:

Name Type Description Default
prices ArrayLike | None

Prices shaped (T, N) (or (T,) for one asset); stored as the field price_field. NaN marks an asset that is not listed at that time. Finite prices must be positive.

None
fields Mapping[str, ArrayLike] | None

More per-asset data, each (T, N): volumes, spreads, features. NaN is allowed; infinities are not.

None
timestamps ArrayLike | None

(T,) strictly increasing numpy.datetime64 values (or anything numpy converts to them, such as date strings) or numbers. Defaults to 0, 1, ..., T-1. Timezone-aware values are refused (see the module docstring for the explicit conversion).

None
assets Sequence[Any] | None

N unique asset identifiers. Defaults to "asset_0", ....

None
exogenous Mapping[str, ArrayLike] | None

Market-wide series, each (T,) or (T, k): interest rates, an index, macro data aligned with sekai.finance.features.asof_join.

None
price_field str

Name of the field that holds the prices.

'close'
warmup int

Number of leading rows that are lookback-only history (split and walk_forward set it): environments may observe them, but never make a decision on them. Between 0 and T.

0
Example
data = MarketData(closes, timestamps=dates, assets=tickers, fields={"volume": volumes})
train, test = data.split(0.8, embargo=5, history=60)

prices property

prices: NDArray[float64]

Prices (T, N), NaN where an asset is not listed (read-only).

mask property

mask: NDArray[bool_]

(T, N) True where the asset has a price (is listed and tradable).

fields property

Every per-asset field, including prices.

exogenous property

exogenous: Mapping[str, NDArray[float64]]

Market-wide series, each (T,) or (T, k).

warmup property

warmup: int

Leading rows that are lookback-only history: observable, but never a decision row.

split and walk_forward set it to the history rows they prepend to a test period; slicing keeps it correct (a slice starting inside the warm-up keeps the remaining warm-up rows), and it is 0 for data built directly.

periods_per_year property

periods_per_year: float | None

Rows per year implied by datetime timestamps (about 252 for trading days), else None.

from_pandas classmethod

from_pandas(prices: Any, *, fields: Mapping[str, Any] | None = None, exogenous: Any = None, price_field: str = 'close') -> MarketData

Build from wide pandas DataFrames (index = timestamps, columns = assets).

Every field is aligned to the prices' index and columns (missing → NaN). exogenous is a DataFrame (one series per column) or a mapping of name to Series/DataFrame, aligned to the same index.

from_long classmethod

from_long(frame: Any, *, time: str = 'date', asset: str = 'symbol', price: str = 'close', fields: Sequence[str] = ()) -> MarketData

Build from a long ("tidy") DataFrame with one row per (time, asset).

Columns time and asset identify the row, price holds the price, and each column in fields becomes a field of the same name.

field

field(name: str) -> NDArray[float64]

One per-asset field (T, N).

exogenous_series

exogenous_series(name: str) -> NDArray[float64]

One market-wide series.

exogenous_width

exogenous_width(name: str) -> int

Number of columns of an exogenous series (1 for a 1-D series).

log_returns

log_returns() -> NDArray[float64]

(T, N) log returns of the prices; NaN in row 0 and wherever either price is missing.

index_of

index_of(when: TimeLike, *, side: str = 'left') -> int

Row position of a point in time: a row position or a timestamp.

  • An int is a row position on every time axis (negative counts from the end), returned as is.
  • A timestamp gives the first row at or after it (after it with side="right"), or n_steps if there is none. On datetime timestamps it is anything numpy.datetime64 accepts (a datetime64, datetime, date, pandas Timestamp or date string); on numeric timestamps, a float.
  • Time(t) is a timestamp on either kind of axis, so it also looks up integer timestamps (epoch seconds, or the timestamps of a split).

Raises:

Type Description
TypeError

If when is a bool.

ValueError

If a timestamp does not suit the axis (a date on numeric timestamps, a number on datetime ones), is NaN or NaT, or is timezone-aware.

between

between(start: TimeLike | None = None, end: TimeLike | None = None) -> MarketData

Rows from start up to, not including, end (either bound optional).

Each bound is read as by index_of: timestamps select start <= timestamp < end (between("2020-01-01", "2021-01-01") is the rows of 2020, and on numeric timestamps between(Time(100), Time(200)) the rows stamped 100 to 199), while an int is a row position (between(100, 200) is data[100:200]).

select

select(assets: Sequence[Any]) -> MarketData

A subset of assets, by identifier.

with_fields

with_fields(**arrays: ArrayLike) -> MarketData

A copy with fields added or replaced (each (T, N)).

with_exogenous

with_exogenous(**arrays: ArrayLike) -> MarketData

A copy with exogenous series added or replaced (each (T,) or (T, k)).

split

split(*boundaries: TimeLike, embargo: int = 0, history: int = 0) -> list[MarketData]

Split chronologically at the given points.

A point is a fraction of the rows (a float in (0, 1), such as 0.8), a row position (an int, counted within this data on every time axis, so test.split(50) cuts test at its own row 50; negative counts from the end), or a timestamp: on datetime timestamps anything numpy.datetime64 accepts ("2021-01-01"), on any axis Time(t) (as in index_of; a float is a fraction here, so numeric timestamps need Time). Every part after the first starts embargo rows after its split point (a gap that keeps information from straddling the boundary) and then prepends history rows before that start, which an environment may observe as lookback without trading on them. The part records them as its warmup, and the finance environments never make a decision on a warm-up row, whatever their lookback (a shorter lookback just observes fewer of them). Pass at least the env's lookback as history so the first decision of a test episode can fall right at the start of the test period.

Example
train, val, test = data.split(0.6, 0.8, embargo=5, history=lookback)

walk_forward

walk_forward(train_size: int, test_size: int, *, step: int | None = None, embargo: int = 0, history: int = 0, expanding: bool = False) -> Iterator[tuple[MarketData, MarketData]]

Rolling (or expanding) train/test folds for walk-forward evaluation.

Fold k trains on train_size rows (all rows so far with expanding=True) and tests on the next test_size rows after an embargo gap, with history extra rows before the test period available as lookback (recorded as the test part's warmup, so no decision falls on them). Folds advance by step rows (default: test_size, so test periods tile the data without overlap).

Time dataclass

Time(value: Any)

A timestamp to look up by value, where a plain int would be a row position.

MarketData.index_of, between and split, and reset(options={"start": ...}) in the finance environments, read an int as a row position on every time axis. Time(t) is always a timestamp: the first row at or after t is used. It is how to address numeric timestamps that are integers, such as epoch seconds or the timestamps of a split (which no longer start at 0). Since info["timestamp"] reports an episode's decision times, options={"start": Time(info["timestamp"])} replays an episode on any data.

Parameters:

Name Type Description Default
value Any

A number on numeric timestamps; on datetime timestamps anything numpy.datetime64 accepts (a datetime64, datetime, date, pandas Timestamp or date string).

required

Raises:

Type Description
TypeError

If value is neither a number nor a date or time.

Example
data = MarketData(prices, timestamps=epoch_seconds)
before, after = data.split(Time(1_700_000_000))

Sampling

sekai.finance.sampling. See Episode samplers.

EpisodeSampler

Bases: ABC

Chooses the first row of each episode's data window.

sample abstractmethod

sample(rng: Generator, n_starts: int, episode_length: int) -> int

A window start in [0, n_starts).

Parameters:

Name Type Description Default
rng Generator

The env's generator (use it for any randomness).

required
n_starts int

Number of valid window starts.

required
episode_length int

Steps per episode (for samplers that tile the history).

required

reset

reset() -> None

Forget any state (e.g. a sequential sampler's position).

spawn

spawn() -> EpisodeSampler

A sampler for one more independent environment (e.g. another sub-env of a VecEnv).

make_vec() and native vector envs call it once per sub-env. The default returns this sampler itself, shared, so later changes to it (a curriculum raising a limit, say) reach every environment. Override it when the sampler keeps per-environment state, as SequentialStart does with its position. (ProcVecEnv workers always hold pickled copies, which later changes in the parent cannot reach.)

RandomStart

Bases: EpisodeSampler

Uniformly random windows: the default for training.

SequentialStart

SequentialStart(start: int = 0, stride: int | None = None)

Bases: EpisodeSampler

Consecutive windows through the history, for deterministic evaluation.

The first episode starts at start; each later one stride rows on (default: the episode length, so episodes tile the history without overlap). Handing out the last window that fits increases epoch and wraps around to start, so while sampler.epoch == 0: run_episode() visits every window exactly once. Rows after the last full window are not used.

spawn

spawn() -> SequentialStart

A fresh copy (same start and stride) with its own position and epoch.

FixedStart

FixedStart(start: int)

Bases: EpisodeSampler

Every episode starts at the same window (replaying one period).

as_sampler

as_sampler(sampler: str | int | EpisodeSampler) -> EpisodeSampler

Normalise a sampler argument: "random", "sequential", a fixed int, or a sampler.

sample_start

sample_start(sampler: EpisodeSampler, rng: Generator, *, n_starts: int, episode_length: int, lookback: int, warmup: int = 0) -> int

A window start from sampler whose decisions all fall after the data's warm-up rows.

The sampler chooses among the valid starts only: its position k is the window starting at row first_start(lookback, warmup) + k. Without warm-up rows (or with lookback >= warmup) that is exactly the sampler's own choice.

Raises:

Type Description
ValueError

If no window fits after the warm-up rows, or the sampler cannot place an episode (with warm-up rows, the message gives the positions in terms of the windows after them).

first_start

first_start(lookback: int, warmup: int) -> int

The first window start whose first decision (row start + lookback) is not a warm-up row.

resolve_start

resolve_start(option: Any, *, timestamps: Any, lookback: int, n_starts: int, warmup: int = 0) -> int

The window start for reset(options={"start": option}).

option places the episode's first decision, read as MarketData.index_of reads it: an int is a row position on every time axis, and a timestamp puts the decision on the first row at or after it (on datetime timestamps anything numpy.datetime64 accepts, on numeric ones a float, on either Time(t)). The window begins lookback rows earlier. info["timestamp"] reports the decision time, so start=Time(info["timestamp"]) replays an episode on any data, including a split whose numeric timestamps no longer start at 0. A first decision on one of the data's warmup rows (lookback-only history of a split) is refused.

Raises:

Type Description
TypeError

If option is a bool.

ValueError

If option is a timestamp that does not suit the axis (such as a date string on numeric timestamps), or the first decision would fall outside the valid range: without lookback rows before it, on a warm-up row, or without a full episode after it.

Simulators

sekai.finance.simulators. See Market simulators.

MarketModel

Bases: ABC

A generator of synthetic price paths for a fixed universe of n_assets assets.

Subclasses implement simulate, which must be vectorised over paths and assets, return strictly positive prices wherever an asset is listed and honour the shapes documented on Simulation.

n_assets abstractmethod property

n_assets: int

Number of assets (the size of the last axis of Simulation.prices).

dt abstractmethod property

dt: float

Step length in years.

state_spec

state_spec() -> dict[str, tuple[int, ...]]

Keys of Simulation.state mapped to their per-step trailing shape.

For example {"regime": (), "variance": (n_assets,)}. Empty by default.

simulate abstractmethod

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate price paths.

Parameters:

Name Type Description Default
rng Generator

Source of randomness.

required
n_steps int

Number of steps (>= 1); the time axis has n_steps + 1 entries.

required
n_paths int | None

Number of independent paths (>= 1). None gives a single path without a leading paths axis; an int (even 1) adds a leading axis of that size.

None

Returns:

Type Description
Simulation

A Simulation with prices of shape (n_steps + 1, n_assets) or

Simulation

(n_paths, n_steps + 1, n_assets).

Raises:

Type Description
ValueError

If n_steps or n_paths is smaller than 1.

TypeError

If n_steps or n_paths is not an integer.

Simulation dataclass

Simulation(prices: NDArray[float64], mask: NDArray[bool_] | None = None, state: Mapping[str, NDArray[float64]] = dict())

Simulated market paths returned by MarketModel.simulate.

Attributes:

Name Type Description
prices NDArray[float64]

Prices of shape (n_steps + 1, n_assets), or (n_paths, n_steps + 1, n_assets) when simulated with n_paths. Row 0 (along the time axis) is the initial price. NaN where an asset is not listed.

mask NDArray[bool_] | None

Boolean array shaped like prices, True where the asset is listed and tradable, or None when every entry is valid.

state Mapping[str, NDArray[float64]]

Latent or auxiliary series keyed by name (see MarketModel.state_spec). Each array's leading dimensions equal prices.shape[:-1], i.e. (n_steps + 1,) or (n_paths, n_steps + 1), followed by the model's per-step shape for that key.

Raises:

Type Description
ValueError

If the shapes of prices, mask and state are inconsistent.

GBM

GBM(mu: ArrayLike = 0.0, sigma: ArrayLike = 0.2, *, s0: ArrayLike = 100.0, corr: ArrayLike | None = None, dt: float = _DEFAULT_DT, n_assets: int | None = None)

Bases: _Model

Geometric Brownian motion with the exact log-normal transition.

log S_{t+1} = log S_t + (mu - sigma^2 / 2) dt + sigma sqrt(dt) z, with z correlated across assets by corr. Without n_paths it draws random numbers in the same order as sekai.envs.finance.market.simulate_gbm, so both give identical paths for the same seed.

Parameters:

Name Type Description Default
mu ArrayLike

Annualised drift, scalar or shape (n_assets,).

0.0
sigma ArrayLike

Annualised volatility (>= 0), scalar or shape (n_assets,).

0.2
s0 ArrayLike

Initial price (> 0), scalar or shape (n_assets,).

100.0
corr ArrayLike | None

Optional (n_assets, n_assets) correlation matrix of the assets' shocks.

None
dt float

Step length in years.

_DEFAULT_DT
n_assets int | None

Number of assets; needed only to broadcast all-scalar parameters to more than one asset (otherwise inferred from the parameters or corr).

None

Raises:

Type Description
ValueError

On inconsistent shapes or out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate GBM paths; see MarketModel.simulate. The state is empty.

Heston

Heston(mu: ArrayLike = 0.05, kappa: ArrayLike = 2.0, theta: ArrayLike = 0.04, xi: ArrayLike = 0.3, rho: ArrayLike = -0.7, *, v0: ArrayLike | None = None, s0: ArrayLike = 100.0, corr: ArrayLike | None = None, dt: float = _DEFAULT_DT, n_assets: int | None = None)

Bases: _Model

Heston stochastic-volatility model, simulated with full-truncation Euler.

With v+ = max(v, 0) at each step: log S += (mu - v+ / 2) dt + sqrt(v+ dt) z1 and v += kappa (theta - v+) dt + xi sqrt(v+ dt) z2, where corr(z1, z2) = rho per asset and z1 is correlated across assets by corr.

Parameters:

Name Type Description Default
mu ArrayLike

Annualised drift, scalar or shape (n_assets,).

0.05
kappa ArrayLike

Mean-reversion speed of the variance (> 0), per year.

2.0
theta ArrayLike

Long-run variance (> 0), annualised.

0.04
xi ArrayLike

Volatility of variance (> 0).

0.3
rho ArrayLike

Correlation between price and variance shocks, in [-1, 1].

-0.7
v0 ArrayLike | None

Initial variance (>= 0); defaults to theta.

None
s0 ArrayLike

Initial price (> 0).

100.0
corr ArrayLike | None

Optional (n_assets, n_assets) correlation matrix of the price shocks.

None
dt float

Step length in years.

_DEFAULT_DT
n_assets int | None

Number of assets when every parameter is a scalar.

None
State

"variance": per-step shape (n_assets,), the truncated variance v+ at each time.

Raises:

Type Description
ValueError

On inconsistent shapes or out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate Heston paths; see MarketModel.simulate.

MertonJumpDiffusion

MertonJumpDiffusion(mu: ArrayLike = 0.05, sigma: ArrayLike = 0.2, jump_intensity: ArrayLike = 1.0, jump_mean: ArrayLike = -0.05, jump_std: ArrayLike = 0.1, *, s0: ArrayLike = 100.0, corr: ArrayLike | None = None, dt: float = _DEFAULT_DT, n_assets: int | None = None)

Bases: _Model

Merton jump-diffusion: GBM plus compound-Poisson log-normal jumps.

Each step's log increment is (mu - sigma^2 / 2 - lambda k) dt + sigma sqrt(dt) z plus the sum of N ~ Poisson(lambda dt) iid Normal(jump_mean, jump_std^2) log jumps, where k = exp(jump_mean + jump_std^2 / 2) - 1 compensates the jumps so that E[S_t] = s0 exp(mu t). Diffusive shocks are correlated across assets by corr; jumps are independent across assets.

Parameters:

Name Type Description Default
mu ArrayLike

Annualised drift (including the jump compensator).

0.05
sigma ArrayLike

Annualised diffusive volatility (>= 0).

0.2
jump_intensity ArrayLike

Expected number of jumps per year (>= 0).

1.0
jump_mean ArrayLike

Mean of a single log jump.

-0.05
jump_std ArrayLike

Standard deviation of a single log jump (>= 0).

0.1
s0 ArrayLike

Initial price (> 0).

100.0
corr ArrayLike | None

Optional (n_assets, n_assets) correlation matrix of the diffusive shocks.

None
dt float

Step length in years.

_DEFAULT_DT
n_assets int | None

Number of assets when every parameter is a scalar.

None
State

"jumps": per-step shape (n_assets,), the number of jumps in the step ending at t (0 at t = 0), as float64.

Raises:

Type Description
ValueError

On inconsistent shapes or out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate jump-diffusion paths; see MarketModel.simulate.

RegimeSwitching

RegimeSwitching(regimes: Sequence[GBM], transition: ArrayLike, *, initial: ArrayLike | None = None)

Bases: _Model

GBM whose drift, volatility and correlation follow a hidden Markov chain of regimes.

The regime at time t governs the increment from t to t + 1. The chain starts from initial at t = 0 and moves according to transition after every step.

Parameters:

Name Type Description Default
regimes Sequence[GBM]

K >= 1 GBM models giving (mu, sigma, corr) for each regime. They must share n_assets, dt and s0.

required
transition ArrayLike

(K, K) row-stochastic matrix; entry (i, j) is the per-step probability of moving from regime i to regime j.

required
initial ArrayLike | None

Distribution of the regime at t = 0, shape (K,). Defaults to the stationary distribution of transition.

None
State

"regime": per-step shape (), the regime index at each time as float64.

Raises:

Type Description
TypeError

If a regime is not a GBM.

ValueError

On inconsistent regimes or an invalid transition matrix or distribution.

s0 property

Initial prices, shape (n_assets,) (shared by every regime).

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate regime-switching paths; see MarketModel.simulate.

GARCH

GARCH(omega: ArrayLike = 1e-06, alpha: ArrayLike = 0.08, beta: ArrayLike = 0.9, *, mu: ArrayLike = 0.0, s0: ArrayLike = 100.0, corr: ArrayLike | None = None, dt: float = _DEFAULT_DT, n_assets: int | None = None)

Bases: _Model

GARCH(1,1) volatility clustering on per-step log returns.

eps_t = sqrt(h_t) z_t, log return_t = mu dt + eps_t and h_{t+1} = omega + alpha eps_t^2 + beta h_t, starting from the unconditional variance h_0 = omega / (1 - alpha - beta). The z_t are correlated across assets by corr.

Parameters:

Name Type Description Default
omega ArrayLike

Per-step (not annualised) variance intercept (> 0).

1e-06
alpha ArrayLike

ARCH coefficient (>= 0).

0.08
beta ArrayLike

GARCH coefficient (>= 0); alpha + beta must be < 1.

0.9
mu ArrayLike

Annualised drift of the log price.

0.0
s0 ArrayLike

Initial price (> 0).

100.0
corr ArrayLike | None

Optional (n_assets, n_assets) correlation matrix of the shocks z.

None
dt float

Step length in years.

_DEFAULT_DT
n_assets int | None

Number of assets when every parameter is a scalar.

None
State

"variance": per-step shape (n_assets,), the conditional variance h_t of the increment starting at t.

Raises:

Type Description
ValueError

On inconsistent shapes or out-of-range parameters.

unconditional_variance property

unconditional_variance: NDArray[float64]

Per-step unconditional variance omega / (1 - alpha - beta), shape (n_assets,).

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate GARCH(1,1) paths; see MarketModel.simulate.

MeanReverting

MeanReverting(theta: ArrayLike = 5.0, mean: ArrayLike = 100.0, sigma: ArrayLike = 0.2, *, s0: ArrayLike = 100.0, corr: ArrayLike | None = None, dt: float = _DEFAULT_DT, n_assets: int | None = None)

Bases: _Model

Exponential Ornstein-Uhlenbeck prices: the log price reverts to log(mean).

With x = log S and m = log(mean), the exact discretisation is x_{t+1} = m + (x_t - m) e^{-theta dt} + sigma sqrt((1 - e^{-2 theta dt}) / (2 theta)) z, with z correlated across assets by corr.

Parameters:

Name Type Description Default
theta ArrayLike

Mean-reversion speed (> 0), per year.

5.0
mean ArrayLike

Price level the log price reverts to (> 0).

100.0
sigma ArrayLike

Annualised volatility of the log price (>= 0).

0.2
s0 ArrayLike

Initial price (> 0).

100.0
corr ArrayLike | None

Optional (n_assets, n_assets) correlation matrix of the shocks.

None
dt float

Step length in years.

_DEFAULT_DT
n_assets int | None

Number of assets when every parameter is a scalar.

None

Raises:

Type Description
ValueError

On inconsistent shapes or out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate exponential OU paths; see MarketModel.simulate. The state is empty.

CointegratedPair

CointegratedPair(mu: float = 0.05, sigma: float = 0.2, theta: float = 5.0, spread_sigma: float = 0.1, *, s0: ArrayLike = (100.0, 100.0), dt: float = _DEFAULT_DT)

Bases: _Model

Two cointegrated assets: a shared GBM trend plus a stationary OU spread.

The common log trend x (x_0 = 0) is a Brownian motion with drift (mu - sigma^2 / 2) and volatility sigma; the spread s (s_0 = 0) is a zero-mean OU process discretised exactly. log P1 = log s0[0] + x_t + s_t / 2 and log P2 = log s0[1] + x_t - s_t / 2, so each price wanders while log(P1 / P2) - log(s0[0] / s0[1]) = s_t mean-reverts.

Parameters:

Name Type Description Default
mu float

Annualised drift of the common trend (scalar).

0.05
sigma float

Annualised volatility of the common trend (scalar, >= 0).

0.2
theta float

Mean-reversion speed of the spread (scalar, > 0), per year.

5.0
spread_sigma float

Annualised volatility of the spread (scalar, >= 0).

0.1
s0 ArrayLike

Initial prices, a scalar or a length-2 sequence (> 0).

(100.0, 100.0)
dt float

Step length in years.

_DEFAULT_DT
State

"spread": per-step shape (), the spread s_t.

Raises:

Type Description
ValueError

On non-scalar or out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate the pair; see MarketModel.simulate.

BlockBootstrap

BlockBootstrap(returns: ArrayLike, *, block_size: int = 20, s0: ArrayLike = 100.0, dt: float = _DEFAULT_DT)

Bases: _Model

Circular block bootstrap of historical log returns.

Each path concatenates blocks of block_size consecutive rows of returns, each starting at a uniformly random row and wrapping around circularly, truncated to n_steps rows. Whole rows are resampled, so cross-asset correlation and short-range dependence are preserved.

Parameters:

Name Type Description Default
returns ArrayLike

Historical per-step log returns, shape (T, n_assets) or (T,) for one asset.

required
block_size int

Rows per block, 1 <= block_size <= T.

20
s0 ArrayLike

Initial price (> 0), scalar or shape (n_assets,).

100.0
dt float

Step length in years of the historical data.

_DEFAULT_DT

Raises:

Type Description
ValueError

If returns are empty or non-finite, or block_size is out of range.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Resample paths; see MarketModel.simulate. The state is empty.

ListingProcess

ListingProcess(model: MarketModel, *, initial_listed: ArrayLike = 0.8, listing_rate: ArrayLike = 1.0, delisting_rate: ArrayLike = 0.25, min_listed: int = 1)

Bases: _Model

A dynamic universe: assets of an inner model list and delist over time.

Each asset follows an independent two-state Markov chain. At t = 0 it is listed with probability initial_listed; after every step an unlisted asset lists with probability 1 - exp(-listing_rate dt) and a listed asset delists with probability 1 - exp(-delisting_rate dt). Whenever fewer than min_listed assets are listed (also at t = 0), the lowest-index unlisted assets are listed until min_listed are. In the long run an asset is listed a fraction p_list / (p_list + p_delist) of the steps, where p_list and p_delist are the two step probabilities above; this tends to listing_rate / (listing_rate + delisting_rate) as dt -> 0.

Prices come from the inner model's path; where unlisted they are NaN and mask is False. If the inner simulation has its own mask, the two are combined with a logical AND (min_listed applies only to this process's own listing flags). The inner model's state passes through unchanged.

Parameters:

Name Type Description Default
model MarketModel

The inner market model.

required
initial_listed ArrayLike

Probability that an asset is listed at t = 0, in [0, 1] (scalar or shape (n_assets,)).

0.8
listing_rate ArrayLike

Listing intensity per year for an unlisted asset (>= 0).

1.0
delisting_rate ArrayLike

Delisting intensity per year for a listed asset (>= 0).

0.25
min_listed int

Minimum number of listed assets at every time, 0 <= min_listed <= n_assets.

1

Raises:

Type Description
TypeError

If model is not a MarketModel.

ValueError

On out-of-range parameters.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate the inner model, then the listing flags; see MarketModel.simulate.

Randomized

Randomized(factory: Callable[[Generator], MarketModel])

Bases: _Model

Domain randomisation: a freshly parameterised model for every simulated path.

factory(rng) builds a model with randomly drawn parameters. The model returned for np.random.default_rng(0) is kept as reference and fixes n_assets, dt and state_spec; every later model must match them.

Parameters:

Name Type Description Default
factory Callable[[Generator], MarketModel]

Callable mapping a generator to a MarketModel.

required

Raises:

Type Description
TypeError

If factory is not callable or does not return a MarketModel.

ValueError

(from simulate) if a built model's n_assets, dt or state_spec differ from the reference.

simulate

simulate(rng: Generator, n_steps: int, *, n_paths: int | None = None) -> Simulation

Simulate one freshly built model per path; see MarketModel.simulate.

Features

sekai.finance.features. See Point-in-time features.

Causal (point-in-time) feature transforms for market data, plus a look-ahead detector.

Every transform works along axis 0 (time) of an array shaped (T,) or (T, N) for N assets, treats NaN as missing data (an asset not yet listed, a halted day, ...) and is causal: the output at time t depends only on inputs at times <= t. Features can therefore be computed once over a whole history and sliced inside an environment without leaking the future. Results are float64 arrays of the input's shape unless stated otherwise.

check_causal verifies the causality property empirically for any transform, and asof_join aligns irregularly timed data (macro releases, earnings) to bar timestamps without look-ahead.

log_returns

log_returns(prices: ArrayLike) -> NDArray[float64]

One-period log returns log(P_t / P_{t-1}).

Parameters:

Name Type Description Default
prices ArrayLike

Prices shaped (T,) or (T, N).

required

Returns:

Type Description
NDArray[float64]

Log returns of the same shape. Row 0 is NaN, as is any entry where either price

NDArray[float64]

is missing (non-finite) or not strictly positive.

simple_returns

simple_returns(prices: ArrayLike) -> NDArray[float64]

One-period simple returns P_t / P_{t-1} - 1.

Parameters:

Name Type Description Default
prices ArrayLike

Prices shaped (T,) or (T, N).

required

Returns:

Type Description
NDArray[float64]

Simple returns of the same shape, with the NaN rules of log_returns.

momentum

momentum(prices: ArrayLike, window: int) -> NDArray[float64]

Trailing log return over window periods, log(P_t / P_{t-window}).

Parameters:

Name Type Description Default
prices ArrayLike

Prices shaped (T,) or (T, N).

required
window int

Look-back in periods (>= 1); window=1 equals log_returns.

required

Returns:

Type Description
NDArray[float64]

Momentum of the same shape; NaN for t < window or where either price is missing

NDArray[float64]

or not strictly positive.

Raises:

Type Description
ValueError

If window < 1.

lag

lag(x: ArrayLike, periods: int = 1) -> NDArray[float64]

Shift a series down (later in time) by periods rows.

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
periods int

Number of rows to shift by (>= 0).

1

Returns:

Type Description
NDArray[float64]

Array of the same shape whose row t is row t - periods of x; the first

NDArray[float64]

periods rows are NaN.

Raises:

Type Description
ValueError

If periods < 0.

forward_fill

forward_fill(x: ArrayLike, *, limit: int | None = None) -> NDArray[float64]

Carry the last valid (non-NaN) value forward along time.

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
limit int | None

Maximum number of consecutive NaNs to fill after a valid value (None = unlimited, 0 = no filling).

None

Returns:

Type Description
NDArray[float64]

Filled array of the same shape. Leading NaNs (before the first valid value)

NDArray[float64]

stay NaN, as do NaNs more than limit rows after the last valid value.

Raises:

Type Description
ValueError

If limit < 0.

rolling_mean

rolling_mean(x: ArrayLike, window: int, *, min_periods: int | None = None) -> NDArray[float64]

Trailing rolling mean over rows t-window+1..t, ignoring NaNs.

Matches pandas rolling(window, min_periods).mean().

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
window int

Window length in rows (>= 1).

required
min_periods int | None

Minimum number of valid values required in the window (1 <= min_periods <= window; default: window).

None

Returns:

Type Description
NDArray[float64]

Rolling mean of the same shape; NaN where the window holds fewer than

NDArray[float64]

min_periods valid values.

Raises:

Type Description
ValueError

If window or min_periods is out of range.

rolling_std

rolling_std(x: ArrayLike, window: int, *, min_periods: int | None = None, ddof: int = 1) -> NDArray[float64]

Trailing rolling standard deviation over rows t-window+1..t, ignoring NaNs.

Matches pandas rolling(window, min_periods).std(ddof).

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
window int

Window length in rows (>= 1).

required
min_periods int | None

Minimum number of valid values required in the window (1 <= min_periods <= window; default: window).

None
ddof int

Delta degrees of freedom (>= 0); the divisor is n_valid - ddof.

1

Returns:

Type Description
NDArray[float64]

Rolling standard deviation of the same shape; NaN where the window holds fewer

NDArray[float64]

than max(min_periods, ddof + 1) valid values.

Raises:

Type Description
ValueError

If window, min_periods or ddof is out of range.

rolling_zscore

rolling_zscore(x: ArrayLike, window: int, *, min_periods: int | None = None) -> NDArray[float64]

Trailing rolling z-score (x_t - mean) / std over rows t-window+1..t.

The mean and the (ddof=1) standard deviation are those of rolling_mean and rolling_std over the same window, which includes x_t itself.

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
window int

Window length in rows (>= 1).

required
min_periods int | None

Minimum number of valid values required in the window (1 <= min_periods <= window; default: window). At least 2 are always needed.

None

Returns:

Type Description
NDArray[float64]

Z-scores of the same shape: NaN where x_t is NaN or the window holds too few

NDArray[float64]

valid values, and 0.0 where the window's standard deviation is 0.

Raises:

Type Description
ValueError

If window or min_periods is out of range.

ewm_mean

ewm_mean(x: ArrayLike, halflife: float, *, min_periods: int = 1) -> NDArray[float64]

Exponentially weighted moving average m_t = (1 - a) m_{t-1} + a x_t.

a = 1 - exp(log(0.5) / halflife) and the recursion starts at the first valid value. Without NaNs this matches pandas ewm(halflife=halflife, adjust=False).mean().

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
halflife float

Half-life in rows (> 0).

required
min_periods int

Number of valid values that must have been seen before a value is emitted (>= 1).

1

Returns:

Type Description
NDArray[float64]

EWMA of the same shape. A NaN input leaves the state unchanged and gives NaN at

NDArray[float64]

that t; the output is also NaN until min_periods valid values have been seen.

Raises:

Type Description
ValueError

If halflife <= 0 or min_periods < 1.

ewm_std

ewm_std(x: ArrayLike, halflife: float, *, min_periods: int = 2) -> NDArray[float64]

Exponentially weighted moving standard deviation.

On each valid x_t, with d = x_t - m_{t-1}: m_t = m_{t-1} + a d and v_t = (1 - a) (v_{t-1} + a d^2), starting from m = x, v = 0 at the first valid value; the output is sqrt(v_t). a is as in ewm_mean.

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T,) or (T, N).

required
halflife float

Half-life in rows (> 0).

required
min_periods int

Number of valid values that must have been seen before a value is emitted (>= 1).

2

Returns:

Type Description
NDArray[float64]

EW standard deviation of the same shape, with the NaN rules of ewm_mean.

Raises:

Type Description
ValueError

If halflife <= 0 or min_periods < 1.

realized_volatility

realized_volatility(returns: ArrayLike, window: int, *, periods_per_year: float | None = None) -> NDArray[float64]

Trailing realised volatility: rolling_std(returns, window) (ddof=1).

Parameters:

Name Type Description Default
returns ArrayLike

Per-period returns shaped (T,) or (T, N).

required
window int

Window length in rows (>= 1); a full window of valid returns is required.

required
periods_per_year float | None

If given (> 0), annualise by multiplying by sqrt(periods_per_year), e.g. 252 for daily returns.

None

Returns:

Type Description
NDArray[float64]

Volatility of the same shape, per period or annualised.

Raises:

Type Description
ValueError

If window < 1 or periods_per_year <= 0.

cross_sectional_zscore

cross_sectional_zscore(x: ArrayLike) -> NDArray[float64]

Z-score each row across assets: (x - mean) / std over the row's valid entries.

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T, N).

required

Returns:

Type Description
NDArray[float64]

(T, N) z-scores using the population (ddof=0) standard deviation. NaN entries

NDArray[float64]

stay NaN; rows with fewer than 2 valid entries are all NaN; rows whose valid

NDArray[float64]

entries are all equal give 0.0.

Raises:

Type Description
ValueError

If x is not 2-D.

cross_sectional_rank

cross_sectional_rank(x: ArrayLike) -> NDArray[float64]

Rank each row across assets, scaled to [0, 1].

Valid entries get (rank - 1) / (n_valid - 1), where rank is the 1-based average rank (ties share the mean of their ranks).

Parameters:

Name Type Description Default
x ArrayLike

Data shaped (T, N).

required

Returns:

Type Description
NDArray[float64]

(T, N) scaled ranks. NaN entries stay NaN; a row with a single valid entry gives

NDArray[float64]

0.5 for it.

Raises:

Type Description
ValueError

If x is not 2-D.

asof_join

asof_join(target_times: ArrayLike, event_times: ArrayLike, values: ArrayLike, *, lag: timedelta64 | timedelta | float | None = None) -> NDArray[float64]

Align irregularly timed data to target timestamps without look-ahead.

For each target time t the result holds the value of the last event with event_time + lag <= t, i.e. the latest information that was public at t. Typical uses are macro releases or earnings (with lag modelling the publication delay) aligned to bar timestamps.

Parameters:

Name Type Description Default
target_times ArrayLike

(T,) target timestamps, sorted ascending.

required
event_times ArrayLike

(E,) event timestamps, sorted ascending; among equal times the last event wins. Either both time arrays are np.datetime64 or both numeric.

required
values ArrayLike

(E,) or (E, k) event values.

required
lag timedelta64 | timedelta | float | None

Non-negative delay before an event becomes known: a np.timedelta64 (or datetime.timedelta) for datetime64 times, a number for numeric times, or None for no delay.

None

Returns:

Type Description
NDArray[float64]

float64 array shaped (T,) or (T, k); NaN where no event is known yet.

Raises:

Type Description
TypeError

If the time arrays are of mixed or unsupported kinds, or lag does not match their kind.

ValueError

If a time array is not 1-D, contains NaN/NaT or is not sorted, if values does not have E rows, or if lag is negative or NaN/NaT.

check_causal

check_causal(fn: Callable[[NDArray[float64]], ArrayLike], x: ArrayLike, *, n_checks: int = 5, rng: Generator | None = None, rtol: float = 0.0, atol: float = 0.0) -> None

Empirically check that a transform has no look-ahead bias.

For cut point 0 and n_checks random cut points c in [1, T-2] (every such point when n_checks >= T - 2), x is copied with the rows after c perturbed in several ways, and a causal fn must give the same output, of the same shape, for rows 0..c on every copy. Cut point 0 is always tested: a leak into the first row only (a backward fill of the leading NaN of returns, a normalisation by row 1) shows nowhere else. The perturbations are:

  • rescaled: valid entries multiplied by exp(Normal(0, 0.1));
  • reordered: the rows after c permuted;
  • values changed so that signs and zeros move, within the data's domain: for data with negative values, signs flipped at random plus additive noise, and some entries set to 0; for non-negative data with zeros, |x + noise| with some entries set to 0 (strictly positive data, such as prices, stay positive);
  • for data with NaNs, the NaN pattern changed: some valid entries become NaN and the NaNs are filled with values drawn from x (this catches survivorship bias, e.g. dropping assets that delist later).

Perturbed inputs can be outside what fn expects downstream of the cut, so numpy floating-point warnings are silenced while fn runs on them.

Parameters:

Name Type Description Default
fn Callable[[NDArray[float64]], ArrayLike]

Transform mapping an array shaped like x to an array whose axis 0 is aligned with x's time axis. It must be deterministic.

required
x ArrayLike

Representative input data shaped (T, ...) with T >= 3.

required
n_checks int

Number of random cut points to test besides 0 (>= 1).

5
rng Generator | None

Source of randomness for the cut points and perturbations (default: np.random.default_rng(0)).

None
rtol float

Relative tolerance for the comparison (as in np.allclose).

0.0
atol float

Absolute tolerance for the comparison (as in np.allclose).

0.0

Raises:

Type Description
LookAheadError

If an output at or before a cut point changed, naming the first time index that differs and the cut point, or if the output's shape changed (e.g. a column per asset that is still listed at the end).

ValueError

If T < 3, n_checks < 1, a tolerance is negative, or fn's output is not aligned with x's time axis.

Costs

sekai.finance.costs. See Cost models.

CostModel

Bases: ABC

Cost of trading trades (weight changes, fraction of equity) as a fraction of equity.

NoCost

Bases: CostModel

Frictionless trading.

ProportionalCost

ProportionalCost(rate: ArrayLike)

Bases: CostModel

rate * |trade| per asset: commissions and fees proportional to traded value.

Parameters:

Name Type Description Default
rate ArrayLike

Cost per unit of traded value; a scalar or one rate per asset (finite,

= 0).

required

SpreadCost

SpreadCost(half_spread: ArrayLike | None = None, *, fallback_spread: float | None = None)

Bases: CostModel

Crossing half the bid-ask spread on every trade.

Parameters:

Name Type Description Default
half_spread ArrayLike | None

Half-spread as a fraction of the price (scalar or per asset; finite, >= 0). If None, the env's quoted spread (full spread, as a fraction of the price) is used instead; an asset whose spread is missing (NaN) or negative is charged the cross-sectional median spread, while a zero spread is valid and costs nothing.

None
fallback_spread float | None

Full spread (fraction of the price) charged when no asset has a valid quoted spread. Without it, trading on such a row raises ValueError rather than trading for free. Only used with quoted spreads.

None

FixedCost

FixedCost(amount: float, *, threshold: float = 1e-06)

Bases: CostModel

A fixed fee in currency for every asset traded (a ticket charge).

Parameters:

Name Type Description Default
amount float

Fee per traded asset, in the same currency as the env's capital (finite, >= 0).

required
threshold float

Weight changes at or below this size are not counted as trades (finite, >= 0; the default is above float32 rounding, so echoing back observed weights is free).

1e-06

SquareRootImpact

SquareRootImpact(coefficient: float = 0.5, *, fallback_volatility: float | None = None)

Bases: CostModel

Square-root market impact: coefficient * vol * sqrt(traded / volume) per unit traded.

The empirical "square-root law" of impact (e.g. Almgren et al. 2005; Tóth et al. 2011): trading a fraction q / V of an asset's volume moves its price by about coefficient * sigma * sqrt(q / V), and the trade pays that on its whole size, so the cost grows as |q|^1.5. Large rebalances in illiquid names become expensive, which a proportional cost cannot express.

Needs equity (currency), volatility (per-step) and volume (traded value per step, currency) from the environment. The impact per unit traded is capped at 1 (the whole traded value), which is also what trading an asset with zero or missing volume costs: there is no liquidity to trade against. A missing (NaN) volatility, e.g. a name with no return history yet, is replaced by the cross-sectional median of the other assets; a zero volatility (a flat window) is a valid estimate and gives zero impact. When no asset has a volatility, trading raises ValueError unless fallback_volatility is given. Environments refuse this model with lookback=1, which gives no estimate.

Parameters:

Name Type Description Default
coefficient float

Impact coefficient (finite, >= 0; order 0.1-1 in empirical studies).

0.5
fallback_volatility float | None

Per-step volatility used when no asset has a valid volatility estimate. Without it such trades raise instead of being free.

None

CompositeCost

CompositeCost(models: Sequence[CostModel])

Bases: CostModel

The sum of several cost models (build it with +).

as_cost_model

as_cost_model(cost: float | CostModel) -> CostModel

A cost model from a CostModel or a proportional rate.

Account

sekai.finance.account. See Account rules.

AccountConfig dataclass

AccountConfig(allow_short: bool = False, max_leverage: float = 1.0, cash_rate: float = 0.0, margin_rate: float | None = None, borrow_rate: float = 0.0)

Constraints and financing terms of a trading account.

Parameters:

Name Type Description Default
allow_short bool

Allow negative weights.

False
max_leverage float

Cap on gross exposure sum(|w|) (1.0 = no leverage); finite and positive.

1.0
cash_rate float

Annual interest earned on positive cash; finite (negative rates are allowed).

0.0
margin_rate float | None

Annual interest paid on borrowed cash (default: cash_rate); finite.

None
borrow_rate float

Annual fee on the value of short positions; finite and non-negative.

0.0

Raises:

Type Description
ValueError

If a numeric field is NaN, infinite or out of its range.

project_weights

project_weights(scores: ArrayLike, tradable: ArrayLike, config: AccountConfig, *, cash_score: ArrayLike | None = None) -> Array

Map raw action values to target risky-asset weights that satisfy the account's rules.

Long-only: scores (and an optional cash_score) are clipped at zero and normalised into weights summing to at most max_leverage (to exactly max_leverage when there is no cash score and some score is positive); all-zero scores mean all cash. Long/short: values are clipped to [-max_leverage, max_leverage] and scaled down if gross exposure exceeds max_leverage. Untradable assets get weight 0 in both cases.

Parameters:

Name Type Description Default
scores ArrayLike

(..., n) action values per asset.

required
tradable ArrayLike

(..., n) bool, False for assets that cannot be held.

required
config AccountConfig

The account rules.

required
cash_score ArrayLike | None

(...) long-only score of cash (the "stay out" option).

None

Returns:

Type Description
Array

(..., n) target weights.

Settlement dataclass

Settlement(growth: Array, drifted: Array, financing: Array, wiped_out: NDArray[bool_])

The outcome of holding weights over one period (arrays broadcast over batch dims).

settle

settle(weights: ArrayLike, returns: ArrayLike, *, cost: ArrayLike, dt: float, config: AccountConfig) -> Settlement

Hold weights (set after paying cost) over a period with asset returns.

Parameters:

Name Type Description Default
weights ArrayLike

(..., n) risky weights held over the period.

required
returns ArrayLike

(..., n) simple returns of the assets over the period (use 0 for assets that are not held or have no price).

required
cost ArrayLike

(...) trading cost already paid, as a fraction of equity.

required
dt float

Period length in years (for financing).

required
config AccountConfig

Account financing terms.

required

turnover

turnover(target: ArrayLike, current: ArrayLike) -> Any

sum(|target - current|) over the last axis (cash excluded).

Rewards

sekai.finance.rewards. See Reward functions.

StepOutcome dataclass

StepOutcome(log_return: Any, simple_return: Any, equity: Any, turnover: Any, cost: Any, pnl: Any = None)

What happened to the account over one step.

Fields are floats for a single env, or arrays of shape (num_envs,).

Attributes:

Name Type Description
log_return Any

Log growth of equity over the step, net of all costs.

simple_return Any

exp(log_return) - 1.

equity Any

Equity after the step, relative to the start of the episode.

turnover Any

Sum of absolute weight changes traded at the start of the step.

cost Any

Trading cost paid, as a fraction of equity.

pnl Any

Change in equity over the step, in units of the initial equity (exact, also on a wipe-out), or None if the environment did not supply the equity before the step.

from_growth classmethod

from_growth(growth: ArrayLike, *, equity: ArrayLike, turnover: ArrayLike, cost: ArrayLike, previous_equity: ArrayLike | None = None) -> StepOutcome

Build from the equity multiplier of the step (floored, so a wipe-out is finite).

Parameters:

Name Type Description Default
growth ArrayLike

Equity multiplier of the step (0 or below on a wipe-out).

required
equity ArrayLike

Equity after the step, relative to the start of the episode.

required
turnover ArrayLike

Sum of absolute weight changes traded.

required
cost ArrayLike

Trading cost paid, as a fraction of equity.

required
previous_equity ArrayLike | None

Equity before the step. When given, pnl is equity - previous_equity; environments should always pass it, since the equity lost on a wipe-out cannot be recovered from equity alone.

None

RewardFunction

Bases: ABC

Maps a StepOutcome to a reward (float, or an array for batched outcomes).

reset

reset(mask: NDArray[bool_] | None = None) -> None

Start new episodes: all of them (mask=None) or the sub-envs where mask is True.

LogReturn

Bases: RewardFunction

Log growth of equity. Summed over an episode: log(final / initial equity).

SimpleReturn

Bases: RewardFunction

Percentage return of the step (P&L as a fraction of current equity).

ProfitAndLoss

Bases: RewardFunction

Change in equity, in units of the initial equity (summed: final equity - 1).

Uses StepOutcome.pnl, which every built-in environment supplies, so the step that wipes the account out is rewarded with the whole equity it lost. For an outcome without pnl the change is recovered from the equity after the step, which is only possible while that equity is positive; a wipe-out then raises ValueError.

MeanVariance

MeanVariance(risk_aversion: float = 1.0)

Bases: RewardFunction

r - (risk_aversion / 2) * r^2 on the log return: a per-step mean-variance utility.

TurnoverPenalty

Bases: RewardFunction

The step's turnover; subtract it to discourage over-trading beyond explicit costs.

DrawdownPenalty

DrawdownPenalty()

Bases: _Stateful

How much the drawdown deepened this step (0 when equity is at or recovering to a peak).

The drawdown is 1 - equity / running_peak; summed over an episode the penalty is the total amount by which drawdowns deepened.

DifferentialSharpe

DifferentialSharpe(eta: float = 0.01)

Bases: _Stateful

The differential Sharpe ratio of Moody & Saffell (2001).

An online, per-step reward whose sum approximately maximises the Sharpe ratio of the step returns: with exponential moving estimates A (mean) and B (second moment) of the returns,

D_t = (B dA - A dB / 2) / (B - A^2)^(3/2), dA = R_t - A, dB = R_t^2 - B.

It is 0 until the variance estimate becomes positive.

Parameters:

Name Type Description Default
eta float

Adaptation rate of the moving estimates (about 1 / window length).

0.01

Metrics

sekai.finance.metrics. See Performance metrics.

Trading performance metrics, and a wrapper that reports them per episode.

The metric functions take a 1-D series of simple per-period returns (0.01 is +1 % over the period), check that it is non-empty and finite (raising ValueError otherwise) and return plain Python floats. Annualisation uses periods_per_year: 252 for daily bars, 52 for weekly, 12 for monthly, 252 * 390 for one-minute bars of a US equity session.

Wipe-outs follow a limited-liability convention, as the finance envs do: equity cannot fall below zero. A return of -100 % or worse wipes the account out and equity stays at zero from then on, so total_return is at least -1, drawdowns lie in [0, 1] and annualized_return is -1. Dispersion and tail statistics (volatility, Sharpe, Sortino, VaR, CVaR, hit rate) use the returns exactly as given.

Returns are undefined for P&L processes that start from zero wealth (e.g. a market maker's mark-to-market P&L); summarise those with pnl_summary, which works on per-period P&L increments in absolute units.

RecordPerformance wraps an env whose info reports account equity (or cumulative P&L) and attaches a PerformanceSummary (or PnLSummary) to the info of each episode's final step.

PerformanceSummary dataclass

PerformanceSummary(n_periods: int, total_return: float, annualized_return: float, annualized_volatility: float, sharpe_ratio: float, sortino_ratio: float, max_drawdown: float, calmar_ratio: float, value_at_risk: float, conditional_value_at_risk: float, hit_rate: float, mean_turnover: float | None = None, total_cost: float | None = None)

Performance metrics of one return series (e.g. one episode).

Return, volatility, drawdown and VaR fields are fractions (0.05 is 5 %); ratios are annualised. See the module functions of the same names for the exact definitions.

Attributes:

Name Type Description
n_periods int

Number of return periods.

total_return float

Compounded simple return over the series.

annualized_return float

Geometric annualised return.

annualized_volatility float

Annualised sample standard deviation of returns.

sharpe_ratio float

Annualised Sharpe ratio.

sortino_ratio float

Annualised Sortino ratio (may be inf).

max_drawdown float

Maximum drawdown, in [0, 1].

calmar_ratio float

Annualised return over maximum drawdown (may be inf).

value_at_risk float

Historical per-period VaR as a positive loss.

conditional_value_at_risk float

Historical per-period expected shortfall.

hit_rate float

Fraction of non-zero periods with a positive return.

mean_turnover float | None

Mean per-period turnover, if turnover was supplied.

total_cost float | None

Sum of per-period trading costs, if costs were supplied.

as_dict

as_dict() -> dict[str, float | int | None]

The fields as a plain dict, e.g. for logging.

PnLSummary dataclass

PnLSummary(n_periods: int, total_pnl: float, mean_pnl: float, pnl_volatility: float, sharpe_ratio: float, max_drawdown: float, hit_rate: float)

Performance of a P&L process measured in absolute units (no returns).

Attributes:

Name Type Description
n_periods int

Number of periods.

total_pnl float

Sum of per-period P&L.

mean_pnl float

Mean per-period P&L.

pnl_volatility float

Sample standard deviation of per-period P&L (ddof=1); 0.0 when n < 2.

sharpe_ratio float

mean_pnl / pnl_volatility * sqrt(periods_per_year); 0.0 when the standard deviation is zero or n < 2.

max_drawdown float

Largest fall of cumulative P&L from its running peak, in absolute units (>= 0); the running peak starts at 0.

hit_rate float

Fraction of non-zero periods with positive P&L.

as_dict

as_dict() -> dict[str, float | int]

The fields as a plain dict, e.g. for logging.

RecordPerformance

RecordPerformance(env: Env[ObsType, ActType], *, periods_per_year: float = 252, equity_key: str = 'equity', pnl_key: str | None = None, turnover_key: str | None = 'turnover', cost_key: str | None = 'cost', risk_free: float = 0.0, deque_size: int = 100)

Bases: Wrapper[ObsType, ActType, ObsType, ActType]

Reports trading performance metrics at the end of every episode.

Equity mode (default, pnl_key None) reads account equity from info[equity_key] on every reset and step. Each step's simple return is equity_t / equity_{t-1} - 1, where the previous equity is the last step's or, on the first step, the reset info's. If the reset info has no equity_key, the starting equity is assumed to be 1.0. A step that takes equity to zero or below is a wipe-out: its return is -1 (-100 %) and the series ends there, i.e. every later return in the episode is 0.

P&L mode (pnl_key given) reads cumulative wealth or P&L in absolute units from info[pnl_key]; it may start at zero or be negative. Each step's P&L is the change since the previous value (from the reset info, else 0.0), and episodes are summarised with pnl_summary, with the episode's largest absolute value as the rounding scale (a P&L that is constant up to the rounding of the wealth level has zero volatility).

In equity mode, info[turnover_key] and info[cost_key] are collected from the steps that report them and summarised as the mean turnover and the total cost (None if no step reported them). P&L mode ignores them.

On the final step of an episode, the summary is attached to that step's info as info["performance"] (in a new dict; the inner info is never modified), appended to summaries and exposed as last_summary, and a new episode starts. Around an inner AutoReset, the terminal value is read from info["final_info"] and the next episode's starting value from info["reset_info"], so back-to-back episodes are summarised correctly without reset() reaching this wrapper. A reset() mid-episode discards the unfinished episode.

Parameters:

Name Type Description Default
env Env[ObsType, ActType]

Environment whose info reports equity (or cumulative P&L).

required
periods_per_year float

Steps per year, for annualisation.

252
equity_key str

Info key holding account equity (equity mode).

'equity'
pnl_key str | None

Info key holding cumulative P&L; selects P&L mode when given.

None
turnover_key str | None

Info key holding per-step turnover, or None to ignore.

'turnover'
cost_key str | None

Info key holding per-step trading cost, or None to ignore.

'cost'
risk_free float

Annual simple risk-free rate for the Sharpe and Sortino ratios.

0.0
deque_size int

Number of recent episode summaries kept in summaries.

100

Raises:

Type Description
ValueError

If a numeric argument is out of range.

TypeError

If a key argument is not a string (or None where allowed).

summary

summary() -> dict[str, float]

Mean of each numeric summary field over the stored episodes.

Returns:

Type Description
dict[str, float]

Field name to mean value across summaries; fields that are None in

dict[str, float]

every stored summary are left out. Empty if no episode has finished.

total_return

total_return(returns: ArrayLike) -> float

Compounded return over the whole series, prod(1 + r) - 1.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required

Returns:

Type Description
float

Total simple return; -1.0 after a wipe-out (never lower).

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite.

annualized_return

annualized_return(returns: ArrayLike, periods_per_year: float = 252) -> float

Geometric annualised return, prod(1 + r) ** (periods_per_year / n) - 1.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252

Returns:

Type Description
float

Compound annual growth rate; -1.0 if equity ever reaches zero or below.

float

May be inf when a short, strongly positive series is annualised.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or periods_per_year is not positive.

annualized_volatility

annualized_volatility(returns: ArrayLike, periods_per_year: float = 252) -> float

Annualised volatility, std(r, ddof=1) * sqrt(periods_per_year).

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252

Returns:

Type Description
float

Annualised standard deviation of returns; 0.0 when n < 2 or the returns are

float

constant up to floating-point rounding.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or periods_per_year is not positive.

sharpe_ratio

sharpe_ratio(returns: ArrayLike, periods_per_year: float = 252, risk_free: float = 0.0) -> float

Annualised Sharpe ratio of returns in excess of a risk-free rate.

mean(e) / std(e, ddof=1) * sqrt(periods_per_year) with e = r - risk_free / periods_per_year.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252
risk_free float

Annual simple risk-free rate (e.g. 0.04 for 4 %).

0.0

Returns:

Type Description
float

The Sharpe ratio; 0.0 when n < 2 or the excess returns are constant

float

(zero standard deviation, up to floating-point rounding). Excess returns

float

within rounding of zero count as exactly zero.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or a parameter is invalid.

sortino_ratio

sortino_ratio(returns: ArrayLike, periods_per_year: float = 252, target: float = 0.0) -> float

Annualised Sortino ratio: mean excess return over downside deviation.

With e = r - target / periods_per_year, the downside deviation is sqrt(mean(min(e, 0) ** 2)) over all periods and the ratio is mean(e) / dd * sqrt(periods_per_year).

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252
target float

Annual minimum acceptable return (simple rate).

0.0

Returns:

Type Description
float

The Sortino ratio. With no period below target, math.inf if the mean

float

excess return is positive, else 0.0. Excess returns within floating-point

float

rounding of zero count as exactly zero, so a series that earns exactly the

float

target (e.g. cash at the risk-free rate) gives 0.0.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or a parameter is invalid.

drawdown_series

drawdown_series(returns: ArrayLike) -> NDArray[float64]

Drawdown from the running peak after each period.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required

Returns:

Type Description
NDArray[float64]

1 - equity / running_peak(equity) for the equity path starting at 1,

NDArray[float64]

shape (n + 1,), values in [0, 1] (1 means wiped out). The first value

NDArray[float64]

is always 0.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite.

max_drawdown

max_drawdown(returns: ArrayLike) -> float

Maximum drawdown as a positive fraction of the running peak.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required

Returns:

Type Description
float

The largest value of drawdown_series, in [0, 1].

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite.

calmar_ratio

calmar_ratio(returns: ArrayLike, periods_per_year: float = 252) -> float

Calmar ratio: annualised return over maximum drawdown.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252

Returns:

Type Description
float

annualized_return / max_drawdown. With no drawdown, math.inf if

float

the annualised return is positive, else 0.0.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or periods_per_year is not positive.

value_at_risk

value_at_risk(returns: ArrayLike, level: float = 0.95) -> float

Historical value at risk, reported as a positive loss.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
level float

Confidence level, strictly between 0 and 1.

0.95

Returns:

Type Description
float

-quantile(returns, 1 - level) (linear interpolation between order

float

statistics): the per-period loss not exceeded with probability level.

float

Negative if even the tail quantile is a gain.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or level is outside (0, 1).

conditional_value_at_risk

conditional_value_at_risk(returns: ArrayLike, level: float = 0.95) -> float

Historical expected shortfall (CVaR), reported as a positive loss.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
level float

Confidence level, strictly between 0 and 1.

0.95

Returns:

Type Description
float

Minus the mean of the returns at or below quantile(returns, 1 - level).

float

Always at least value_at_risk at the same level.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or level is outside (0, 1).

hit_rate

hit_rate(returns: ArrayLike) -> float

Fraction of active (non-zero) periods with a positive return.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns (or P&L increments), shape (n,).

required

Returns:

Type Description
float

Winning periods over non-zero periods, in [0, 1]; 0.0 if every period

float

is zero.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite.

equity_curve

equity_curve(returns: ArrayLike, initial: float = 1.0) -> NDArray[float64]

Equity path implied by compounding simple returns.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
initial float

Starting equity (positive, any currency unit).

1.0

Returns:

Type Description
NDArray[float64]

Equity, shape (n + 1,): initial followed by initial * cumprod(1 + r).

NDArray[float64]

Once equity reaches zero or below (a return of -100 % or worse) it stays

NDArray[float64]

at zero.

Raises:

Type Description
ValueError

If returns is empty, not 1-D or not finite, or initial is not positive and finite.

returns_from_equity

returns_from_equity(equity: ArrayLike) -> NDArray[float64]

Simple returns between consecutive equity values (the inverse of equity_curve).

Parameters:

Name Type Description Default
equity ArrayLike

Equity path, shape (n + 1,) with n >= 1, starting positive. The first value at or below zero is a wipe-out, and every value after it must be 0 (equity stays at zero, as equity_curve reports it).

required

Returns:

Type Description
NDArray[float64]

Simple returns equity[t + 1] / equity[t] - 1, shape (n,); a wipe-out

NDArray[float64]

gives a return of exactly -1 (-100 %) and the periods after it 0, as

NDArray[float64]

RecordPerformance records them.

Raises:

Type Description
ValueError

If equity has fewer than two values, is not 1-D or finite, does not start positive, or is not 0 after a wipe-out.

performance_summary

performance_summary(returns: ArrayLike, periods_per_year: float = 252, *, risk_free: float = 0.0, level: float = 0.95, turnover: ArrayLike | None = None, costs: ArrayLike | None = None) -> PerformanceSummary

Compute every return-series metric at once.

Parameters:

Name Type Description Default
returns ArrayLike

Simple per-period returns, shape (n,).

required
periods_per_year float

Number of return periods in a year.

252
risk_free float

Annual simple risk-free rate; the hurdle for both the Sharpe and the Sortino ratio.

0.0
level float

Confidence level for VaR and CVaR, strictly between 0 and 1.

0.95
turnover ArrayLike | None

Optional per-period turnover (e.g. traded notional as a fraction of equity), shape (m,). It need not cover every period.

None
costs ArrayLike | None

Optional per-period trading costs, shape (k,), in the units the env reports them (typically a fraction of equity).

None

Returns:

Type Description
PerformanceSummary

A PerformanceSummary; mean_turnover / total_cost are None when

PerformanceSummary

turnover / costs are not given.

Raises:

Type Description
ValueError

If any series is empty, not 1-D or not finite, or a parameter is invalid.

pnl_summary

pnl_summary(pnl: ArrayLike, periods_per_year: float = 252, *, scale: float = 0.0) -> PnLSummary

Summarise a series of per-period P&L increments.

Parameters:

Name Type Description Default
pnl ArrayLike

Per-period P&L increments in absolute units, shape (n,).

required
periods_per_year float

Number of periods in a year (annualises the Sharpe ratio).

252
scale float

Size of the cumulative wealth or P&L that the increments are differences of, such as its largest absolute value. A difference of values near scale carries rounding of order eps * scale, so a standard deviation within that of zero counts as zero (and the Sharpe ratio is 0), as for returns. 0 (the default) treats the increments as exact; RecordPerformance passes the episode's largest absolute value.

0.0

Returns:

Type Description
PnLSummary

A PnLSummary.

Raises:

Type Description
ValueError

If pnl is empty, not 1-D or not finite, periods_per_year is not positive, or scale is negative or not finite.

Universe

sekai.finance.universe. See Slots and dynamic universes.

SlotMap

SlotMap(max_slots: int)

Assigns up to max_slots instruments to stable slot indices.

Example
slots = SlotMap(3)
slots.update(["AAPL", "MSFT"])          # -> [0, 1]
slots.update(["MSFT", "NVDA", "AMZN"])  # AAPL left: -> [1, 0, 2]

assets property

assets: tuple[Any, ...]

The instrument in each slot (None for empty slots).

mask property

mask: NDArray[int8]

1 for occupied slots.

reset

reset() -> None

Forget every assignment.

update

update(present: Iterable[Hashable]) -> NDArray[intp]

Set the current universe; return each present instrument's slot, in order.

Instruments that are no longer present free their slots; new ones take the lowest free slots. changed marks slots whose owner differs from before the call (freed or newly assigned), e.g. to reset per-slot state.

Raises:

Type Description
ValueError

If more instruments are present than there are slots.

slot_of

slot_of(asset: Hashable) -> int | None

The slot an instrument occupies, or None.

Order book

sekai.finance.lob. See Order books.

OrderBook

OrderBook()

A price-time priority limit order book for one instrument, in integer price ticks.

Limit orders match against the opposite side while the prices cross and rest any remainder; market orders walk the opposite side and drop any unfilled remainder. Executions always happen at the resting order's price. Self-trades are allowed. Every accepted order, including one that fills completely on arrival, is assigned a new order id, increasing in arrival order (ids restart at 1 after clear).

Complexity: inserting or removing a price level is O(log L) search on a sorted list of L prices; cancelling or reducing an order is O(1); matching is O(1) per fill.

Attributes:

Name Type Description
time float

Current book time. A plain attribute: the caller (or the background flow) advances it, and it stamps accepted orders and fills.

last_price int | None

Price of the most recent fill, or a reference price set by the caller (ZeroIntelligenceFlow.initialise sets it to the initial mid); None before either.

limit

limit(side: Side, price: int, quantity: int, owner: int = BACKGROUND) -> tuple[int | None, list[Fill]]

Submit a limit order.

Parameters:

Name Type Description Default
side Side

Side.BID to buy or Side.ASK to sell (or the integers 1 / -1).

required
price int

Limit price in ticks, an integer >= 1.

required
quantity int

Quantity, an integer >= 1.

required
owner int

Owner id; defaults to BACKGROUND.

BACKGROUND

Returns:

Type Description
int | None

(order_id, fills): the id of the resting remainder, or None if the order

list[Fill]

filled completely on arrival (its id still appears in fills as

tuple[int | None, list[Fill]]

taker_order_id), and the executions in matching order.

Raises:

Type Description
ValueError

If side, price, quantity or owner is invalid.

market

market(side: Side, quantity: int, owner: int = BACKGROUND) -> tuple[int, list[Fill]]

Submit a market order: walk the opposite side and drop any unfilled remainder.

Parameters:

Name Type Description Default
side Side

Side.BID to buy or Side.ASK to sell (or the integers 1 / -1).

required
quantity int

Quantity, an integer >= 1.

required
owner int

Owner id; defaults to BACKGROUND.

BACKGROUND

Returns:

Type Description
int

(order_id, fills): the taker order's id and the executions in matching order

list[Fill]

(empty if the opposite side is empty).

Raises:

Type Description
ValueError

If side, quantity or owner is invalid.

cancel

cancel(order_id: int) -> bool

Cancel a resting order.

Parameters:

Name Type Description Default
order_id int

Id returned by limit.

required

Returns:

Type Description
bool

True if the order was resting and is now cancelled; False if the id is unknown

bool

or the order has already left the book.

reduce

reduce(order_id: int, quantity: int) -> bool

Reduce a resting order's remaining quantity, keeping its time priority.

Parameters:

Name Type Description Default
order_id int

Id returned by limit.

required
quantity int

New remaining quantity, an integer smaller than the current one; 0 cancels the order.

required

Returns:

Type Description
bool

True if the order was resting and has been reduced (or cancelled); False if

bool

the id is unknown or the order has already left the book.

Raises:

Type Description
ValueError

If quantity is not an integer >= 0, or is not smaller than the order's current remaining quantity.

clear

clear() -> None

Remove every resting order and return the book to its freshly constructed state.

Resets time to 0, last_price to None and the order-id counter to 1.

get

get(order_id: int) -> Order | None

The resting order with this id (a live record: do not mutate it), or None.

best_bid

best_bid() -> int | None

Highest resting bid price in ticks, or None if the bid side is empty.

best_ask

best_ask() -> int | None

Lowest resting ask price in ticks, or None if the ask side is empty.

mid

mid() -> float | None

(best_bid + best_ask) / 2 in ticks, or None unless both sides are quoted.

spread

spread() -> int | None

best_ask - best_bid in ticks, or None unless both sides are quoted.

volume_at

volume_at(side: Side, price: int) -> int

Total resting quantity on side at price (0 if there is no such level).

n_levels

n_levels(side: Side) -> int

Number of non-empty price levels on side.

depth

Aggregated depth of the best levels price levels on each side.

Parameters:

Name Type Description Default
levels int

Number of levels per side, an integer >= 0.

required

Returns:

Type Description
NDArray[int64]

(bid_prices, bid_sizes, ask_prices, ask_sizes), each an int64 array of shape

NDArray[int64]

(levels,), best level first. Missing levels have price 0 and size 0.

Raises:

Type Description
ValueError

If levels is not an integer >= 0.

orders_of

orders_of(owner: int) -> list[Order]

Resting orders of owner, oldest first (live records: do not mutate them).

n_orders_of

n_orders_of(owner: int) -> int

Number of resting orders of owner.

queue_ahead

queue_ahead(order_id: int) -> int

Total resting quantity ahead of an order in its price level's queue.

Parameters:

Name Type Description Default
order_id int

Id of a resting order.

required

Returns:

Type Description
int

The quantity that must trade at the order's price before it starts to fill.

Raises:

Type Description
KeyError

If the order is not resting in the book.

Side

Bases: IntEnum

Side of an order. The value is the sign of the owner's position change on a fill.

opposite property

opposite: Side

The other side of the book.

Order dataclass

Order(order_id: int, side: Side, price: int, quantity: int, owner: int, time: float)

A resting limit order.

The objects returned by OrderBook.get and OrderBook.orders_of are the book's live internal records: read them, but never mutate them (use OrderBook.cancel and OrderBook.reduce). Once an order leaves the book, by a complete fill, a cancellation or OrderBook.clear, its quantity is 0.

Attributes:

Name Type Description
order_id int

Unique identifier assigned by the book, increasing in arrival order.

side Side

Side.BID (buy) or Side.ASK (sell).

price int

Limit price in integer ticks, >= 1.

quantity int

Remaining quantity; > 0 while the order rests.

owner int

Owner id: BACKGROUND for background flow, an agent id otherwise.

time float

Book time at which the order was accepted.

Fill dataclass

Fill(price: int, quantity: int, maker_order_id: int, taker_order_id: int, maker_owner: int, taker_owner: int, taker_side: Side, time: float)

One execution between a resting (maker) order and an incoming (taker) order.

Attributes:

Name Type Description
price int

Execution price in ticks: the maker order's limit price.

quantity int

Executed quantity, >= 1.

maker_order_id int

Id of the resting order.

taker_order_id int

Id of the incoming order.

maker_owner int

Owner of the resting order.

taker_owner int

Owner of the incoming order.

taker_side Side

Side of the incoming order (Side.BID for a buy that lifted an ask).

time float

Book time of the execution.

BACKGROUND module-attribute

BACKGROUND: int = -1

Owner id of orders submitted by background flow (e.g. ZeroIntelligenceFlow).

ZeroIntelligenceFlow dataclass

ZeroIntelligenceFlow(limit_rate: float = 10.0, market_rate: float = 2.0, cancel_rate: float = 0.5, max_offset: int = 10, offset_decay: float = 0.5, mean_size: float = 3.0)

Poisson background order flow for an OrderBook.

In the spirit of Smith, Farmer, Gillemot & Krishnamurthy (2003) and Cont, Stoikov & Talreja (2010): limit orders at random distances from the opposite best quote, market orders, and cancellations of resting background orders, each arriving as an independent Poisson process. All orders are submitted with owner BACKGROUND.

  • Limit orders arrive at limit_rate per side. A buy is placed k ticks below the best ask and a sell k ticks above the best bid, with k in 1..max_offset and P(k) proportional to exp(-offset_decay * (k - 1)), so a limit order never crosses on arrival (except where prices are clipped at 1 tick). If the opposite side is empty the reference is one tick beyond the same side's best quote, or book.last_price if the book is empty.
  • Market orders arrive at market_rate per side and take the opposite side; an order that finds the opposite side empty is skipped.
  • Cancellations remove a uniformly random resting background order, at cancel_rate per resting background order. Orders of other owners (agents) are never cancelled.
  • Sizes are 1 + Poisson(mean_size - 1), so their mean is mean_size.

If an event leaves one side of the book empty, a background limit order is placed on that side at once, so the book stays two-sided.

Volatility is governed mostly by cancel_rate relative to limit_rate. With the defaults the book holds about 30 resting orders, the spread is about one tick and the mid diffuses by roughly 0.5 ticks per unit time; cancel_rate=1.0 gives a thinner book and about 1.3 ticks, while a low rate such as 0.1 lets the queues at the best quotes grow so long that the mid barely moves.

Parameters:

Name Type Description Default
limit_rate float

Limit orders per unit time, per side, > 0.

10.0
market_rate float

Market orders per unit time, per side, > 0.

2.0
cancel_rate float

Cancellations per resting background order per unit time, >= 0.

0.5
max_offset int

Largest limit-order distance from the opposite best quote in ticks, >= 1.

10
offset_decay float

Decay rate of the offset distribution per tick, >= 0 (0 is uniform).

0.5
mean_size float

Mean order size, >= 1.

3.0

Raises:

Type Description
ValueError

If a parameter is out of range.

initialise

initialise(book: OrderBook, rng: Generator, *, mid: int, levels: int = 10, orders_per_level: int = 3) -> None

Clear book and seed it with a symmetric two-sided background book.

Bids rest at mid - 1, ..., mid - levels and asks at mid + 1, ..., mid + levels (prices clipped to >= 1 tick), each level holding orders_per_level background orders of random size. The book's time is reset to 0 and book.last_price is set to mid as the reference price.

Parameters:

Name Type Description Default
book OrderBook

The book to (re)initialise.

required
rng Generator

Source of randomness.

required
mid int

Initial mid price in ticks, an integer >= 1.

required
levels int

Price levels per side, an integer >= 1.

10
orders_per_level int

Orders per price level, an integer >= 1.

3

Raises:

Type Description
ValueError

If mid, levels or orders_per_level is invalid.

advance

advance(book: OrderBook, rng: Generator, until: float) -> list[Fill]

Simulate background events from book.time up to time until.

Events arrive as competing Poisson clocks with total rate 2 * limit_rate + 2 * market_rate + cancel_rate * n, where n is the number of resting background orders, recomputed after each event. book.time is set to each event's time as it happens and to until at the end.

Parameters:

Name Type Description Default
book OrderBook

The book to drive; it may also hold other owners' (agents') orders.

required
rng Generator

Source of randomness.

required
until float

End time, finite and >= book.time.

required

Returns:

Type Description
list[Fill]

Every fill that occurred, in order, including fills against non-background

list[Fill]

orders.

Raises:

Type Description
ValueError

If until is not finite or is earlier than book.time, or if a limit order is needed while the book is empty and book.last_price is None (call initialise first).

Rendering

sekai.finance.rendering: charts for dashboards of market environments. The classes are also exported from sekai.finance.

PriceChart

PriceChart(price: Source, *, position: Source | None = None, quotes: tuple[Source, Source] | None = None, spread: tuple[Source, Source] | None = None, buys: Source | None = None, sells: Source | None = None, context: Source | None = None, reference: float | Source | None = None, reference_label: str | None = None, title: str | None = 'Price', tag: bool = True, min_trade: float = 1e-06, width: float = 1.0, height: float = 1.0)

Bases: Panel

A price over time, with the agent's position, trades and quotes.

Every argument after price is optional:

Parameters:

Name Type Description Default
price Source

The price per step (a mid price, for market makers).

required
position Source | None

The position held over the step that just ended, signed (long > 0, short < 0): the background is shaded in the up colour while long and down while short, deeper for larger positions, and every change is marked as a trade (a triangle at the price it was decided at).

None
quotes tuple[Source, Source] | None

(bid, ask) sources: the agent's own quotes over the step that just ended, drawn as thin lines below and above the price.

None
spread tuple[Source, Source] | None

(bid, ask) sources: the market's best bid and ask, drawn as a band.

None
buys Source | None

Prices at which the agent bought during the step that just ended (a number, a sequence of numbers, or None).

None
sells Source | None

Likewise for sales.

None
context Source | None

The prices before the episode, ending with the price at its start (e.g. a lookback window), read once per episode and drawn muted.

None
reference float | Source | None

A price level drawn as a dashed line (an arrival or entry price, a strike): a number or a source.

None
reference_label str | None

Legend label for the reference level.

None
title str | None

Panel title.

'Price'
tag bool

Show the latest price in a tag on the y-axis.

True
min_trade float

The smallest change in position marked as a trade.

1e-06
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

fills

fills(view: View) -> dict[str, list[tuple[float, float]]]

The agent's trades as {"buys": [(step, price), ...], "sells": [...]}.

By default from the buys and sells sources; override to derive them from other recorded state.

OrderBookChart

OrderBookChart(book: Source, *, owners: Mapping[int, tuple[str, ColorSpec]] | Source | None = None, levels: int = 8, title: str | None = 'Order book', width: float = 1.0, height: float = 1.0)

Bases: Panel

A limit order book as a depth ladder around the mid price.

Asks are above the mid and bids below, one row per price tick, each with a bar for the resting volume. The orders of highlighted owners are drawn inside the bars at their place in the queue (the volume ahead of them on the left), so you see how close an order is to being filled.

Parameters:

Name Type Description Default
book Source

A source of the sekai.finance.OrderBook.

required
owners Mapping[int, tuple[str, ColorSpec]] | Source | None

Owners whose orders to highlight: {owner_id: (label, colour)}, or a source of such a mapping. The colour is a theme role, hex, RGB or slot.

None
levels int

Price ticks shown on each side of the mid.

8
title str | None

Panel title.

'Order book'
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

UniverseChart

UniverseChart(weights: Source, *, available: Source | None = None, labels: Sequence[str] | Source | None = None, color: ColorSpec = 'accent', negative_color: ColorSpec = 'down', title: str | None = 'Universe', width: float = 1.0, height: float = 1.0)

Bases: Panel

A changing universe over time: one row per slot, one column per step.

A cell is filled with the slot's colour while its asset is held (deeper for a larger weight), drawn in the grid colour while the asset is tradable but not held, and left empty while the slot has no tradable asset, so listings, delistings and rotations show as the rows fill and empty.

Parameters:

Name Type Description Default
weights Source

A source of the weight of each slot.

required
available Source | None

A source of whether each slot is tradable now.

None
labels Sequence[str] | Source | None

Slot labels (asset names), a sequence or a source; shown in the gutter.

None
color ColorSpec

The colour of held cells (theme role, hex, RGB or slot).

'accent'
negative_color ColorSpec

The colour of cells held short.

'down'
title str | None

Panel title.

'Universe'
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0