Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The strategic bet is **interface ownership**: if every trading agent in the open

## Status: published, active (pre-1.0)

Published to **crates.io**, **npm**, and **PyPI** at **v0.5.0**, depending on the **published** `sharpebench-sim 0.0.7` engine (not a vendored copy). CI is green across four surfaces: Rust (`fmt`, `clippy -D warnings`, tests, a WASM target build), `cargo-deny`, the npm package, and the Python wheel (`maturin` + `pytest`).
Published to **crates.io**, **npm**, and **PyPI** at **v0.6.0**, depending on the **published** `sharpebench-sim 0.0.8` engine (not a vendored copy). CI is green across four surfaces: Rust (`fmt`, `clippy -D warnings`, tests, a WASM target build), `cargo-deny`, the npm package, and the Python wheel (`maturin` + `pytest`).

Beyond the core `reset`/`step` lifecycle, the environment now ships a full **reinforcement-learning training surface**:

Expand All @@ -53,9 +53,9 @@ Beyond the core `reset`/`step` lifecycle, the environment now ships a full **rei
| **Vectorized rollouts** | `VecTradingEnv` runs B scenario lanes in lockstep (rayon, structure-of-arrays JSON, current-Gymnasium `AutoresetMode`, async `send`/`recv`), exposed as a `gymnasium.vector` env. |
| **Point-in-time-safe wrappers** | Causal normalize (no future-bar leak), `TimeLimit`, `FrameStack`, `RecordEpisodeStatistics`, vector-env variants, and `flatten`/`unflatten` Dict-obs helpers, plus a `check_env` conformance harness that *proves* seed-determinism (and adopts Gymnasium's own `check_env`). |
| **Gymnasium registration** | Versioned, namespaced IDs: `gymnasium.make("OpenOutcry/Hard-v1")` and `make_vec(...)` route to the scalar and vector envs, with `-Eval-v1` variants on a disjoint held-out seed band. |
| **Multi-agent markets** | A PettingZoo `MultiAgentOpenOutcryEnv` (batched competition: N agents on one frozen scenario, SharpeBench-ranked), and `EndogenousMarketEnv`, a real shared-book market where aggregate flow *moves* the cleared price (Kyle permanent + Almgren-Chriss temporary impact). |
| **Multi-agent markets** | A PettingZoo `MultiAgentOpenOutcryEnv` (batched competition: N agents on one frozen scenario, SharpeBench-ranked), an `EndogenousMarketEnv` (a shared-book market where aggregate flow *moves* the cleared price via Kyle + Almgren-Chriss impact), and an `LOBMarketEnv` over a real **deterministic limit-order-book matching engine** (integer ticks, price-time priority, market/limit/cancel/modify, depth-ladder + microprice + queue-imbalance obs). |
| **Per-scenario mandates** | Each scenario samples a trading mandate (long-only, market-neutral, drawdown-capped, beat-a-benchmark); the `verifiers` rubric is mandate-conditioned, so wrong-objective behavior is penalized, not just unrewarded. |
| **Offline-RL + checkpointing** | `to_minari` exports rollouts as a Farama [Minari](https://minari.farama.org) dataset (leak-safe, `recover_environment`-ready); `CheckpointableEnv` clones/restores/branches market state for tree search; `OpenOutcryFuncEnv` is a stateless `gymnasium.functional.FuncEnv` view. |
| **Offline-RL + checkpointing** | `to_minari` exports rollouts as a Farama [Minari](https://minari.farama.org) dataset (leak-safe, `recover_environment`-ready); `CheckpointableEnv` clones/restores/branches market state for tree search (O(1) native engine snapshot or replay-from-decisions); `OpenOutcryFuncEnv` is a stateless `gymnasium.functional.FuncEnv` view. |
| **Benchmark protocol** | A committed [`EVALUATION.md`](EVALUATION.md): the canonical eval contract, the disjoint train/held-out split, and a baseline leaderboard (deflated Sharpe to beat is 0.0; pass^k degrades Calm to Hard to Extreme). |
| **Harness integration** | An MCP server (`reset` / `step` / `spec` tools) so any MCP agent harness drives an episode with zero glue, a `LookaheadGuard` that refuses agent operations reading future data, versioned JSONL rollout traces that re-score offline through the SharpeBench kernel, and a cost-adjusted `RunMetrics` block for leaderboard ranking. |

Expand Down Expand Up @@ -205,7 +205,7 @@ The load-bearing standard. An `Observation` is point-in-time; a `Decision` is a
A Rust [Cargo workspace](Cargo.toml), `#![forbid(unsafe_code)]`, that **depends on** the published SharpeBench engine rather than vendoring it, so the env and the benchmark cannot drift.

```
sharpebench-sim (published 0.0.7) ... the leak-free point-in-time engine
sharpebench-sim (published 0.0.8) ... the leak-free point-in-time engine
|
crates/openoutcry ......... the env, scenario generator, batched VecTradingEnv,
| mandate / exec-noise / market-clearing cores, the
Expand Down
2 changes: 1 addition & 1 deletion crates/openoutcry-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ crate-type = ["cdylib"]

[dependencies]
openoutcry = { path = "../openoutcry", version = "0.5.0" }
sharpebench-core = "0.0.7"
sharpebench-core = "0.0.8"
serde_json = "1"

[dependencies.pyo3]
Expand Down
4 changes: 4 additions & 0 deletions crates/openoutcry-py/python/openoutcry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
make_block_env,
)
from .cascade import LiquidationCascadeEnv, cascade_survived, cascade_summary
from .lob_env import LOBMarketEnv, symmetric_quote_policy, noise_trader_policy
from .reward_misspecification import (
MISSPECIFIED_REWARDS,
MISSPECIFIED_PROXY_POLICIES,
Expand Down Expand Up @@ -237,6 +238,9 @@
"LiquidationCascadeEnv",
"cascade_survived",
"cascade_summary",
"LOBMarketEnv",
"symmetric_quote_policy",
"noise_trader_policy",
"MISSPECIFIED_REWARDS",
"MISSPECIFIED_PROXY_POLICIES",
"misspecification_gap",
Expand Down
49 changes: 34 additions & 15 deletions crates/openoutcry-py/python/openoutcry/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
into an *independent* env, which is what tree search / MCTS / counterfactual rollouts need:
explore a subtree without perturbing the parent.

Cost model (be honest): ``restore_state`` / ``branch`` are **O(prefix length)** — they
replay every action up to the snapshot. For deep trees this is quadratic in the worst case.
An O(1) engine-level snapshot (copy the native simulator state) is a future
``sharpebench-sim`` enhancement; until then, replay is the leak-free, engine-agnostic way to
get exact restoration.
Two restoration paths:

- **Replay (default):** ``restore_state`` / ``branch`` are O(prefix length) — they replay
every action up to the snapshot. Engine-agnostic and leak-free by construction.
- **Native O(1) (opt-in, ``native=True``):** since ``sharpebench-sim 0.0.8`` the engine
exposes ``clone_state`` / ``restore_state``, so a snapshot is a direct copy of the native
simulator state (cursor + book). ``clone_state(native=True)`` captures that and
``restore_state`` rewinds in O(1) with no replay — the fast path for deep tree search.

Leak-safety: the state carries only construction params + decisions — **never** the
underlying ``Dataset`` / native ``TradingEnv`` handle or a raw price series. Those would let
Expand Down Expand Up @@ -118,13 +121,15 @@ class CheckpointState:
actions: list = field(default_factory=list)
step: int = 0
include_rng: bool = True
native_state: Any = None # the native O(1) snapshot JSON (set when native=True)

def to_dict(self) -> dict:
return {
"params": dict(self.params),
"actions": [list(a) for a in self.actions],
"step": int(self.step),
"include_rng": bool(self.include_rng),
"native_state": self.native_state,
}

@classmethod
Expand All @@ -134,6 +139,7 @@ def from_dict(cls, d: dict) -> "CheckpointState":
actions=[list(a) for a in d.get("actions", [])],
step=int(d.get("step", 0)),
include_rng=bool(d.get("include_rng", True)),
native_state=d.get("native_state"),
)


Expand Down Expand Up @@ -170,28 +176,36 @@ def step(self, action):

# -- checkpoint API ----------------------------------------------------

def clone_state(self, *, include_rng: bool = True) -> CheckpointState:
def clone_state(self, *, include_rng: bool = True, native: bool = False) -> CheckpointState:
"""Capture the current env state as a serializable :class:`CheckpointState`.

O(prefix length) in space (the action list). See :meth:`restore_state` for the
replay cost. ``include_rng`` is documented on :class:`CheckpointState`.
With ``native=False`` (default) the snapshot is the recorded action prefix
(O(prefix length); engine-agnostic). With ``native=True`` it is the engine's O(1)
``clone_state`` snapshot (cursor + book) — the fast path for deep tree search.
``include_rng`` is documented on :class:`CheckpointState`.
"""
return CheckpointState(
params=_extract_params(self.env),
actions=[a.tolist() for a in self._actions],
step=self._step,
include_rng=include_rng,
native_state=self.env.clone_state() if native else None,
)

def restore_state(self, state: CheckpointState) -> None:
"""Rewind THIS env to ``state`` by rebuilding from params and replaying actions.

Builds a brand-new native env from ``state.params`` (so no stale simulator state
survives), ``reset``s it, and replays every recorded action. **O(prefix length)** —
it re-executes the whole decision prefix. Exact because the engine is deterministic.
"""Rewind THIS env to ``state``. If ``state`` carries a ``native_state`` snapshot,
rebuild the env and restore the engine in O(1); otherwise rebuild and replay the
recorded action prefix (O(prefix length)). Both are exact (the engine is
deterministic), so the restored env reproduces the snapshot point byte-for-byte.
"""
self.env = _build_env(state.params)
self._replay(state)
if state.native_state is not None:
self.env.reset()
self.env.restore_state(state.native_state)
self._actions = []
self._step = int(state.step)
else:
self._replay(state)

def branch(self, state: CheckpointState) -> "CheckpointableEnv":
"""Return a NEW, independent :class:`CheckpointableEnv` restored to ``state``.
Expand All @@ -202,7 +216,12 @@ def branch(self, state: CheckpointState) -> "CheckpointableEnv":
O(prefix length) to materialize.
"""
fork = CheckpointableEnv(_build_env(state.params))
fork._replay(state)
if state.native_state is not None:
fork.env.reset()
fork.env.restore_state(state.native_state)
fork._step = int(state.step)
else:
fork._replay(state)
return fork

# -- internal ----------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions crates/openoutcry-py/python/openoutcry/gym.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ def step(
terminated = float(info.get("nav", 1.0)) <= 0.0
return obs, float(reward), terminated, truncated, info

def clone_state(self) -> str:
"""An O(1) native snapshot of the sim state (cursor + book) as a JSON string.
Pair with :meth:`restore_state` for what-if branching without replaying decisions
(the engine-level checkpoint, vs the replay path in ``CheckpointableEnv``)."""
return self._env.clone_state()

def restore_state(self, state_json: str) -> None:
"""Restore the env to a :meth:`clone_state` snapshot in O(1) (no replay)."""
self._env.restore_state(state_json)

def render(self): # pragma: no cover - no visual rendering
return None

Expand Down
Loading
Loading