Changelog¶
All notable changes to sekai are documented here. The format follows Keep a Changelog and the project uses Semantic Versioning (pre-1.0, minor versions may change behaviour).
Unreleased¶
0.2.0 - 2026-10-20¶
The first release with built-in environments, a finance toolkit, a test suite, CI and a documentation site.
Added¶
Dynamic dimensionality and action masks
sekai.MaskedBox: a variable-size set of up tomax_sizeelements padded to a fixed shape, with an int8 validity mask ({"values": (max_size, *shape), "mask": (max_size,)}). It is aDictsubclass, so flattening, batching, vectorisation and gymnasium conversion work unchanged;pack()/unpack()build and read samples.sekai.Sequence: variable-length sequences of any feature space (tuples, or stacked batches withstack=True), with batching, JSON and gymnasium conversion.Box.sample(mask=...)(masked entries are set toBox.zero()) andsekai.spaces.zero_sample().- An action-mask convention: environments implement
action_mask()in the format ofaction_space.sample(mask=...).sekai.get_action_mask(env)finds it through wrappers,check_envsamples random actions with it (and reports malformed masks),ActionMaskObservationadds it to the observation, andto_gymnasium()exposes it asaction_masks()for sb3-contrib's MaskablePPO.MultiDiscrete.sample(mask=...)andflatten_action_mask()reject mask entries with the wrong number of flags, andRescaleActionpasses the env's zero for the components the env masks. - Wrappers
PadSequenceObservation(Sequence → MaskedBox),ActionMaskObservation, andDelayAction(order latency: actions executedelaysteps after they are chosen). FrameStackObservationstacks any batchable observation space (Dict, Tuple, MaskedBox, Discrete, ...), leaf by leaf, withpadding="reset"or"zeros".NormalizeObservation/VecNormalizeObservationnormalise nested observation spaces (each Box leaf, MaskedBox leaves per feature over occupied slots only, other leaves passed through), can use exponentially weighted statistics for non-stationary data (stats="ewm", halflife=...; the half-life counts steps, so it needs no scaling withnum_envs), clip (clip=), and save/restore withstate_dict()/load_state_dict().sekai.ExponentialMovingStatsis the EWM counterpart ofRunningMeanStd.
Vectorisation
- Native vector environments:
sekai.NativeVecEnvis a base class for environments that advance all sub-envs at once with array operations (seeding, per-env options, same-step autoreset and per-env infos are handled for you). Register one withregister(..., vector_entry_point=...);make_vec()then uses it by default (vectorization_mode=None→"native"when available, else"sync"). Native envs takemax_episode_stepsand truncate exactly likeTimeLimit(infos[i]["elapsed_steps"]andget_attr("elapsed_steps")included), andset_attr()raisesAttributeErrorfor names the env doesn't have. They raiseResetRequiredwhen stepped beforereset()and accept the same seeds as every VecEnv. A member that is the same for every sub-env can be marked@sekai.vector.shared, soget_attr()/call()return it whole instead of splitting it by sub-env. An ID registered withadditional_wrappersvectorises withSyncVecEnv(explicit"native"raises), since a native env can't apply them. - Episode samplers have a
spawn()hook:make_vec()(every backend) and the native finance envs give each sub-envsampler.spawn(), so sub-envs behave as independent copies of the env.SequentialStartspawns a fresh copy, so withsampler="sequential"every sub-env replays the same windows and the instance you pass in is not advanced; other samplers are shared by default, so later changes to them (a curriculum, say) reach every sub-env. Overridespawn()for per-environment state. The vector guide shows how to split a history across sub-envs. - The native finance envs expose their parameters and state through
get_attr()/set_attr()asSyncVecEnvdoes; shape-defining ones (lookback,episode_length,n_assets,allow_short) and the data are read-only. Likewiseepisode_lengthandlookbackare read-only onTrading-v0,Portfolio-v0andMarket-v0. ProcVecEnv(num_workers=..., shared_memory=...): observations travel through shared memory (fixed-size spaces, on by default) and several sub-envs can share a worker process. A dead worker that hosts several sub-envs is reported asWorker w (sub-envs a-b) raised ....sekai bench --mode auto|native|sync|async|proc --workers N;sekai listmarks environments with a native vector implementation.
Finance toolkit (sekai.finance)
MarketData: an immutable, point-in-time panel of prices, per-asset fields and market-wide (exogenous) series, where NaN marks an unlisted asset; built from arrays or pandas (wide or long format); chronologicalsplit()andwalk_forward()with embargo and lookback history. The history rows of a split are recorded asMarketData.warmup, and no environment ever makes a decision on one, whatever its lookback (sampler positions, and their error messages, count the windows after the warm-up). Timezone-aware timestamps are rejected with a message showing how to convert them, never shifted silently. An integer always means a row position (counted within the object, negative from the end) inindex_of(),between(),split()andreset(options={"start": ...}), on dated and numeric time axes alike;sekai.finance.Time(t)looks up a timestamp by value on any axis (options={"start": Time(info["timestamp"])}replays an episode). A date on numeric timestamps, or a float on datetime ones, raisesValueErrorinstead of picking a row.- Episode samplers (
RandomStart,SequentialStart,FixedStart) andreset(options={"start": ...})(a row, a date orTime(t)) for every historical environment (a start on a warm-up row raisesValueError). - Market simulators, batch-first with latent state: GBM, Heston, Merton jump-diffusion, regime
switching, GARCH(1,1), exponential Ornstein-Uhlenbeck, cointegrated pairs, block bootstrap,
Randomized(domain randomisation) andListingProcess(assets list and delist). - Causal feature transforms (rolling/EWM statistics, momentum, cross-sectional ranks and
z-scores), point-in-time
asof_joinwith publication lags, andcheck_causal, which detects look-ahead bias (raising the newsekai.LookAheadError) by rescaling, reordering, sign-flipping and re-masking the future and checking the past is unaffected. Every check includes the first row as a cut point, and an output whose shape depends on the future also raisesLookAheadError. - Cost models (
ProportionalCost,SpreadCost,FixedCost,SquareRootImpact, composable with+), weight-space accounting (AccountConfig: leverage, shorting, cash, margin and borrow rates), and reward functions (LogReturn,ProfitAndLoss,MeanVariance,DifferentialSharpe,DrawdownPenalty,TurnoverPenalty, composable arithmetically). Cost models never price a trade at zero because an input is missing: they raise unless givenfallback_spread=/fallback_volatility=. Only NaN (or a negative value) counts as missing; a finite zero spread or volatility is valid and costs nothing. Environments reject a volatility-based cost withlookback=1, fallback or not. Cost models andAccountConfigreject NaN, infinite and (where meaningless) negative parameters.ProfitAndLossusesStepOutcome.pnl, the exact change in equity, so its rewards sum to final equity − 1 even through a wipe-out. - Performance metrics (Sharpe, Sortino, Calmar, drawdown, VaR/CVaR, ...),
PerformanceSummary, and theRecordPerformancewrapper (info["performance"]at the end of each episode).returns_from_equity()invertsequity_curve()also through a wipe-out, andpnl_summary(..., scale=)(whichRecordPerformanceuses in P&L mode) measures rounding against the wealth level, so a constant P&L reports zero volatility, not a huge Sharpe ratio. SlotMap(stable slots for a changing universe) and a limit order book: a price-time priority matching engine (OrderBook) with zero-intelligence Poisson order flow.
Built-in environments
- Environments registered on
import sekai: - Classic control:
CartPole-v1,MountainCar-v0,MountainCarContinuous-v0,Pendulum-v1,Acrobot-v1. Dynamics are bit-for-bit identical to gymnasium's (verified by tests) and need only NumPy. - Finance:
Trading-v0(single-asset trading),Portfolio-v0(multi-asset allocation),MarketMaking-v0(Avellaneda–Stoikov) andOptimalExecution-v0(Almgren–Chriss), with simulated markets or your own price history, plus closed-form baselines (avellaneda_stoikov_action(),almgren_chriss_actions()). Trading and Portfolio take exactly one price source: raw prices (prices=, arrays or pandas, where aDatetimeIndexkeeps its dates), aMarketData(data=, the keyword Market-v0 uses too) or any marketmodel=; plus any cost model, areward=function and asampler=. Portfolio-v0'sn_assetsmust agree with the history or model when given. In both, positions drift with prices between steps and costs are charged on the trade back to the target, so they agree exactly on one asset. - Episode horizons follow one rule: environments whose horizon is part of the task
(market making, optimal execution, the order book) observe time and terminate at the
horizon; those whose episode is a window of an ongoing market (
Trading-v0,Portfolio-v0,Market-v0) truncate and don't observe time (addTimeAwareObservationto observe it). - Finance environments raise
ResetRequiredwhen stepped after the episode ended, check their numeric parameters at construction (ValueErrornaming the parameter), and copy areward_fnset later (also throughset_attr), so vector sub-envs never share a stateful reward. Read-only accessors expose the current state to renderers and loggers:timestamp(Trading, Portfolio, Market),price_history(Trading, Portfolio),latent_state(Market) andMarketMaking-v0's last quotes and fills (bid,ask,bid_filled,ask_filled). Market-v0: multi-asset trading over a changing universe (padded-set observations, action masks, stable slots), with data fields and exogenous series as features, cost models, financing, leverage and shorting, pluggable rewards and optional privileged (latent) state; simulated by default from a two-regime market whose assets list and delist.LimitOrderBook-v0: event-driven market making and trading in a simulated order book, andLimitOrderBookMarket, a multi-agent market in which traders act on their own clocks. A trader's market order first cancels its own resting quote on the other side (self-trade prevention), andLimitOrderBookMarketchecks every action before carrying out any.- Native vector versions of the four classic finance environments (
TradingVecEnv,PortfolioVecEnv,MarketMakingVecEnv,OptimalExecutionVecEnv), used bymake_vec(): 1.3-2.6 million environment steps per second at 1024 sub-envs on an 8-core machine, about 15-20xSyncVecEnv. sekai.envs.finance.simulate_gbm()for correlated geometric Brownian motion paths.- Finance environments report
info["timestamp"]andinfo["dt"](step length), the convention for time-aware and event-driven environments. - Gymnasium interop (
pip install "sekai[gymnasium]"):sekai.compat.to_gymnasium()exposes any sekai env to gymnasium-based libraries (Stable-Baselines3, CleanRL, RLlib);from_gymnasium()brings any gymnasium env into sekai; space converters both ways, which keepDictkey order so both libraries flatten observations identically. Environments with nested observations (Market-v0,LimitOrderBook-v0) train with SB3 throughsekai.FlattenObservationandMlpPolicy(MaskablePPO included). - PettingZoo interop (
pip install "sekai[pettingzoo]"):sekai.compat.to_pettingzoo()andfrom_pettingzoo()convert betweenMultiAgentEnvand PettingZoo's Parallel API. - Rendering: a dependency-free NumPy
Canvas, RGB renderers for every classic control env (CartPoleRenderer, ...), theRecordFrameswrapper andsave_gif()/save_video(), which reject float frames, values outside [0, 255] and mixed frame sizes before writing. - Finance renderers (
sekai.envs.finance):TradingRenderer,PortfolioRenderer,MarketRenderer,MarketMakingRenderer,OptimalExecutionRenderer,LimitOrderBookRendererandLimitOrderBookMarketRendererdraw each episode as a dashboard: prices (with the lookback window), positions shaded and trades marked, equity against a benchmark, allocations over time, the changing universe, quotes and fills, the Almgren–Chriss schedule, and the order book as a depth ladder with the agent's orders at their place in the queue. They takewidth,height,themeandwindow(a scrolling time axis), and draw a frame in 10–20 ms. - Dashboards (
sekai.rendering.Dashboard): renderers for any environment, assembled from charts that read its state through sources (attribute paths or functions) and record it per episode. A new episode is detected fromelapsed_steps, with one history per env. The charts areLineChartwithSeries(rebased, signed, step or shaded),BarChartandStackedArea;sekai.finance.renderingaddsPriceChart,OrderBookChartandUniverseChart. Dashboards also provideRow/Columnlayouts, a header ofStats with their change since the start, a time axis shared across charts, andPanelfor charts of your own.sekai.rendering.chartsholds the drawing helpers (Plot,nice_ticks,format_number, ...). - Themes:
sekai.rendering.LIGHTandDARK, in the Qian Capital palette. Colours are named by role (theup/downpolarity pair,accent,highlight, ...), with an eight-colour categorical palette checked for colour-vision deficiency.Theme.with_colors()adapts a theme. Canvas(..., antialias=True)draws shapes with exact edge coverage, and every shape takesalpha=. Canvas also gainsclip(), roundedrect(radius=), dashed lines,fill_between(),image()andtext(). Text uses BDO Grotesk glyphs rasterised ahead of time (9–32 px, regular and bold, kerned, with tabular digits), shipped under the SIL Open Font License, so the package's license expression is nowApache-2.0 AND OFL-1.1.sekai.rendering.text_width()measures text. Withoutantialias, existing shapes draw exactly as before.save_gif()gives an RGB GIF's frames one shared palette, so each frame stores only what changed (animated charts come out about half the size) and colours can't flicker. Frames with 256 colours or fewer are saved exactly.examples/render_finance.pyrecords every finance environment with a baseline policy; it produced the documentation's animations.- Wrappers:
ClipReward(Noneleaves a side unbounded),RescaleObservation,DtypeObservation,FilterObservation,TimeAwareObservation,DelayObservation,StickyAction,ActionRepeat. Stateful wrappers (TimeLimit,TimeAwareObservation,FrameStackObservation,DelayObservation,DelayAction,StickyAction) warn when built around anAutoReset, which must be the outermost wrapper. - Vector wrappers:
VecRecordEpisodeStatistics(each episode recorded once, with autoreset on or off),VecNormalizeObservation,VecNormalizeReward,VecTransformObservationandVecTransformReward(which takefunc, like gymnasium's). sekai.make_vec(id, num_envs, vectorization_mode="sync" | "async" | "proc").sekai.make(..., max_episode_steps=N)overrides the registered time limit (-1removes it);make()also accepts anEnvSpec. The spec attached to the env records the kwargs actually used.sekai.deregister(),sekai.pprint_registry(),EnvSpec.pprint(), and "did you mean" suggestions inEnvNotFound.- Vector envs: autoreset (see Changed),
batch_action_space, aclosedflag,async_step()/async_reset()on every VecEnv and vector wrapper (so wrapping anAsyncVecEnvkeeps it concurrent),call()/get_attr()/set_attr()onAsyncVecEnv, andProcVecEnv(close_timeout=...). VecObservationWrapper.final_observation(): vector observation wrappers also transform the terminal observations stored by autoreset.- Space utilities:
batch_spacefor every space type,concatenate(),iterate(),space_from_jsonable()/register_space_type(), andfrom_jsonable()forDict/Tuple.Dict.seed()/Tuple.seed()take per-sub-space seeds (a dict / list), as their constructors do. check_env:check_determinismis now implemented; new checks for the async API,Spacetypes, observation dtypes and observations mutated in place after being returned. It checks everyreset()(between episodes andreset(seed=0)too; one that raises is reported as an error), warns whenreset()overwrites the terminal observation in place, validates action masks withflatten_action_mask(), and records copies for the determinism check.ResetResult.done(alwaysFalse), soresult = env.reset()/while not result.done: result = env.step(...)works with one variable. LikewiseMAResetResult.all_done/any_doneandVecResetResult.dones.MultiAgentEnvgets the same seeding API asEnv(rng,_set_rng(seed)).Env.set_attr();sekai.rendering.BaseRenderer(no-opclose()pluswithsupport);elapsed_stepson the finance envs.- sekai exceptions can be pickled (e.g. raised inside a
ProcessPoolExecutorjob). sekai.RunningMeanStd, withstate_dict()/load_state_dict()to save normalisation stats. Its first batch sets the mean (no variance bias for data far from zero), and empty batches are ignored.- A
sekaicommand-line tool (alsopython -m sekai):list,info,check,bench. py.typed: sekai now ships type information, and type-checks clean undermypy --strict.- A test suite, GitHub Actions CI (Python 3.10–3.14 on Linux, plus macOS and Windows), trusted-publishing release workflow, MkDocs documentation site (Qian Capital theme), runnable examples.
Changed¶
These change behaviour for code written against 0.1.x:
- Vector envs reset finished sub-envs automatically (
autoreset=True), AutoReset-style: the returned observation starts the new episode, and the terminal observation/info are ininfos[i]["final_observation"]/["final_info"]. Sub-envs already wrapped inAutoResetare not reset twice. Passautoreset=Falseto manage resets yourself: a finished sub-env is then frozen until the nextreset()(where 0.1.x stepped it again). It is not stepped, its action is ignored, and each later step returns its final observation, reward 0, its finalterminated/truncatedflags and an empty info, sowhile not finished.all()evaluation loops work with every backend and env. Env._set_rng(None)keeps the current RNG instead of re-seeding from OS entropy, soreset(seed=s)followed by plainreset()calls is reproducible (gymnasium semantics). Previously every unseeded reset drew fresh entropy.- Wrappers share the wrapped env's RNG (
wrapper.rng is env.rng). NormalizeObservation/NormalizeRewarduseRunningMeanStd(obs_rms/return_rms) and can be frozen withupdate_running_mean = False.NormalizeRewardnow resets its discounted return at episode ends, not only onreset().flatten_space()always returns afloat32Box, matching whatflatten()produces; forDict/Tuplethe Box carries the sub-spaces' bounds instead of(-inf, inf).- Batched observations:
Discreteobservations are stacked into arrays, and all three vector backends batchDict/Tupleobservations the same way (recursively). AutoResetalso puts the reset's info ininfo["reset_info"].ProcVecEnvraisesVecEnvWorkerError(with the worker's traceback) instead ofRuntimeError, keeps workers alive after an error, and reads the spaces from worker 0 instead of building an extra env in the parent process.MultiDiscrete.sample()returns the space'sdtype(defaultint64).sekai.make(id, max_episode_steps=N)now sets the time limit (as in gymnasium) instead of passingmax_episode_stepsto the environment's constructor. An env that takes its ownmax_episode_stepsargument should be registered with it inkwargs=.- Vector envs raise
VecEnvClosedwhen used afterclose(). RescaleObservationandTimeAwareObservationoutputfloat32for integer observations (they produced truncated or overflowing integers).RecordEpisodeStatisticsstarts a new episode's counters when it reports one, so it is also correct around an innerAutoReset.VecEnv.get_attr()/set_attr()/call()andWrapper.set_attr()resolve names through the wrapper stack, somake_vec("CartPole-v1").get_attr("x_threshold")reaches the base env andset_attrchanges the layer that has the attribute (it used to create a new attribute on the outermost wrapper).- The
Rendererprotocol requires onlyrender()andclose(); duck-typed renderers now passisinstance(r, Renderer). Context-manager support moved toBaseRenderer. Boxreprs show exact bounds without float32 noise, and per-dimension bounds when they differ.check_envreports each per-step problem once, with how many steps showed it.- A custom
Spacesubclass without__eq__compares by identity (it used to equal every instance of its type), andMultiDiscreteequality includes the dtype. Dict/TupleraiseValueErrorfor seeds that name unknown keys or extra sub-spaces, instead of ignoring them.- Wrapper argument validation raises
TypeError/ValueErrorinstead ofAssertionError(asserts disappear underpython -O).Wrapper(...)rejects objects that aren'tsekai.Env. - Optional dependency groups:
allnow installs the runtime extras (gymnasium,pettingzoo,pandas,render); development tools moved todev, tests totest.
Deprecated¶
sekai.backend(BackendOps,NumpyBackend) warns on import and will be removed in 0.3. Nothing in sekai used it; for fast batched simulation write a native vector environment instead.sekai.BackendMismatchgoes with it: it is no longer insekai.__all__and warns when used.- The
fn=keyword ofTransformObservation/TransformReward: the argument is nowfunc, gymnasium's name (positional calls are unaffected).fn=warns and will be removed in 0.3.
Fixed¶
await env.async_step()/async_reset()skipped the logic of every wrapper (e.g.TimeLimitnever truncated). Built-in wrappers now implement both paths, and any wrapper or env that overrides onlystep()/reset()has its async methods routed through them.EnvSpecparsed"CartPole-v1"as name"CartPole-v1", versionNone.- Integer
Boxsampling overflowed at the dtype's maximum (Box(0, 255, dtype=uint8)raisedValueError). AsyncVecEnv.step()/reset()usedasyncio.get_event_loop(), which fails on Python 3.14 when no loop is set; they now run on a private loop and give a clear error inside a running loop.ProcVecEnvcould crash withAttributeErroronNonewhen a worker failed, because errors travelled through a queue that raced the result pipe.- Vector envs iterated over the keys of
Dictaction batches instead of the sub-envs. Dict/Tuplespaces crashed when seeded with anumpy.random.Generator.MultiDiscrete.sample(mask)failed for the tuple-of-arrays mask format.Discrete.contains()rejected 0-d integer arrays;MultiBinary.contains()rejected any dtype other thanint8;MultiDiscrete.contains()rejected lists.RecordEpisodeStatisticsreported a seed ofNonewhen the env was wrapped.EnvSpec.to_json()droppedadditional_wrappers.make()failed when an entry point returned an already-wrapped env.- A namespace loader that raised was discarded, so the namespace could never load afterwards.
- Renderers crashed on a diverged (NaN) state; non-finite shapes are now skipped.
- An integer
Boxwith infinite bounds (Box(-np.inf, np.inf, dtype=np.int64)) collapsed to a single value; infinite and out-of-range bounds now map to the dtype's limits, as in gymnasium. NaN bounds raiseValueError, and bounds are copied instead of aliasing the caller's array. Box.contains()rejected Python ints for unsigned or narrower integer Boxes (Box(0, 255, (), np.uint8).contains(3)wasFalse).MAStepResult.all_done/any_donepairedterminatedandtruncatedby position instead of by agent; they now look up each agent, and an empty result is not done.RescaleActioncould return actions one float32 step outside the env's bounds; it clips now.RecordEpisodeStatisticsandNormalizeRewardsummednp.float32rewards in float32.Tuple.sample(mask=...)with too few masks returned a shorter, invalid tuple; it raises now.check_envcrashed whenaction_mask()masked every action in a reachable state; that is reported as an error now.MultiBinaryrejected NumPy integers forn;Discrete.sampleandMultiBinary.samplerejected list masks.- A sub-env whose
close()raised stoppedVecEnv.close()from closing the others, and madeProcVecEnv.close()wait out its whole timeout; every sub-env is now closed, the first error is raised afterwards, and workers that ignore termination are killed. SyncVecEnv,AsyncVecEnvandProcVecEnvleaked the sub-envs already built when building another one failed; they are closed before the error propagates.ProcVecEnvleft an "Exception ignored ... BufferError" message at exit after an error.unflatten()could return values outside the space (a float64Boxjust above its upper bound; an int64Boxvalue near the dtype's maximum with its sign flipped).Boxvalues are now clipped into the bounds, integer ones rounded without overflow, andMultiBinaryvalues thresholded at 0.5.- Vector envs dropped every action when the action space contained an empty
Dict/Tuple. MultiBinary.contains()andMultiDiscrete.contains()raised on ragged input instead of returningFalse.Boxkept NumPy integers in its shape, so itsto_jsonable()output could not be serialised to JSON.
0.1.2 - 2026-05-06¶
Fixed¶
- The package metadata version now matches
sekai.__version__.
0.1.1 - 2026-05-06¶
Added¶
- First public release:
Env/Wrapper/MultiAgentEnvwithStepResult/ResetResult, async methods on every env, spaces (Box,Discrete,MultiDiscrete,MultiBinary,Dict,Tuple), registry with lazy namespaces,SyncVecEnv/AsyncVecEnv/ProcVecEnv, core wrappers, typedEpisodeStatswith pluggable trackers,Rendererprotocol, andcheck_env.