diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b05a9b2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +**Architecture** + +MercuriusMesh (Phase 0) — a deterministic, offline-first sandbox for cross-venue experiments. + +Tech stack +- Language: Python 3.9+ +- Runtime packages: pydantic (data contracts), pytest (tests) + +Project layout +- mercuriusmesh/: core package with contracts, toy adapters, and a minimal deterministic simulator +- tests/: pytest test-suite +- test.sh: runs tests and builds the package + +Guiding principles +- Keep core contracts minimal and canonical: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta +- Deterministic replay: append-only event log; replay reinitialises adapters and replays events +- Small, well-tested chunks: this agent implements Phase 0 core contracts + toy two-venue demo + +Testing +- Run `bash test.sh` to run the full verification: pytest and `python3 -m build`. + +Contribution rules +- Follow the existing contracts module when adding fields; prefer additive, backward-compatible changes +- Add adapter conformance tests to `tests/` and update AGENTS.md when adding new adapters diff --git a/README.md b/README.md index 43904d7..54c2469 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# idea83-mercuriusmesh-cross-venue +# MercuriusMesh — Cross-Venue Market Microstructure Sandbox (Phase 0) -Source logic for Idea #83 \ No newline at end of file +This repository contains a Phase 0 implementation of MercuriusMesh: a deterministic, +offline-first sandbox for cross-venue market microstructure experiments. + +What is included +- Core pydantic contracts: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta +- A tiny deterministic two-venue simulator and toy adapters +- Tests validating model serialization and deterministic replay + +Run tests and build +1. Install dev deps: `pip install -r requirements.txt` or `pip install pydantic pytest build` +2. Run `bash test.sh` which runs `pytest` and `python3 -m build` + +This is a focused chunk (Phase 0). Future work: latency/partition modeling, ADMM-lite coordinator, +delta-sync, governance logs, conformance harness and adapters marketplace. diff --git a/mercuriusmesh/__init__.py b/mercuriusmesh/__init__.py new file mode 100644 index 0000000..ce85eab --- /dev/null +++ b/mercuriusmesh/__init__.py @@ -0,0 +1,16 @@ +"""MercuriusMesh core package (Phase 0). + +Contains canonical contracts and a tiny deterministic two-venue simulator used in tests. +""" + +from .contracts import LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta +from .simulator import DeterministicSimulator + +__all__ = [ + "LocalBook", + "OrderEvent", + "QuoteSnapshot", + "SharedSignals", + "PlanDelta", + "DeterministicSimulator", +] diff --git a/mercuriusmesh/adapters.py b/mercuriusmesh/adapters.py new file mode 100644 index 0000000..fd6fe9c --- /dev/null +++ b/mercuriusmesh/adapters.py @@ -0,0 +1,44 @@ +from typing import Callable, List +from .contracts import QuoteSnapshot, OrderEvent + + +class ToyVenueAdapter: + """A tiny in-process adapter that exposes a simple order acceptance model. + + It accepts OrderEvent objects and updates an internal top-of-book. Deterministic + behaviour is achieved by not using random jitter and relying on caller ordering. + """ + + def __init__(self, venue_id: str, initial_bid: float = 100.0, initial_ask: float = 100.5): + self.venue_id = venue_id + self.best_bid = initial_bid + self.best_ask = initial_ask + self.listeners: List[Callable[[QuoteSnapshot], None]] = [] + + def on_snapshot(self, cb: Callable[[QuoteSnapshot], None]): + self.listeners.append(cb) + + def publish_snapshot(self): + snap = QuoteSnapshot( + venue_id=self.venue_id, + asset="TEST/USD", + bids=[[self.best_bid, 100.0]], + asks=[[self.best_ask, 100.0]], + ) + for cb in self.listeners: + cb(snap) + + def handle_order(self, order: OrderEvent) -> OrderEvent: + # Very small deterministic matcher: marketable at top-of-book executes + if order.side == "buy" and order.price is None: + order.status = "filled" + # naive mid update + self.best_bid = max(self.best_bid, order.price or self.best_bid) + elif order.side == "sell" and order.price is None: + order.status = "filled" + self.best_ask = min(self.best_ask, order.price or self.best_ask) + else: + order.status = "accepted" + # publish new snapshot deterministically + self.publish_snapshot() + return order diff --git a/mercuriusmesh/contracts.py b/mercuriusmesh/contracts.py new file mode 100644 index 0000000..d170567 --- /dev/null +++ b/mercuriusmesh/contracts.py @@ -0,0 +1,57 @@ +from typing import List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +class LocalBook(BaseModel): + """A minimal per-venue order book snapshot. + + This is intentionally tiny for Phase 0: top-of-book bids/asks with sizes and timestamp. + """ + venue_id: str + asset: str + best_bid: Optional[float] + best_bid_size: Optional[float] + best_ask: Optional[float] + best_ask_size: Optional[float] + timestamp: datetime = Field(default_factory=datetime.utcnow) + + +class OrderEvent(BaseModel): + """A canonical order lifecycle event used between adapters and simulator.""" + order_id: str + venue_id: str + asset: str + side: str # 'buy' or 'sell' + price: Optional[float] + size: float + created_at: datetime = Field(default_factory=datetime.utcnow) + status: str = "submitted" # submitted, accepted, filled, cancelled + + +class QuoteSnapshot(BaseModel): + venue_id: str + asset: str + bids: List[List[float]] = [] # [[price, size], ...] + asks: List[List[float]] = [] + timestamp: datetime = Field(default_factory=datetime.utcnow) + + +class SharedSignals(BaseModel): + """Lightweight aggregated signals used for cross-venue coordination.""" + asset: str + mid_price: Optional[float] + liquidity: Optional[float] + timestamp: datetime = Field(default_factory=datetime.utcnow) + + +class PlanDelta(BaseModel): + """An incremental plan update: proposed hedge or quote update.""" + plan_id: str + author: str + asset: str + venue_id: str + side: str + price: Optional[float] + size: float + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/mercuriusmesh/simulator.py b/mercuriusmesh/simulator.py new file mode 100644 index 0000000..9198528 --- /dev/null +++ b/mercuriusmesh/simulator.py @@ -0,0 +1,70 @@ +from typing import Dict, List +from .adapters import ToyVenueAdapter +from .contracts import OrderEvent, LocalBook, SharedSignals + + +class DeterministicSimulator: + """A tiny deterministic simulator that runs a sequence of events and supports replay. + + The simulator stores a simple event log (append-only). Replaying the log deterministically + rebuilds the same state. This is a minimal skeleton intended for Phase 0 testing and demos. + """ + + def __init__(self, seed: int = 0): + self.seed = seed + self.adapters: Dict[str, ToyVenueAdapter] = {} + self.event_log: List[OrderEvent] = [] + self.state: Dict[str, LocalBook] = {} + # store adapter initial params so replay can re-create identical adapters + self._adapter_initials: Dict[str, Dict[str, float]] = {} + + def add_adapter(self, adapter: ToyVenueAdapter): + self.adapters[adapter.venue_id] = adapter + # capture initial topology for deterministic replay + self._adapter_initials[adapter.venue_id] = {"best_bid": adapter.best_bid, "best_ask": adapter.best_ask} + + def snapshot_listener(snap): + # update simple LocalBook + self.state[adapter.venue_id] = LocalBook( + venue_id=snap.venue_id, + asset=snap.asset, + best_bid=snap.bids[0][0] if snap.bids else None, + best_bid_size=snap.bids[0][1] if snap.bids else None, + best_ask=snap.asks[0][0] if snap.asks else None, + best_ask_size=snap.asks[0][1] if snap.asks else None, + ) + + adapter.on_snapshot(snapshot_listener) + + def submit_order(self, order: OrderEvent) -> OrderEvent: + # append to log and route to adapter deterministically by venue_id + self.event_log.append(order) + adapter = self.adapters.get(order.venue_id) + if adapter is None: + order.status = "rejected" + return order + return adapter.handle_order(order) + + def replay(self) -> None: + # clear state and re-apply events in order + self.state = {} + # re-create adapters to ensure deterministic initial conditions + adapters_copy = {} + for vid, initials in self._adapter_initials.items(): + adapters_copy[vid] = ToyVenueAdapter(vid, initial_bid=initials.get("best_bid", 100.0), initial_ask=initials.get("best_ask", 100.5)) + self.adapters = adapters_copy + for vid in list(self.adapters.keys()): + # re-register listeners and initial state + self.add_adapter(self.adapters[vid]) + for e in list(self.event_log): + self.submit_order(e) + + def compute_shared_signals(self, asset: str = "TEST/USD") -> SharedSignals: + # simple average mid from known LocalBook entries + mids = [] + for lb in self.state.values(): + if lb.best_bid is not None and lb.best_ask is not None: + mids.append((lb.best_bid + lb.best_ask) / 2) + mid = sum(mids) / len(mids) if mids else None + liquidity = sum((lb.best_bid_size or 0) + (lb.best_ask_size or 0) for lb in self.state.values()) + return SharedSignals(asset=asset, mid_price=mid, liquidity=liquidity) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dc7e728 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "mercuriusmesh" +version = "0.0.1" +readme = "README.md" +requires-python = ">=3.9" +dependencies = ["pydantic>=1.10"] + +[tool.setuptools] +packages = ["mercuriusmesh"] +include-package-data = true diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7562639 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = mercuriusmesh +version = 0.0.1 +description = MercuriusMesh: Cross-venue market microstructure sandbox (toy phase0) +long_description = file: README.md +long_description_content_type = text/markdown +author = OpenCode Agent +license = MIT + +[options] +packages = find: +install_requires = + pydantic>=1.10 + pytest>=7.0 diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..6cd7761 --- /dev/null +++ b/test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Running tests..." +# ensure package dir is importable for pytest +export PYTHONPATH="$(pwd)" +echo "Installing test dependencies (pydantic, pytest, build)..." +python3 -m pip install --quiet --upgrade pip +python3 -m pip install --quiet pydantic pytest build +pytest -q + +echo "Building source distribution and wheel..." +python3 -m build + +echo "All checks passed." diff --git a/tests/test_contracts.py b/tests/test_contracts.py new file mode 100644 index 0000000..3682110 --- /dev/null +++ b/tests/test_contracts.py @@ -0,0 +1,14 @@ +from mercuriusmesh.contracts import LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta + + +def test_contract_models(): + lb = LocalBook(venue_id="V1", asset="TEST/USD", best_bid=100.0, best_bid_size=10, best_ask=100.5, best_ask_size=12) + assert lb.venue_id == "V1" + oe = OrderEvent(order_id="o1", venue_id="V1", asset="TEST/USD", side="buy", price=None, size=1) + assert oe.status == "submitted" + qs = QuoteSnapshot(venue_id="V1", asset="TEST/USD", bids=[[100.0, 10]], asks=[[100.5, 12]]) + assert qs.bids[0][0] == 100.0 + ss = SharedSignals(asset="TEST/USD", mid_price=100.25, liquidity=22) + assert ss.asset == "TEST/USD" + pd = PlanDelta(plan_id="p1", author="a1", asset="TEST/USD", venue_id="V1", side="buy", price=100.1, size=2) + assert pd.plan_id == "p1" diff --git a/tests/test_simulation_replay.py b/tests/test_simulation_replay.py new file mode 100644 index 0000000..1e051a1 --- /dev/null +++ b/tests/test_simulation_replay.py @@ -0,0 +1,27 @@ +from mercuriusmesh.simulator import DeterministicSimulator +from mercuriusmesh.adapters import ToyVenueAdapter +from mercuriusmesh.contracts import OrderEvent + + +def test_deterministic_replay(): + sim = DeterministicSimulator() + a = ToyVenueAdapter("V1", initial_bid=100.0, initial_ask=100.5) + b = ToyVenueAdapter("V2", initial_bid=99.8, initial_ask=100.3) + sim.add_adapter(a) + sim.add_adapter(b) + + o1 = OrderEvent(order_id="o1", venue_id="V1", asset="TEST/USD", side="buy", price=None, size=1) + o2 = OrderEvent(order_id="o2", venue_id="V2", asset="TEST/USD", side="sell", price=None, size=2) + + sim.submit_order(o1) + sim.submit_order(o2) + + # snapshot state + s1 = sim.compute_shared_signals() + + # replay and compare + sim.replay() + s2 = sim.compute_shared_signals() + + assert s1.mid_price == s2.mid_price + assert s1.liquidity == s2.liquidity