MarlSimEnv — event-time financial environment¶
SDK 5 runs local funded asset engines on one deterministic Rust event calendar. The SDK supplies the world, delivered observations, pending action execution and generic controls. Applications own agents, features, rewards, models and learning. There are no SDK-owned studies, reference policies, trainers or graphs.
See SDK 5 migration for SDK 4 callers, or v4 migration for a v3 turn-based caller.
Ask for a decision, then submit pending work¶
from fastboxex import MarlSimEnv
env = MarlSimEnv([
{"id": 1, "mode": "training",
"scheduler": {"default_compute_ns": 100, "min_wake_gap_ns": 200}},
{"id": 2, "scheduler": {"first_wake_ns": 10, "min_wake_gap_ns": 200}},
], horizon_ns=2_000, md_interval_ns=10, trace_capacity=0)
for state in ("Suspended", "Auction", "Trade"):
env.transition_to(state)
while request := env.next_decision():
# The caller supplies its agent with observation, events and mode.
actions = [] # A no-op exercises the boundary without designing an agent.
ticket = env.submit_decision(request["decision_id"], actions)
# ticket contains scheduling facts, not fills or a reward.
env.validate()
The native equivalents are sim::marl::{MarlConfig, MarlSimEnv, AgentDecision}.
Rust advances market events with the Python GIL released. Python crosses the
boundary at decisions, never once per matching/calendar event. Actions are
converted directly without a JSON encode/parse round trip.
delivered observation → declared computation → outbound delay → command arrival
publication → public delivery
execution → private delivery
An agent deciding at time 0 with compute duration 100 does not prevent another agent waking at 10 and completing at 15 from arriving first. Policy wall-clock time does not automatically advance modeled time. Calibrating declared computation cost remains the application's responsibility.
Per-agent training and inference¶
env = MarlSimEnv([{"id": 1}, {"id": 2}])
env.set_agent_mode(1, "training")
env.set_agent_mode(2, "inference")
assert env.agent_mode(1) == "training"
- Initial
modeis configurable per agent; the default isinference. - Each issued request carries a snapshot in
request["mode"]. - A switch affects subsequent requests. An issued request and pending actions retain their original mode/scheduling, even if the setter is called mid-request.
- A switch resets neither world nor model. Different agents can use different modes.
reset()restores the configured initial modes; restore/replay preserve changes.- Unknown modes/agents and switches after episode termination raise errors.
The caller chooses the learning schedule and must prevent parameter/optimizer
updates in inference. The SDK cannot freeze a model it does not own. Inference
can remain stochastic and retain recurrent memory. PyTorch model.eval() alone
does not prohibit optimizer updates; training rollout collection can use
no_grad(). This control is independent of the asset engine's parity/diagnostic
mode. The SDK has no algorithm-specific mode or training callback.
Configuration and timing¶
MarlSimEnv.from_config(mapping) accepts the native schema; unknown fields fail.
The convenience constructor uses asset ID 1 unless assets is supplied. Do not
mix assets with single-asset configuration keywords.
| Field | Meaning |
|---|---|
assets |
Tick/lot, risk, fees, depth and publication settings per asset |
agents |
Participant endowments, subscriptions, scheduler and initial mode |
horizon_ns |
Episode boundary; eligible events through that timestamp execute |
seed |
Caller seed namespace; the SDK does not invent learner randomness |
history_capacity |
Bounded delivered-event history per agent |
trace_capacity |
Bounded full journal; default 0 disables recording |
max_events |
Processing budget; exhaustion raises an error |
provenance |
Caller-supplied revisions recorded in the manifest, not policy input |
Scheduler fields are first_wake_ns, min_wake_gap_ns, default_compute_ns,
priority, outbound_ns, public_delay_ns and private_delay_ns. Computation and
minimum wake gap must be positive. The next wake is completion plus the requested
gap, bounded below by the minimum; outbound delay does not postpone the next wake.
Calendar ordering is (timestamp, phase, priority, insertion sequence):
lifecycle boundaries, computation/commands, publication, delivery, then wakes.
Cancellations have no blanket priority over orders. An auction boundary precedes
commands arriving at the same timestamp. These are explicit modeled assumptions.
Multi-asset observations and actions¶
For multiple assets, supply endowments={asset_id: {cash: ..., inventory: ...}}
per agent, with observed_assets and tradable_assets as needed. Each asset has
segregated funding, matching, lifecycle and fees. There is no automatic currency
conversion, cross-margin or atomic basket. Prices, quantities and times are raw
integer SDK units.
submit_decision(id, actions, compute_ns=None, next_wake_delay_ns=None) seals an
ordered list of separate commands. Kinds are limit, market, delta, ioc,
cancel, modify and noop. Market actions require their asset_id; controls
and trading permissions are validated natively. A failed submission keeps the
request active so the caller can correct it.
The delivered observation contains asset-keyed market snapshots and receipt
times, own account state, standing orders and a cursor. request["events"]
contains deliveries since that agent's previous decision cut: public snapshots,
anonymous public trades or private fills/acks/account/order changes. Public trades
do not disclose participant/order identities. exchange_view(agent_id) reads
delivered state, not authoritative matcher state. events_since(agent_id, cursor)
raises on a future cursor or overwritten history.
depth_array(agent_id, asset_id) returns an owned contiguous NumPy uint64[10,4]
array: bid price/quantity, ask price/quantity, zero padded, with exchange and receipt
timestamps. It reads delivered information only. The buffer cannot mutate the
engine and later events cannot overwrite it. External Torch code can use
torch.from_numpy(); feature normalization, dtype/device transfer and models stay
outside the SDK. No Torch dependency or GPU zero-copy guarantee is imposed.
Setup, termination and evaluator facts¶
Before the first decision, use bootstrap(command) for funded setup commands and
transition_to(state, asset_id=...) for lifecycle changes. After starting, use
schedule_transition() or schedule_command(). These are controller facilities,
not extra information for an autonomous policy.
At the horizon, no new decision is issued. Eligible arrivals/deliveries execute;
future pending work remains checkpointed. There is no forced liquidation or fresh
terminal delivery. exchange_view() still returns the last delivered information.
reset() reconstructs initial endowments, modes and scheduling; repeat setup.
evaluator_state() returns authoritative accounts (agent then asset), signed
venue_cash (asset), clock_ns, ended and processed_events. Keys and financial
values are exact Python integers. This on-demand snapshot does not refresh agent
caches or calculate rewards, wealth, statistics or reports. Never pass it to an
autonomous policy; applications compute their stated evaluator metrics separately.
Checkpoints and replay¶
checkpoint() returns schema-2 world bytes, including active requests, selected
modes, pending events, delivered caches and journal state. Restore requires the
same schema/client version. The caller separately saves models, optimizers,
buffers, recurrent memory and RNG state. Load only trusted checkpoints.
The full journal records controller inputs, mode changes, decisions, completions,
executions, lifecycle, publications and deliveries. Drain it incrementally with
drain_trace(); a full buffer raises an error before consuming the next event.
history() is the evaluator journal, not an agent observation stream. Store
manifest() beside the journal. The SDK does not inspect Git, package files or
the host to invent provenance.
MarlSimEnv.replay(config, inputs) verifies recorded decision identities/times and
mode-switch boundaries. Inputs are the input fields of journal records with
kind == "input". Continue with next_decision() to process remaining events.
trace_hash is a reproducibility checksum, meaningful only with recording enabled,
not a security certificate. Disable detailed tracing when it is unnecessary.
Optional native read views (SDK 5.1)¶
next_decision_view() issues the same native request as next_decision(), but
returns a read-only fastboxex.marl.DecisionView instead of eagerly materializing
every order/event as Python dictionaries. Do not call both while a request is active.
request = env.next_decision_view()
if request is not None:
cash = request.account(1)["cash"]
own_orders = request.own_orders_array(1)
public_depth = request.public_depth(1)
delivered_tables = request.events_arrays()
ticket = env.submit_decision(request.decision_id, [])
The view captures identity, mode, cursor, exchange/receipt times and delivered
state. It owns no controller/world handle and cannot select another participant.
Later executions/mode changes cannot refresh old views. own_order() is a point
query; own_best_bid/ask() mean own resting prices, not public market prices.
public_best_bid/ask() read the captured public prices without a depth-array copy.
No second order tracker, maintained statistic or policy/model is added to matching.
Arrays are owned contiguous uint64, with layouts published in ORDER_COLUMNS
and DELIVERED_COLUMNS in fastboxex.marl. The delivered tables include all
delivery headers, owner-private events/accounts/order changes (including tombstones),
public snapshot counters/levels and anonymous public trades. Tables join on the
per-agent delivery cursor and retain source order. This is a non-consuming captured
stream, not a drain of immediate matcher events. as_dict() explicitly requests
the complete legacy representation when required.
The MarlSimEnv and synchronous SimExchange controllers separately provide
monitor_depth_array, monitor_depth_summary and monitor_owner_queue_array. They expose authoritative
state for reconstruction/diagnostics and must never replace an autonomous agent's
delivered input. SimConnection.drain_events_array() consumes both private event
kinds once in native order, retaining producer capacity; it is an alternative to,
not an extra copy of, the legacy event drains. Prices/quantities/fees remain exact;
raw wide depth totals use Python integers. Device transfer and normalization stay
in the application. No GPU interoperability or speedup is implied by these APIs.