build(agent): semicolon#54de0b iteration

This commit is contained in:
agent-54de0bcc6a17828b 2026-04-24 19:26:10 +02:00
parent c41d1a1db4
commit d4fc4fd742
10 changed files with 245 additions and 38 deletions

View File

@ -7,13 +7,13 @@ Tech stack
- Runtime packages: pydantic (data contracts), pytest (tests) - Runtime packages: pydantic (data contracts), pytest (tests)
Project layout Project layout
- mercuriusmesh/: core package with contracts, toy adapters, and a minimal deterministic simulator - mercuriusmesh/: core package with contracts, toy adapters, and a deterministic simulator with latency and partition support
- tests/: pytest test-suite - tests/: pytest test-suite
- test.sh: runs tests and builds the package - test.sh: runs tests and builds the package
Guiding principles Guiding principles
- Keep core contracts minimal and canonical: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta - Keep core contracts minimal and canonical: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta
- Deterministic replay: append-only event log; replay reinitialises adapters and replays events - Deterministic replay: append-only event log; replay reinitialises adapters, replays events, and preserves queued/offline order delivery
- Small, well-tested chunks: this agent implements Phase 0 core contracts + toy two-venue demo - Small, well-tested chunks: this agent implements Phase 0 core contracts + toy two-venue demo
Testing Testing

View File

@ -6,11 +6,12 @@ offline-first sandbox for cross-venue market microstructure experiments.
What is included What is included
- Core pydantic contracts: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta - Core pydantic contracts: LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta
- A tiny deterministic two-venue simulator and toy adapters - A tiny deterministic two-venue simulator and toy adapters
- Tests validating model serialization and deterministic replay - Deterministic latency, packet-loss, and partition handling for offline-first replay
- Tests validating model serialization, delayed delivery, and replay reconciliation
Run tests and build Run tests and build
1. Install dev deps: `pip install -r requirements.txt` or `pip install pydantic pytest 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` 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, This is a focused chunk (Phase 0). Future work: ADMM-lite coordinator, delta-sync,
delta-sync, governance logs, conformance harness and adapters marketplace. governance logs, conformance harness and adapters marketplace.

View File

@ -3,7 +3,7 @@
Contains canonical contracts and a tiny deterministic two-venue simulator used in tests. Contains canonical contracts and a tiny deterministic two-venue simulator used in tests.
""" """
from .contracts import LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta from .contracts import LocalBook, OrderEvent, QuoteSnapshot, SharedSignals, PlanDelta, NetworkLink
from .simulator import DeterministicSimulator from .simulator import DeterministicSimulator
__all__ = [ __all__ = [
@ -12,5 +12,6 @@ __all__ = [
"QuoteSnapshot", "QuoteSnapshot",
"SharedSignals", "SharedSignals",
"PlanDelta", "PlanDelta",
"NetworkLink",
"DeterministicSimulator", "DeterministicSimulator",
] ]

View File

@ -9,34 +9,51 @@ class ToyVenueAdapter:
behaviour is achieved by not using random jitter and relying on caller ordering. 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): def __init__(self, venue_id: str, initial_bid: float = 100.0, initial_ask: float = 100.5, asset: str = "TEST/USD"):
self.venue_id = venue_id self.venue_id = venue_id
self.asset = asset
self.initial_bid = initial_bid
self.initial_ask = initial_ask
self.best_bid = initial_bid self.best_bid = initial_bid
self.best_ask = initial_ask self.best_ask = initial_ask
self.listeners: List[Callable[[QuoteSnapshot], None]] = [] self.listeners: List[Callable[[QuoteSnapshot], None]] = []
def reset(self):
self.best_bid = self.initial_bid
self.best_ask = self.initial_ask
def _normalize_book(self):
if self.best_ask <= self.best_bid:
midpoint = (self.best_bid + self.best_ask) / 2
self.best_bid = round(midpoint - 0.005, 4)
self.best_ask = round(midpoint + 0.005, 4)
def on_snapshot(self, cb: Callable[[QuoteSnapshot], None]): def on_snapshot(self, cb: Callable[[QuoteSnapshot], None]):
self.listeners.append(cb) self.listeners.append(cb)
def publish_snapshot(self): def publish_snapshot(self):
snap = QuoteSnapshot( snap = QuoteSnapshot(
venue_id=self.venue_id, venue_id=self.venue_id,
asset="TEST/USD", asset=self.asset,
bids=[[self.best_bid, 100.0]], bids=[[self.best_bid, 100.0]],
asks=[[self.best_ask, 100.0]], asks=[[self.best_ask, 100.0]],
) )
for cb in self.listeners: for cb in list(self.listeners):
cb(snap) cb(snap)
def handle_order(self, order: OrderEvent) -> OrderEvent: def handle_order(self, order: OrderEvent) -> OrderEvent:
# Very small deterministic matcher: marketable at top-of-book executes # Very small deterministic matcher: marketable at top-of-book executes
impact = round(max(order.size, 1.0) * 0.05, 4)
if order.side == "buy" and order.price is None: if order.side == "buy" and order.price is None:
order.status = "filled" order.status = "filled"
# naive mid update self.best_bid = round(self.best_bid + impact, 4)
self.best_bid = max(self.best_bid, order.price or self.best_bid) self.best_ask = round(self.best_ask + impact, 4)
self._normalize_book()
elif order.side == "sell" and order.price is None: elif order.side == "sell" and order.price is None:
order.status = "filled" order.status = "filled"
self.best_ask = min(self.best_ask, order.price or self.best_ask) self.best_bid = round(self.best_bid - impact, 4)
self.best_ask = round(self.best_ask - impact, 4)
self._normalize_book()
else: else:
order.status = "accepted" order.status = "accepted"
# publish new snapshot deterministically # publish new snapshot deterministically

View File

@ -32,8 +32,8 @@ class OrderEvent(BaseModel):
class QuoteSnapshot(BaseModel): class QuoteSnapshot(BaseModel):
venue_id: str venue_id: str
asset: str asset: str
bids: List[List[float]] = [] # [[price, size], ...] bids: List[List[float]] = Field(default_factory=list) # [[price, size], ...]
asks: List[List[float]] = [] asks: List[List[float]] = Field(default_factory=list)
timestamp: datetime = Field(default_factory=datetime.utcnow) timestamp: datetime = Field(default_factory=datetime.utcnow)
@ -55,3 +55,14 @@ class PlanDelta(BaseModel):
price: Optional[float] price: Optional[float]
size: float size: float
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=datetime.utcnow)
class NetworkLink(BaseModel):
"""Deterministic connectivity and delay profile for one venue."""
venue_id: str
connected: bool = True
latency_ms: int = 0
jitter_ms: int = 0
packet_loss: float = 0.0
updated_at: datetime = Field(default_factory=datetime.utcnow)

View File

@ -1,6 +1,24 @@
from typing import Dict, List from dataclasses import dataclass, field
from heapq import heappop, heappush
import hashlib
from typing import Any, Dict, List, Optional
from .adapters import ToyVenueAdapter from .adapters import ToyVenueAdapter
from .contracts import OrderEvent, LocalBook, SharedSignals from .contracts import OrderEvent, LocalBook, SharedSignals, NetworkLink
@dataclass(order=True)
class _ScheduledAction:
due_tick: int
sequence: int
kind: str = field(compare=False)
payload: Dict[str, Any] = field(compare=False, default_factory=dict)
def _copy_order(order: OrderEvent) -> OrderEvent:
copier = getattr(order, "model_copy", None)
if copier is not None:
return copier(deep=True)
return order.copy(deep=True)
class DeterministicSimulator: class DeterministicSimulator:
@ -13,15 +31,88 @@ class DeterministicSimulator:
def __init__(self, seed: int = 0): def __init__(self, seed: int = 0):
self.seed = seed self.seed = seed
self.adapters: Dict[str, ToyVenueAdapter] = {} self.adapters: Dict[str, ToyVenueAdapter] = {}
self.event_log: List[OrderEvent] = [] self.event_log: List[Dict[str, Any]] = []
self.state: Dict[str, LocalBook] = {} self.state: Dict[str, LocalBook] = {}
self.current_tick = 0
self._sequence = 0
self._recording = True
self._pending: List[_ScheduledAction] = []
self._offline_queues: Dict[str, List[OrderEvent]] = {}
# store adapter initial params so replay can re-create identical adapters # store adapter initial params so replay can re-create identical adapters
self._adapter_initials: Dict[str, Dict[str, float]] = {} self._adapter_initials: Dict[str, Dict[str, Any]] = {}
self._links: Dict[str, NetworkLink] = {}
def _next_sequence(self) -> int:
self._sequence += 1
return self._sequence
def _log(self, record: Dict[str, Any]) -> None:
if self._recording:
self.event_log.append(record)
def _link_for(self, venue_id: str) -> NetworkLink:
return self._links.setdefault(venue_id, NetworkLink(venue_id=venue_id))
def _should_drop(self, order: OrderEvent, link: NetworkLink) -> bool:
if link.packet_loss <= 0:
return False
digest = hashlib.sha256(f"{self.seed}:{link.venue_id}:{order.order_id}".encode("utf-8")).hexdigest()
roll = int(digest[:8], 16) / 0xFFFFFFFF
return roll < link.packet_loss
def _deterministic_jitter(self, order: OrderEvent, link: NetworkLink) -> int:
if link.jitter_ms <= 0:
return 0
digest = hashlib.sha256(f"{self.seed}:{link.venue_id}:{order.order_id}:jitter".encode("utf-8")).hexdigest()
return int(digest[:8], 16) % (link.jitter_ms + 1)
def _schedule(self, kind: str, due_tick: int, payload: Dict[str, Any]) -> None:
heappush(self._pending, _ScheduledAction(due_tick=due_tick, sequence=self._next_sequence(), kind=kind, payload=payload))
def _deliver_ready(self) -> None:
while self._pending and self._pending[0].due_tick <= self.current_tick:
action = heappop(self._pending)
if action.kind == "order":
order = action.payload["order"]
adapter = self.adapters.get(order.venue_id)
if adapter is None:
order.status = "rejected"
continue
adapter.handle_order(order)
def _enqueue_offline_order(self, order: OrderEvent) -> None:
self._offline_queues.setdefault(order.venue_id, []).append(order)
def _flush_offline_queue(self, venue_id: str) -> None:
queued = self._offline_queues.get(venue_id, [])
if not queued:
return
link = self._link_for(venue_id)
if not link.connected:
return
self._offline_queues[venue_id] = []
for order in queued:
self._schedule_order(order, link)
def _schedule_order(self, order: OrderEvent, link: NetworkLink) -> OrderEvent:
if self._should_drop(order, link):
order.status = "dropped"
return order
delay = link.latency_ms + self._deterministic_jitter(order, link)
order.status = "accepted"
self._schedule("order", self.current_tick + delay, {"order": order})
return order
def add_adapter(self, adapter: ToyVenueAdapter): def add_adapter(self, adapter: ToyVenueAdapter):
self.adapters[adapter.venue_id] = adapter self.adapters[adapter.venue_id] = adapter
# capture initial topology for deterministic replay # capture initial topology for deterministic replay
self._adapter_initials[adapter.venue_id] = {"best_bid": adapter.best_bid, "best_ask": adapter.best_ask} self._adapter_initials[adapter.venue_id] = {
"best_bid": adapter.best_bid,
"best_ask": adapter.best_ask,
"asset": adapter.asset,
}
self._links.setdefault(adapter.venue_id, NetworkLink(venue_id=adapter.venue_id))
self._offline_queues.setdefault(adapter.venue_id, [])
def snapshot_listener(snap): def snapshot_listener(snap):
# update simple LocalBook # update simple LocalBook
@ -35,29 +126,93 @@ class DeterministicSimulator:
) )
adapter.on_snapshot(snapshot_listener) adapter.on_snapshot(snapshot_listener)
adapter.publish_snapshot()
def set_link(self, venue_id: str, connected: bool, latency_ms: Optional[int] = None, jitter_ms: Optional[int] = None, packet_loss: Optional[float] = None) -> NetworkLink:
link = self._link_for(venue_id)
link.connected = connected
if latency_ms is not None:
link.latency_ms = latency_ms
if jitter_ms is not None:
link.jitter_ms = jitter_ms
if packet_loss is not None:
link.packet_loss = packet_loss
self._log({"kind": "link", "venue_id": venue_id, "connected": connected, "latency_ms": link.latency_ms, "jitter_ms": link.jitter_ms, "packet_loss": link.packet_loss})
if connected:
self._flush_offline_queue(venue_id)
self._deliver_ready()
return link
def submit_order(self, order: OrderEvent) -> OrderEvent: def submit_order(self, order: OrderEvent) -> OrderEvent:
# append to log and route to adapter deterministically by venue_id # append to log and route to adapter deterministically by venue_id
self.event_log.append(order) self._log({"kind": "order", "order": _copy_order(order)})
adapter = self.adapters.get(order.venue_id) adapter = self.adapters.get(order.venue_id)
if adapter is None: if adapter is None:
order.status = "rejected" order.status = "rejected"
return order return order
return adapter.handle_order(order) link = self._link_for(order.venue_id)
if not link.connected:
order.status = "queued"
self._enqueue_offline_order(order)
return order
result = self._schedule_order(order, link)
self._deliver_ready()
return result
def advance(self, ticks: int = 1) -> None:
if ticks < 0:
raise ValueError("ticks must be non-negative")
if ticks:
self._log({"kind": "advance", "ticks": ticks})
self.current_tick += ticks
self._deliver_ready()
def drain(self) -> None:
while self._pending:
next_tick = self._pending[0].due_tick
if next_tick > self.current_tick:
self.current_tick = next_tick
self._deliver_ready()
def replay(self) -> None: def replay(self) -> None:
# clear state and re-apply events in order # clear state and re-apply events in order
records = list(self.event_log)
self.state = {} self.state = {}
self.current_tick = 0
self._sequence = 0
self._pending = []
self._offline_queues = {vid: [] for vid in self._adapter_initials}
# re-create adapters to ensure deterministic initial conditions # re-create adapters to ensure deterministic initial conditions
adapters_copy = {} adapters_copy = {}
for vid, initials in self._adapter_initials.items(): 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)) adapters_copy[vid] = ToyVenueAdapter(
vid,
initial_bid=initials.get("best_bid", 100.0),
initial_ask=initials.get("best_ask", 100.5),
asset=initials.get("asset", "TEST/USD"),
)
self.adapters = adapters_copy self.adapters = adapters_copy
self._links = {vid: NetworkLink(venue_id=vid) for vid in self._adapter_initials}
self._recording = False
for vid in list(self.adapters.keys()): for vid in list(self.adapters.keys()):
# re-register listeners and initial state # re-register listeners and initial state
self.add_adapter(self.adapters[vid]) self.add_adapter(self.adapters[vid])
for e in list(self.event_log): for record in records:
self.submit_order(e) kind = record.get("kind")
if kind == "link":
self.set_link(
venue_id=record["venue_id"],
connected=record["connected"],
latency_ms=record.get("latency_ms"),
jitter_ms=record.get("jitter_ms"),
packet_loss=record.get("packet_loss"),
)
elif kind == "order":
self.submit_order(_copy_order(record["order"]))
elif kind == "advance":
self.advance(record["ticks"])
self.drain()
self._recording = True
def compute_shared_signals(self, asset: str = "TEST/USD") -> SharedSignals: def compute_shared_signals(self, asset: str = "TEST/USD") -> SharedSignals:
# simple average mid from known LocalBook entries # simple average mid from known LocalBook entries

View File

@ -5,8 +5,11 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "mercuriusmesh" name = "mercuriusmesh"
version = "0.0.1" version = "0.0.1"
description = "MercuriusMesh: Cross-venue market microstructure sandbox with deterministic replay"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
license = "MIT"
authors = [{ name = "OpenCode Agent" }]
dependencies = ["pydantic>=1.10"] dependencies = ["pydantic>=1.10"]
[tool.setuptools] [tool.setuptools]

View File

@ -1,14 +0,0 @@
[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

View File

@ -0,0 +1,27 @@
from mercuriusmesh.adapters import ToyVenueAdapter
from mercuriusmesh.contracts import OrderEvent
from mercuriusmesh.simulator import DeterministicSimulator
def test_partitioned_orders_flush_after_reconnect():
sim = DeterministicSimulator(seed=7)
sim.add_adapter(ToyVenueAdapter("V1", initial_bid=100.0, initial_ask=100.5))
sim.set_link("V1", connected=False)
order = OrderEvent(order_id="queued-1", venue_id="V1", asset="TEST/USD", side="buy", price=None, size=3)
queued = sim.submit_order(order)
assert queued.status == "queued"
before = sim.compute_shared_signals()
sim.set_link("V1", connected=True, latency_ms=2)
assert queued.status == "accepted"
sim.advance(1)
mid = sim.compute_shared_signals()
assert mid.mid_price == before.mid_price
sim.advance(1)
after = sim.compute_shared_signals()
assert after.mid_price != before.mid_price
assert after.liquidity == before.liquidity

View File

@ -13,11 +13,16 @@ def test_deterministic_replay():
o1 = OrderEvent(order_id="o1", venue_id="V1", asset="TEST/USD", side="buy", price=None, size=1) 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) o2 = OrderEvent(order_id="o2", venue_id="V2", asset="TEST/USD", side="sell", price=None, size=2)
sim.set_link("V1", connected=False)
sim.submit_order(o1) sim.submit_order(o1)
sim.set_link("V1", connected=True, latency_ms=1)
sim.advance(1)
sim.submit_order(o2) sim.submit_order(o2)
sim.advance(1)
# snapshot state # snapshot state
s1 = sim.compute_shared_signals() s1 = sim.compute_shared_signals()
log_len = len(sim.event_log)
# replay and compare # replay and compare
sim.replay() sim.replay()
@ -25,3 +30,4 @@ def test_deterministic_replay():
assert s1.mid_price == s2.mid_price assert s1.mid_price == s2.mid_price
assert s1.liquidity == s2.liquidity assert s1.liquidity == s2.liquidity
assert len(sim.event_log) == log_len