From bba46497cc8b45825788c25ad75d6efd48bcf6f3 Mon Sep 17 00:00:00 2001 From: agent-tmlr7wo3s0 Date: Fri, 24 Apr 2026 20:44:00 +0200 Subject: [PATCH] feat: implement DeltaTrace core - event graph, replay engine, audit log, adapters Core components: - Event graph model with DAG structure, topological ordering, serialization - Deterministic replay engine with handler registration and type filtering - Crypto-signed tamper-evident audit log with Ed25519 and hash chaining - FIX feed adapter and sandbox exchange adapter - 30 unit tests, all passing AGENT_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudElkIjoiYTM4MDdiN2Y3NjQ2MTRlOTk5NWQzMzE3MDIwODU2Y2VhMjZlZmVkNDZjOTBmYjU4ODBkZjVmNTIyMGU1Y2E4YSIsInR5cGUiOiJhZ2VudCIsImlhdCI6MTc3NzA1NTQxNiwiZXhwIjoxNzc3MDk4NjE2fQ.QhBfLiMGiT-0RIt6nvDuw0BBaH4P58bA9XwjWcA0egc --- =41.0.0 | 0 README.md | 30 +++- deltatrace/__init__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 261 bytes .../__pycache__/adapters.cpython-312.pyc | Bin 0 -> 7333 bytes .../__pycache__/audit_log.cpython-312.pyc | Bin 0 -> 5170 bytes deltatrace/__pycache__/demo.cpython-312.pyc | Bin 0 -> 4234 bytes .../__pycache__/event_graph.cpython-312.pyc | Bin 0 -> 8288 bytes .../__pycache__/replay_engine.cpython-312.pyc | Bin 0 -> 3616 bytes deltatrace/adapters.py | 169 ++++++++++++++++++ deltatrace/audit_log.py | 129 +++++++++++++ deltatrace/demo.py | 71 ++++++++ deltatrace/event_graph.py | 156 ++++++++++++++++ deltatrace/replay_engine.py | 74 ++++++++ requirements.txt | 1 + test.sh | 18 ++ tests/__init__.py | 0 tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 146 bytes ...test_adapters.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6583 bytes ...est_audit_log.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 4867 bytes ...t_event_graph.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9537 bytes ...replay_engine.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5206 bytes tests/test_adapters.py | 76 ++++++++ tests/test_audit_log.py | 60 +++++++ tests/test_event_graph.py | 91 ++++++++++ tests/test_replay_engine.py | 66 +++++++ 26 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 =41.0.0 create mode 100644 deltatrace/__init__.py create mode 100644 deltatrace/__pycache__/__init__.cpython-312.pyc create mode 100644 deltatrace/__pycache__/adapters.cpython-312.pyc create mode 100644 deltatrace/__pycache__/audit_log.cpython-312.pyc create mode 100644 deltatrace/__pycache__/demo.cpython-312.pyc create mode 100644 deltatrace/__pycache__/event_graph.cpython-312.pyc create mode 100644 deltatrace/__pycache__/replay_engine.cpython-312.pyc create mode 100644 deltatrace/adapters.py create mode 100644 deltatrace/audit_log.py create mode 100644 deltatrace/demo.py create mode 100644 deltatrace/event_graph.py create mode 100644 deltatrace/replay_engine.py create mode 100644 requirements.txt create mode 100755 test.sh create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_adapters.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_audit_log.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_event_graph.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_replay_engine.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/test_adapters.py create mode 100644 tests/test_audit_log.py create mode 100644 tests/test_event_graph.py create mode 100644 tests/test_replay_engine.py diff --git a/=41.0.0 b/=41.0.0 new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 5684009..053e5fa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ -# idea105-deltatrace +# DeltaTrace -DeltaTrace: Deterministic Replayable Latency & Compliance Tracing for Live Market-Execution Pipelines \ No newline at end of file +Deterministic Replayable Latency & Compliance Tracing for Live Market-Execution Pipelines. + +A cross-layer traceability toolkit for high-frequency trading systems that enables deterministic replay of order lifecycles, end-to-end latency accounting, governance-ready audit trails, and vendor-agnostic adapters. + +## Core Components + +- **Event Graph**: Models market events (ticks, signals, orders, fills, risk checks) as a DAG with causal edges and latency budgets +- **Replay Engine**: Deterministically replays captured event streams to reproduce decision paths +- **Audit Log**: Crypto-signed, tamper-evident governance log for regulatory reviews +- **Adapters**: Pluggable adapters for FIX feeds and exchange gateways + +## Quick Start + +```bash +pip install -r requirements.txt +python -m deltatrace.demo +``` + +## Testing + +```bash +./test.sh +``` + +## License + +MIT diff --git a/deltatrace/__init__.py b/deltatrace/__init__.py new file mode 100644 index 0000000..73c5012 --- /dev/null +++ b/deltatrace/__init__.py @@ -0,0 +1,3 @@ +"""DeltaTrace: Deterministic Replayable Latency & Compliance Tracing.""" + +__version__ = "0.1.0" diff --git a/deltatrace/__pycache__/__init__.cpython-312.pyc b/deltatrace/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9326182fbddef325393fa30735205a7126c9a18 GIT binary patch literal 261 zcmX@j%ge<81gtw=XC?sY#~=<2FhUuhd4P=R3@Hpz3@MCJjFn89%vDY)bpQYW literal 0 HcmV?d00001 diff --git a/deltatrace/__pycache__/adapters.cpython-312.pyc b/deltatrace/__pycache__/adapters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95554ec5f96378b0144e5083118f864672e95928 GIT binary patch literal 7333 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 Dz}*Yo literal 0 HcmV?d00001 diff --git a/deltatrace/__pycache__/audit_log.cpython-312.pyc b/deltatrace/__pycache__/audit_log.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..418ace7cd4a6dbf1e39caeb1ac14ceec9086cf66 GIT binary patch literal 5170 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 literal 0 HcmV?d00001 diff --git a/deltatrace/__pycache__/demo.cpython-312.pyc b/deltatrace/__pycache__/demo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fe386d061ef8dd44980028c5996e493447b2365 GIT binary patch literal 4234 zcmb7HU2GHC6&~B;zwu8XzmSB*KmwRJ&JTpYhOlY=poEYTw$KQe9ei(`ab|2YGl{U} zv|edfC1NEKsS=@NOHdyOVx{uXhxCm;b)~+HH=(9?S+!CtRjYlWf_>>z&mDU(fn@1k zDLH4(J@=e*&pG$r&wugxI0lBg{H=HxxPQ_Qb6AVai#Ra17{rV+$bzhrMYfJwEex_D zR&k6r@=cUm+T&O%iJhud^;tN>>c$2??LX5*-;<%BW_|Z!zeB* zgMAN+&MZvIm2w5W>EHHZIdL?ATnv8&9A)t!UI7R_s5`nsuh=#duXuLrS+1!WPJ388q3@hRkL~#nI zzOm35EAjWAywHy^>PA8m?33ydFnT3%t?yc7T!=?;Q4Tg--PG=2A_}XxJ{sfzjghzw z{#+`FLCJg82)HjEg?$f2is>?l3;x^S@5LV+v``uiz!+vyrx}gTua#LeyD6h({oMAj zSnz49<}_zw)b<%=5rmltgw2Gxor)j00`n3Lmk0Q&OTC_j*!P{&~g2VnzOs_}?qpGdYOfjw(?V zs$O7U)s5R=2i!Y%ux^wYZr#{xeo*WIGvX3OThLZii|Ss>y=Q~SJ@1Ts-54{TbtA7| z6S;N0o1lV~0)c61~HqMwmHTl9(LYorYT&?YYCEy`R16*fmiF zVP&VOU!ohvu1~%PJ;jONQ#$LVmVLP?(X5iw4?2CN4TT4NIQzOov>}Kt*t=^vF`66){YKh zyCNcD6VwS}J#YY|hZPpw!J}$7URdIi6EeKIlQL*drlR|e@$K=Tjkr{ex{s!a8;J^z zXksF62yr~BjVtuUOnU(}Of+{Q5Lndx3IXm3ouEX_>&1$q@H+Vmas`dRmGv0g@K6dk#QWEAPxnqlafYU_!?+W zDmcA~u5Ip8M0J8M=-3kq!b;FiSPd|QvK@jY_M{?4uo9p=tBMGxJ#YrFa2A9Sx+NeJF}mU|a=g0-U>mvD8q&sAu-$aCh1kZXZiK zWx!`iA)e;?!KWl@DbOx6a1TH{O%V-46$Js{BXBb%NFq8K2<}iQ22m(jQ2^gC3Vk?< zD_X>T3dJhKAp)XOHR2W!;xUC##A6WNfO|@fp%hUt6jN~z#l0#Yc#ruCwD)5t#r=P>kDV#hRxq^$7C7nCp}o3w-mPRyo{tHeK6OdPL9 z5(=0vrj0nJup*A7f@Q>R?3H-N#B01zM4e8xfI5k70)ux0!4fCckku&h(AF*FOC?r3 zRm@YN8=>!LH?p)ph}YP!sp^3C2NAAj z6W~BaaLXE2PK1*w!pcx~R?d#xIGyc#?qI67E;^R3J$C8C7xWLua{g#`;A#1`e0f8z zykYVDN_lhk)K~tk*R4-ozMEw;W%Jf2uDa(ormXfm7h~V`)K`A<;>^W`V7_)=u6EyY zt$y~rj>TLkw&I(}_CICazdG~Lnc4nTwvO^c9}Ug^X#VP4SH7YlSJALIc3-?FKKey= zXqD}USiGBWcFuGzxbs^=xh@1g>{ipj?AOG z(c96b%hXWkTZZ(*C-ifp`uO+SIdJRH+@bm2y92ie^rmCfP!|}gth?1U*R^zLdF+cR z{j#7>By&5ju2d-5!LR(aZzrYhAH_`;tPL)0zI`TNAI{Z>AIa2Y?=vTNy>rF6Wxnl8 zXFW}+f5tzXS?r$sWxghus|hZ3E?XWP&o>>*H643=>AHW_H3CtTRp$MTIe+6{{d)>N zy))j~v$sB&`(Qq{RQ~&I54Polow;DAzW113-}Tt~XYNnj6W1wProAs+jKB8AWvGR( z;^w89OYE3M%k;TUQp?jf~_geIAtvc7X%C$&Y& zscg_I8g*{>D!YeHoS6MtzN#r#)udO3mQFqz%O5(BN=~@jPvLo`_G zT)~SWPGL1Hz)K=yxI80aT=*4Qyc>&$UoBmla52)~m^|EG#)YYbA%McgYTSmt!p*}M z&o#ZWjFU)Xj0-a6`3H^!$wESCRRSquA}m$3|6 s{>^lL&20ZW6aJdn^TKh!(*8VdVOa0=^q01+e`8&r^g}h-TEp)D0OG3S=l}o! literal 0 HcmV?d00001 diff --git a/deltatrace/__pycache__/event_graph.cpython-312.pyc b/deltatrace/__pycache__/event_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1e3d3c3ca9b4083ccc05b4344b40fb5d3841b62 GIT binary patch literal 8288 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 iN8{uBNEE3@ODnU=4tR<<3EqxPHjuNXj| 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 str: + ... + + @abc.abstractmethod + def ingest(self, raw_data: dict) -> list[Event]: + """Parse raw data into typed events.""" + ... + + +class FIXFeedAdapter(BaseAdapter): + """Adapter for FIX-protocol market data feeds. + + Parses FIX-like message dicts into MDTick events. + Supports tags: 35 (MsgType), 55 (Symbol), 44 (Price), + 38 (OrderQty), 54 (Side), 60 (TransactTime). + """ + + @property + def name(self) -> str: + return "fix_feed" + + def ingest(self, raw_data: dict) -> list[Event]: + events = [] + msg_type = raw_data.get("35", "") + + if msg_type == "W": # Market Data Snapshot + event = Event.create( + event_type=EventType.MD_TICK, + payload={ + "symbol": raw_data.get("55", ""), + "bid": float(raw_data.get("bid", 0)), + "ask": float(raw_data.get("ask", 0)), + "last": float(raw_data.get("44", 0)), + "volume": int(raw_data.get("38", 0)), + }, + source_adapter=self.name, + ) + events.append(event) + + elif msg_type == "8": # Execution Report + event = Event.create( + event_type=EventType.FILL, + payload={ + "symbol": raw_data.get("55", ""), + "side": raw_data.get("54", ""), + "price": float(raw_data.get("44", 0)), + "qty": int(raw_data.get("38", 0)), + "exec_type": raw_data.get("150", ""), + }, + source_adapter=self.name, + ) + events.append(event) + + elif msg_type == "D": # New Order Single + event = Event.create( + event_type=EventType.ORDER, + payload={ + "symbol": raw_data.get("55", ""), + "side": raw_data.get("54", ""), + "price": float(raw_data.get("44", 0)), + "qty": int(raw_data.get("38", 0)), + }, + source_adapter=self.name, + ) + events.append(event) + + return events + + +class SandboxExchangeAdapter(BaseAdapter): + """Simulated exchange gateway for testing and validation. + + Generates synthetic market data, accepts orders, and produces + fills with configurable latency and fill rates. + """ + + def __init__(self, fill_rate: float = 0.8, latency_ms: float = 1.0) -> None: + self._fill_rate = fill_rate + self._latency_ms = latency_ms + + @property + def name(self) -> str: + return "sandbox_exchange" + + def ingest(self, raw_data: dict) -> list[Event]: + msg_type = raw_data.get("type", "") + events = [] + + if msg_type == "tick": + event = Event.create( + event_type=EventType.MD_TICK, + payload={ + "symbol": raw_data.get("symbol", "TEST"), + "bid": raw_data.get("bid", 100.0), + "ask": raw_data.get("ask", 100.05), + "last": raw_data.get("last", 100.02), + }, + source_adapter=self.name, + ) + events.append(event) + + elif msg_type == "order": + order = Event.create( + event_type=EventType.ORDER, + payload=raw_data, + source_adapter=self.name, + ) + events.append(order) + + # Simulate fill with configured probability + if random.random() < self._fill_rate: + time.sleep(self._latency_ms / 1000) + fill = Event.create( + event_type=EventType.FILL, + payload={ + "order_ref": order.id, + "symbol": raw_data.get("symbol", "TEST"), + "price": raw_data.get("price", 100.0), + "qty": raw_data.get("qty", 100), + "status": "filled", + }, + source_adapter=self.name, + ) + events.append(fill) + + return events + + def generate_ticks(self, symbol: str, count: int, base_price: float = 100.0) -> list[Event]: + """Generate synthetic tick data for testing.""" + events = [] + price = base_price + for _ in range(count): + delta = random.gauss(0, 0.05) + price += delta + spread = abs(random.gauss(0.03, 0.01)) + event = Event.create( + event_type=EventType.MD_TICK, + payload={ + "symbol": symbol, + "bid": round(price - spread / 2, 4), + "ask": round(price + spread / 2, 4), + "last": round(price, 4), + }, + source_adapter=self.name, + ) + events.append(event) + return events diff --git a/deltatrace/audit_log.py b/deltatrace/audit_log.py new file mode 100644 index 0000000..4f2b98e --- /dev/null +++ b/deltatrace/audit_log.py @@ -0,0 +1,129 @@ +"""Crypto-signed, tamper-evident audit log for governance and compliance. + +Each entry is signed with Ed25519 and chained via previous-hash to form +a tamper-evident log suitable for regulatory reviews. +""" + +from __future__ import annotations + +import hashlib +import json +import time +from dataclasses import dataclass +from typing import Optional + +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) +from cryptography.hazmat.primitives import serialization + + +@dataclass +class AuditEntry: + """A single signed audit log entry.""" + + sequence: int + timestamp_ns: int + event_id: str + action: str + details: dict + prev_hash: str + entry_hash: str + signature: bytes + + def to_dict(self) -> dict: + return { + "sequence": self.sequence, + "timestamp_ns": self.timestamp_ns, + "event_id": self.event_id, + "action": self.action, + "details": self.details, + "prev_hash": self.prev_hash, + "entry_hash": self.entry_hash, + "signature": self.signature.hex(), + } + + +class AuditLog: + """Append-only, crypto-signed audit log with hash chaining.""" + + def __init__(self, private_key: Ed25519PrivateKey) -> None: + self._private_key = private_key + self._public_key = private_key.public_key() + self._entries: list[AuditEntry] = [] + self._prev_hash = "0" * 64 # Genesis hash + + def append(self, event_id: str, action: str, details: Optional[dict] = None) -> AuditEntry: + details = details or {} + sequence = len(self._entries) + timestamp_ns = time.time_ns() + + # Compute hash of this entry's content + content = json.dumps({ + "sequence": sequence, + "timestamp_ns": timestamp_ns, + "event_id": event_id, + "action": action, + "details": details, + "prev_hash": self._prev_hash, + }, sort_keys=True) + + entry_hash = hashlib.sha256(content.encode()).hexdigest() + signature = self._private_key.sign(entry_hash.encode()) + + entry = AuditEntry( + sequence=sequence, + timestamp_ns=timestamp_ns, + event_id=event_id, + action=action, + details=details, + prev_hash=self._prev_hash, + entry_hash=entry_hash, + signature=signature, + ) + + self._entries.append(entry) + self._prev_hash = entry_hash + return entry + + def verify(self, public_key: Optional[Ed25519PublicKey] = None) -> bool: + """Verify the entire chain: hash linkage + signatures.""" + pk = public_key or self._public_key + prev_hash = "0" * 64 + + for entry in self._entries: + # Verify chain linkage + if entry.prev_hash != prev_hash: + return False + + # Recompute hash + content = json.dumps({ + "sequence": entry.sequence, + "timestamp_ns": entry.timestamp_ns, + "event_id": entry.event_id, + "action": entry.action, + "details": entry.details, + "prev_hash": entry.prev_hash, + }, sort_keys=True) + + expected_hash = hashlib.sha256(content.encode()).hexdigest() + if entry.entry_hash != expected_hash: + return False + + # Verify signature + try: + pk.verify(entry.signature, entry.entry_hash.encode()) + except Exception: + return False + + prev_hash = entry.entry_hash + + return True + + @property + def entries(self) -> list[AuditEntry]: + return list(self._entries) + + def to_dict(self) -> dict: + return {"entries": [e.to_dict() for e in self._entries]} diff --git a/deltatrace/demo.py b/deltatrace/demo.py new file mode 100644 index 0000000..f20ffb1 --- /dev/null +++ b/deltatrace/demo.py @@ -0,0 +1,71 @@ +"""Demo: end-to-end DeltaTrace pipeline. + +Generates synthetic market data, builds an event graph, +replays it deterministically, and produces a signed audit log. +""" + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from .adapters import FIXFeedAdapter, SandboxExchangeAdapter +from .audit_log import AuditLog +from .event_graph import EventGraph, EventType +from .replay_engine import ReplayEngine + + +def main() -> None: + # 1. Setup + graph = EventGraph() + fix_adapter = FIXFeedAdapter() + sandbox = SandboxExchangeAdapter(fill_rate=1.0, latency_ms=0.1) + key = Ed25519PrivateKey.generate() + audit = AuditLog(key) + + # 2. Ingest FIX market data + fix_msg = {"35": "W", "55": "AAPL", "bid": "185.50", "ask": "185.55", "44": "185.52", "38": "1000"} + tick_events = fix_adapter.ingest(fix_msg) + for ev in tick_events: + graph.add_event(ev) + + # 3. Generate signal from tick + from .event_graph import Event + signal = Event.create(EventType.SIGNAL, {"strategy": "momentum", "direction": "buy", "strength": 0.85}) + graph.add_event(signal) + graph.add_edge(tick_events[0].id, signal.id, label="tick->signal") + + # 4. Plan delta + plan = Event.create(EventType.PLAN_DELTA, {"action": "buy", "symbol": "AAPL", "qty": 500, "limit": 185.55}) + graph.add_event(plan) + graph.add_edge(signal.id, plan.id, label="signal->plan") + + # 5. Risk check + risk = Event.create(EventType.RISK_CHECK, {"check": "position_limit", "result": "pass", "exposure": 92750}) + graph.add_event(risk) + graph.add_edge(plan.id, risk.id, label="plan->risk_check") + + # 6. Order via sandbox exchange + order_events = sandbox.ingest({"type": "order", "symbol": "AAPL", "price": 185.55, "qty": 500, "side": "buy"}) + for ev in order_events: + graph.add_event(ev) + graph.add_edge(risk.id, order_events[0].id, label="risk->order") + if len(order_events) > 1: + graph.add_edge(order_events[0].id, order_events[1].id, label="order->fill") + + # 7. Audit log + for ev in graph.topological_order(): + audit.append(ev.id, f"processed:{ev.event_type.value}", ev.payload) + + # 8. Replay + engine = ReplayEngine() + result = engine.replay(graph) + + # 9. Output + print(f"Events in graph: {len(graph.events)}") + print(f"Edges: {len(graph.edges)}") + print(f"Replay: {result.events_replayed} events, {result.total_latency_ns / 1e6:.2f}ms span") + print(f"Fidelity: {result.fidelity_score}") + print(f"Audit entries: {len(audit.entries)}") + print(f"Audit chain valid: {audit.verify()}") + + +if __name__ == "__main__": + main() diff --git a/deltatrace/event_graph.py b/deltatrace/event_graph.py new file mode 100644 index 0000000..97232f9 --- /dev/null +++ b/deltatrace/event_graph.py @@ -0,0 +1,156 @@ +"""Event graph model for market execution tracing. + +Nodes represent market events (ticks, signals, orders, fills, risk checks). +Edges encode causal relationships with precise timestamps and latency budgets. +""" + +from __future__ import annotations + +import enum +import time +import uuid +from dataclasses import dataclass, field +from typing import Optional + + +class EventType(enum.Enum): + MD_TICK = "md_tick" + SIGNAL = "signal" + PLAN_DELTA = "plan_delta" + ORDER = "order" + FILL = "fill" + RISK_CHECK = "risk_check" + + +@dataclass(frozen=True) +class Event: + """A node in the event graph.""" + + id: str + event_type: EventType + timestamp_ns: int + payload: dict + source_adapter: str = "" + + @staticmethod + def create(event_type: EventType, payload: dict, source_adapter: str = "") -> Event: + return Event( + id=uuid.uuid4().hex, + event_type=event_type, + timestamp_ns=time.time_ns(), + payload=payload, + source_adapter=source_adapter, + ) + + +@dataclass(frozen=True) +class CausalEdge: + """Directed edge encoding causality and latency between events.""" + + from_id: str + to_id: str + latency_ns: int + label: str = "" + + +class EventGraph: + """Directed acyclic graph of market events with causal edges.""" + + def __init__(self) -> None: + self._events: dict[str, Event] = {} + self._edges: list[CausalEdge] = [] + self._children: dict[str, list[str]] = {} + self._parents: dict[str, list[str]] = {} + + def add_event(self, event: Event) -> None: + self._events[event.id] = event + self._children.setdefault(event.id, []) + self._parents.setdefault(event.id, []) + + def add_edge(self, from_id: str, to_id: str, label: str = "") -> CausalEdge: + if from_id not in self._events or to_id not in self._events: + raise KeyError("Both events must exist in the graph") + latency = self._events[to_id].timestamp_ns - self._events[from_id].timestamp_ns + edge = CausalEdge(from_id=from_id, to_id=to_id, latency_ns=latency, label=label) + self._edges.append(edge) + self._children[from_id].append(to_id) + self._parents[to_id].append(from_id) + return edge + + def get_event(self, event_id: str) -> Event: + return self._events[event_id] + + def get_roots(self) -> list[Event]: + return [e for e in self._events.values() if not self._parents.get(e.id)] + + def get_children(self, event_id: str) -> list[Event]: + return [self._events[cid] for cid in self._children.get(event_id, [])] + + def get_parents(self, event_id: str) -> list[Event]: + return [self._events[pid] for pid in self._parents.get(event_id, [])] + + def topological_order(self) -> list[Event]: + """Return events in topological (causal) order.""" + visited: set[str] = set() + order: list[str] = [] + + def dfs(eid: str) -> None: + if eid in visited: + return + visited.add(eid) + for child_id in self._children.get(eid, []): + dfs(child_id) + order.append(eid) + + for eid in self._events: + dfs(eid) + + order.reverse() + return [self._events[eid] for eid in order] + + @property + def events(self) -> list[Event]: + return list(self._events.values()) + + @property + def edges(self) -> list[CausalEdge]: + return list(self._edges) + + def to_dict(self) -> dict: + return { + "events": [ + { + "id": e.id, + "type": e.event_type.value, + "timestamp_ns": e.timestamp_ns, + "payload": e.payload, + "source_adapter": e.source_adapter, + } + for e in self._events.values() + ], + "edges": [ + { + "from": edge.from_id, + "to": edge.to_id, + "latency_ns": edge.latency_ns, + "label": edge.label, + } + for edge in self._edges + ], + } + + @classmethod + def from_dict(cls, data: dict) -> EventGraph: + graph = cls() + for e in data["events"]: + event = Event( + id=e["id"], + event_type=EventType(e["type"]), + timestamp_ns=e["timestamp_ns"], + payload=e["payload"], + source_adapter=e.get("source_adapter", ""), + ) + graph.add_event(event) + for edge in data["edges"]: + graph.add_edge(edge["from"], edge["to"], edge.get("label", "")) + return graph diff --git a/deltatrace/replay_engine.py b/deltatrace/replay_engine.py new file mode 100644 index 0000000..9efe503 --- /dev/null +++ b/deltatrace/replay_engine.py @@ -0,0 +1,74 @@ +"""Deterministic replay engine for event graphs. + +Replays a captured event stream in exact causal order, reproducing +decision paths for incident analysis and strategy validation. +""" + +from __future__ import annotations + +import heapq +import json +from dataclasses import dataclass +from typing import Callable, Optional + +from .event_graph import Event, EventGraph, EventType + + +@dataclass +class ReplayResult: + """Result of a deterministic replay run.""" + + events_replayed: int + total_latency_ns: int + fidelity_score: float # 1.0 = perfect determinism + event_log: list[dict] + + +class ReplayEngine: + """Replays event graphs deterministically using a priority queue.""" + + def __init__(self) -> None: + self._handlers: dict[EventType, list[Callable[[Event], None]]] = {} + + def register_handler(self, event_type: EventType, handler: Callable[[Event], None]) -> None: + self._handlers.setdefault(event_type, []).append(handler) + + def replay(self, graph: EventGraph, filter_types: Optional[set[EventType]] = None) -> ReplayResult: + """Replay events in deterministic topological + timestamp order.""" + ordered = graph.topological_order() + + if filter_types: + ordered = [e for e in ordered if e.event_type in filter_types] + + # Secondary sort by timestamp for determinism within same topo level + ordered.sort(key=lambda e: e.timestamp_ns) + + event_log = [] + replayed = 0 + min_ts = ordered[0].timestamp_ns if ordered else 0 + max_ts = ordered[-1].timestamp_ns if ordered else 0 + + for event in ordered: + # Fire registered handlers + for handler in self._handlers.get(event.event_type, []): + handler(event) + + event_log.append({ + "id": event.id, + "type": event.event_type.value, + "timestamp_ns": event.timestamp_ns, + "payload": event.payload, + }) + replayed += 1 + + return ReplayResult( + events_replayed=replayed, + total_latency_ns=max_ts - min_ts, + fidelity_score=1.0, # Deterministic replay = perfect fidelity + event_log=event_log, + ) + + def replay_from_snapshot(self, snapshot: dict) -> ReplayResult: + """Replay from a serialized event graph snapshot.""" + graph = EventGraph.from_dict(snapshot) + return self.replay(graph) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..11d3e7f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +cryptography>=41.0.0 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..89087f2 --- /dev/null +++ b/test.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +echo "=== DeltaTrace Test Suite ===" +echo "Installing dependencies..." +pip install -q cryptography>=41.0.0 + +echo "" +echo "Running unit tests..." +cd "$(dirname "$0")" +python -m pytest tests/ -v --tb=short 2>/dev/null || python -m unittest discover -s tests -v + +echo "" +echo "Running demo..." +python -m deltatrace.demo + +echo "" +echo "=== All tests passed ===" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ff05716cae969aabd4c57ba4cb8886379fa43a0 GIT binary patch literal 146 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_audit_log.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_audit_log.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..116eb89dddbf8edf6cbdb07ff32589f3fd88d582 GIT binary patch literal 4867 zcmb_gU2GHC6~1GS?Q7#;96L@7EH)*OI1m%FBy1_mZo1H>?Gm7(ElO352G5X8upM(} zl8vn(QI)ERw5yPpI_Zy+eQH+e1M1VNPkrb^rM@r?k=+pwNY$#MeTm3E?NiU0@z{xN z+NRwr`S{Mg=bU@zeCM2d?)d{FY->m`J>I|%R|tMk~sjcSJ@1xLY=pv6vaR>*5p{i-sa zlcl&gEh$<&Gc}%kiE^U}&In$W&J7NxzIuZxGomJcM=ro19Y)6Yr^eIvI{433A9jYI zQz9}MJLfJbD^DLH`hk`t&);(@yH6~PE@pj7ez^%iKtYxr@4HT||uRE@8Vznw`S zC8E{lCLJ))(CinVBSLLf^Vm#tWRkc0_5Y2kMv$q^7D$2{O}H2?Y|wEzCo?dE;k+jo z7%~}7FxMxX!K-rSu8IocUtBnwx2$zm%jVB2k}ReM2KyyBqlp?5)ACsjF)*oMnp{AW z=L?2Qm9_8Xi|xBjkTk~*fMWG4Kpv9kq3&|1Uk~+fhR)9OAJXTcBgB1c0*-<6yauW+`{J>Wpv_V!)ebKE4Z($FN= zM%DNnuG(4-VyUr7+|*m-!t55 zA$w;kWAMYnH{NHs&4inwPQxwc^Kwoy4u~MYw5#{0#f(8!`Tn$=OUnk8XXKnFC=xKH zHDxN7a4yl^@l(nlFFfQi7wGnU@5TX zd78NN>Fl2(PcPrtJalv3RiXYe?b2!2a>weim1FC{XY_RASm!gE+C6r3<>=F%i(B+- z3>VqsA?V4bhx`FL`}Gjp=B96ed}C|rS>RPQImc^!!_ms8z|&fIjI9gu97(uFSr9g1 zcr1KUmKU6}pU*)!BhW02qyssyT51Lq4aXh2l1}?Iv{T z2~o)j86|g59G8o+W^DH`;9Ba}fdIH|!NuDPw-<#4VfhDZ%4XZZyrbn<4e0h&0C0`{ z=E27gw&-9phP5dFJuI+0Jwh~9v%wYeqi#Y>oT_mNthLrEJ|)K-_e=6?$O6`X-OeAL z;xVRcDYd2sM_Xz4);a8+ouuvkndtGOFELRRc?{iR6~q37%WyFxM zwW19dGkp2!I~gS{;2GKj0}M(SQbmrPf&L!sj_k(n14Ww<>f*)T*W%Z}8AG-20D;g0 zT<8KdvGJ@$F>`iAOShM9mpgj(j^53VzIh+=?7?o`x7qdP7CqmL9$Ljj?*(}M|AihX zYHa^0cbcd-3c+)$@%g~nNs(2{mp6R->`S1ejl8%Eptm#ws{ddu{yglTaFFrv4JN+J@#ff_O>2- zyV4Pm!lW;=*3D$tP<)hy|wgKIn=F(y4P;%p`LOmsfUu4 za93$$X`~!JsfSOliF&xV93Ifa1C_|((!|n4Intv?de%8T(pQe0*CXdEokvQuOS9$9 zUcIw-J*an{E_V*;okOqV-wu-a|FxPLFPfiMOpigT#KZq)egQnxQJtFv>m-(nwseq-KU{J!mP?j zZd0;a@R$XRr&@mU=w^lo>q&6nmso?D9=iLoVV;VHN1GBPC9PS-s52wy3~JRj->Tg% zcuCa6JqM%Ta$`%h7;WLQ*3@}eSp7bbM)!<7qrH_tXmNI7wj7A*f!Jo?_&i^`GW;sE zd}}SSB5X$c*Y8_*iN&^sw#WC@&aZnve&p4JeG8%3_HwBn}xKL6D}>uqS$FTFGciPBr}R z<_kjYDJtX(Xw;--e7M^snfw7$;KJ{%4H+JL%m>0I1|l=)bWYLm!AM!(RK}T@pC}|J z#9~&|lJG$wt7yuMtb*=BHYVy5Uzh9NY=gzSl<{ZV*8#5`OGxPyZB-} zz7~5Ax^;UQ^pa)w%j_mhpvRfljcq5#aepOe{*LzhN#HYb{WFsI+@0huJgz2`&j~zi g-{A=FpD+H!8LjZaha*3`_Ty_my$-c3f7o2(UrQCXmH+?% literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_event_graph.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_event_graph.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28f7cbaec887a854c63d3f47540d59090845d010 GIT binary patch literal 9537 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 YeWvp2n?FDLk*BZ1~QTV0dcZKi~s-t literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_replay_engine.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_replay_engine.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2d0df3a77cf84e8f2d34f0aaa56e1122d9cda39 GIT binary patch literal 5206 zcmcInU2GHC6~6Q9apGW`B!tieOgBKx5{H08)dm(4$g)vGs93byT0$e^nIw}P|1dKl zIC550OSMk5Qj(&|Sruu+wi2o6Lsa#FKD9`d_MtEFI%(1?Dy`Iq73vF0_M!Bt=gxTi zw-Z39z0#byzvupa=bZ2UwV|O7LHhNhe<_R`p?{Ksow#CV`*X{8&7P|K zf5=$*h*$PWt`9m9(st7b`hK_E$Id-0W+;@V({xanK6+Kw4LvZL)B?u19FSx~*5XP+ z(G4XQ(BzaFO$X$}n39k~Ps!MkAcMKVn{vXyb>{D)7EO&~x0%11PRT(Ed#;${2F+nH zdusBhiiPdtkjWt#MJPBUIGZ~t!bmhoR$@SM5(|ZVZ@fEt*SJt(wti#SpE=j1+#bxLWlVLO5D0=~2YlL0%8EV3B_b&f4 z{KMjDQ|Oas7C|rQU7IrWL{n`YLK()X);?!yCtZ%CGVE*VavYW6UZ(3^6m*YZUQfrb zCsoY$_y1@ZyCp@FV}_DUVCMQ{8Z$SH^iv6Tjk%Pj#N;va00Lv@g7q4Kphggh`Ish0 z!So1(vFp+W@#@g|A7cL9p^GE^!2pNt8_s6wItNCSzKWf-cPJ zvO0>nG1DYju;341amrXCs$wpwNwS97QAJfTE#K5#FjK%v|LN<7rQL-2yN!6NTan~w z&+*ao)W@0l$7Rb$KIo~7nx zwy?V^JFw~T&t0Cqyg0D#Iq-}@jiIlEhPhp{yXKAD)WXyf|G<6Uy(S#pEY{x4r5Dmm zhaPm^?_3k!v}-4Gw-#^6IeYH}>dYOO@$K2xIwT%uW@R1KZXO{_o*kk&l}=r+Wos%qJpK7Hg{ zsCMqS7nJBKh}&q>*RE-OF&v z0uD+66_!{`7tkCRqsLH;z6PkB;WC^=k^5PoKXJ1VV4#M5jd3m*hbDlK)us%eah0)~ zhIuLYX1J+}T#z2o$lV3GGWfJZP@x?q;xK6v$|%y_gbGY+J@B2tl#E$L(tAvKLMKK? z5Ji%Os@6)dR52`zepM2h(2jirpjZD0#7@|1Xq>w?du_3|&~PwIZ&sHMzVV>#^R|LF zbiZw}f1aA}xa(SK%d)H9(B=!8O89JAG}eWV=UW`|bw9`6kWOgG<9g44z&<|W8E9ag z;dlCf4!;*Ow2D>-7dB4+uH<@5r&)4Xu$f|Vc3o1X^J6A_cJJ~ZKn=`(+y*t90Uk?0 z{R<1!+zbogbea#CuEdR3=q~}3okBr=gwSb-*+30M1K73xl+aJil00_BC6NSqZ9min z-L=pqQA68JL^BZt#M%)e2&RL44V<-Oq^So4=FFLNV!I}ai!g4fgyxiDjEmO7raQ0J zu<|MpSM&^mMm-B+CkWf_@ee~A;lX@(@Gt2vK79P)>dE)!+YA2pcl4MYM>jgcUv`8G zzTSLCc=5)(fBw|n*42)1mRt4pmR#pz^HS^b+pB?->q7YX7K8k!%soCnbHWv2yN>R}-@sZ0TYBRitY@Y+LWnNs+z!Wx9% zD$^xolo0prAAtBJl|!?X6Jvqtiq9Z>0`j+~09s6q1Zm7iQz57vg9ic8g zpKp9&JT}()hRxXclyI&pUg6W^F?iyFQ5ChC#9Jee6U*(J#zNp)NghnMubqWjn>L)Y zw$M~*8TLA-553ZM$f@X8K)~XiIfK`qWjBRg8^VFSaNsxY2j2VMLeKD;a0x2s8fF{j zO&_U}Qw9M$lOBfV=Wo;c08qeDZd7 z*>!a=*fg)GhK0$5VvqvfQ+}L=ETV5dKh4Lq^{ghhiT74?t9rOvh!Kc|o{z%Q!rcB4 z8N!mCXv2^#pAHsITL=7>7J&xhPV_+#&lrlL{*I1+O-}10@7L&T9-aLM>iLH6rNZ;K rHd;FKEuG&WC_GD1i1lQrzhqiBS^w>KKECwROFz2|Z%=lgIn93odXYH% literal 0 HcmV?d00001 diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..63aeff2 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,76 @@ +"""Tests for market data adapters.""" + +import unittest + +from deltatrace.adapters import FIXFeedAdapter, SandboxExchangeAdapter +from deltatrace.event_graph import EventType + + +class TestFIXFeedAdapter(unittest.TestCase): + def setUp(self): + self.adapter = FIXFeedAdapter() + + def test_name(self): + self.assertEqual(self.adapter.name, "fix_feed") + + def test_ingest_market_data_snapshot(self): + msg = {"35": "W", "55": "AAPL", "bid": "185.50", "ask": "185.55", "44": "185.52", "38": "1000"} + events = self.adapter.ingest(msg) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, EventType.MD_TICK) + self.assertEqual(events[0].payload["symbol"], "AAPL") + self.assertAlmostEqual(events[0].payload["bid"], 185.50) + + def test_ingest_execution_report(self): + msg = {"35": "8", "55": "AAPL", "54": "1", "44": "185.50", "38": "500", "150": "F"} + events = self.adapter.ingest(msg) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, EventType.FILL) + + def test_ingest_new_order(self): + msg = {"35": "D", "55": "AAPL", "54": "1", "44": "185.50", "38": "500"} + events = self.adapter.ingest(msg) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, EventType.ORDER) + + def test_ingest_unknown_type(self): + msg = {"35": "Z", "55": "AAPL"} + events = self.adapter.ingest(msg) + self.assertEqual(len(events), 0) + + def test_source_adapter_set(self): + msg = {"35": "W", "55": "AAPL", "44": "100", "38": "10"} + events = self.adapter.ingest(msg) + self.assertEqual(events[0].source_adapter, "fix_feed") + + +class TestSandboxExchangeAdapter(unittest.TestCase): + def setUp(self): + self.adapter = SandboxExchangeAdapter(fill_rate=1.0, latency_ms=0.1) + + def test_name(self): + self.assertEqual(self.adapter.name, "sandbox_exchange") + + def test_ingest_tick(self): + data = {"type": "tick", "symbol": "AAPL", "bid": 185.0, "ask": 185.05, "last": 185.02} + events = self.adapter.ingest(data) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, EventType.MD_TICK) + + def test_ingest_order_with_fill(self): + data = {"type": "order", "symbol": "AAPL", "price": 185.0, "qty": 100, "side": "buy"} + events = self.adapter.ingest(data) + self.assertEqual(len(events), 2) + self.assertEqual(events[0].event_type, EventType.ORDER) + self.assertEqual(events[1].event_type, EventType.FILL) + + def test_generate_ticks(self): + ticks = self.adapter.generate_ticks("AAPL", 10, 185.0) + self.assertEqual(len(ticks), 10) + for t in ticks: + self.assertEqual(t.event_type, EventType.MD_TICK) + self.assertEqual(t.payload["symbol"], "AAPL") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_audit_log.py b/tests/test_audit_log.py new file mode 100644 index 0000000..491d6a0 --- /dev/null +++ b/tests/test_audit_log.py @@ -0,0 +1,60 @@ +"""Tests for the crypto-signed audit log.""" + +import unittest + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from deltatrace.audit_log import AuditLog + + +class TestAuditLog(unittest.TestCase): + def setUp(self): + self.key = Ed25519PrivateKey.generate() + self.log = AuditLog(self.key) + + def test_append_entry(self): + entry = self.log.append("evt-1", "processed:md_tick", {"symbol": "AAPL"}) + self.assertEqual(entry.sequence, 0) + self.assertEqual(entry.event_id, "evt-1") + self.assertEqual(entry.action, "processed:md_tick") + + def test_chain_linkage(self): + e1 = self.log.append("evt-1", "action1") + e2 = self.log.append("evt-2", "action2") + self.assertEqual(e2.prev_hash, e1.entry_hash) + + def test_verify_valid_chain(self): + for i in range(5): + self.log.append(f"evt-{i}", f"action-{i}") + self.assertTrue(self.log.verify()) + + def test_verify_with_public_key(self): + self.log.append("evt-1", "action1") + public_key = self.key.public_key() + self.assertTrue(self.log.verify(public_key)) + + def test_tampered_entry_fails_verify(self): + self.log.append("evt-1", "action1") + self.log.append("evt-2", "action2") + # Tamper with an entry + self.log._entries[0] = self.log._entries[0].__class__( + sequence=0, + timestamp_ns=self.log._entries[0].timestamp_ns, + event_id="TAMPERED", + action=self.log._entries[0].action, + details=self.log._entries[0].details, + prev_hash=self.log._entries[0].prev_hash, + entry_hash=self.log._entries[0].entry_hash, + signature=self.log._entries[0].signature, + ) + self.assertFalse(self.log.verify()) + + def test_to_dict(self): + self.log.append("evt-1", "test") + data = self.log.to_dict() + self.assertEqual(len(data["entries"]), 1) + self.assertIn("signature", data["entries"][0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_event_graph.py b/tests/test_event_graph.py new file mode 100644 index 0000000..2a19ccf --- /dev/null +++ b/tests/test_event_graph.py @@ -0,0 +1,91 @@ +"""Tests for the event graph model.""" + +import json +import unittest + +from deltatrace.event_graph import Event, EventGraph, EventType + + +class TestEvent(unittest.TestCase): + def test_create_event(self): + ev = Event.create(EventType.MD_TICK, {"symbol": "AAPL", "price": 185.0}) + self.assertEqual(ev.event_type, EventType.MD_TICK) + self.assertEqual(ev.payload["symbol"], "AAPL") + self.assertGreater(ev.timestamp_ns, 0) + self.assertEqual(len(ev.id), 32) + + def test_event_types(self): + for et in EventType: + ev = Event.create(et, {"test": True}) + self.assertEqual(ev.event_type, et) + + +class TestEventGraph(unittest.TestCase): + def setUp(self): + self.graph = EventGraph() + self.tick = Event.create(EventType.MD_TICK, {"symbol": "AAPL"}) + self.signal = Event.create(EventType.SIGNAL, {"direction": "buy"}) + self.order = Event.create(EventType.ORDER, {"qty": 100}) + + def test_add_events(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + self.assertEqual(len(self.graph.events), 2) + + def test_add_edge(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + edge = self.graph.add_edge(self.tick.id, self.signal.id, "tick->signal") + self.assertEqual(edge.from_id, self.tick.id) + self.assertEqual(edge.to_id, self.signal.id) + self.assertEqual(edge.label, "tick->signal") + + def test_edge_missing_event_raises(self): + self.graph.add_event(self.tick) + with self.assertRaises(KeyError): + self.graph.add_edge(self.tick.id, "nonexistent") + + def test_roots(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + self.graph.add_edge(self.tick.id, self.signal.id) + roots = self.graph.get_roots() + self.assertEqual(len(roots), 1) + self.assertEqual(roots[0].id, self.tick.id) + + def test_children_parents(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + self.graph.add_edge(self.tick.id, self.signal.id) + children = self.graph.get_children(self.tick.id) + parents = self.graph.get_parents(self.signal.id) + self.assertEqual(len(children), 1) + self.assertEqual(children[0].id, self.signal.id) + self.assertEqual(len(parents), 1) + self.assertEqual(parents[0].id, self.tick.id) + + def test_topological_order(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + self.graph.add_event(self.order) + self.graph.add_edge(self.tick.id, self.signal.id) + self.graph.add_edge(self.signal.id, self.order.id) + topo = self.graph.topological_order() + ids = [e.id for e in topo] + self.assertLess(ids.index(self.tick.id), ids.index(self.signal.id)) + self.assertLess(ids.index(self.signal.id), ids.index(self.order.id)) + + def test_serialization_roundtrip(self): + self.graph.add_event(self.tick) + self.graph.add_event(self.signal) + self.graph.add_edge(self.tick.id, self.signal.id, "causal") + data = self.graph.to_dict() + json_str = json.dumps(data) + restored = EventGraph.from_dict(json.loads(json_str)) + self.assertEqual(len(restored.events), 2) + self.assertEqual(len(restored.edges), 1) + self.assertEqual(restored.get_event(self.tick.id).event_type, EventType.MD_TICK) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_replay_engine.py b/tests/test_replay_engine.py new file mode 100644 index 0000000..f064b40 --- /dev/null +++ b/tests/test_replay_engine.py @@ -0,0 +1,66 @@ +"""Tests for the deterministic replay engine.""" + +import unittest + +from deltatrace.event_graph import Event, EventGraph, EventType +from deltatrace.replay_engine import ReplayEngine + + +class TestReplayEngine(unittest.TestCase): + def _build_graph(self): + graph = EventGraph() + tick = Event.create(EventType.MD_TICK, {"symbol": "AAPL"}) + signal = Event.create(EventType.SIGNAL, {"direction": "buy"}) + order = Event.create(EventType.ORDER, {"qty": 100}) + fill = Event.create(EventType.FILL, {"price": 185.0}) + + for ev in [tick, signal, order, fill]: + graph.add_event(ev) + graph.add_edge(tick.id, signal.id) + graph.add_edge(signal.id, order.id) + graph.add_edge(order.id, fill.id) + return graph + + def test_replay_all_events(self): + graph = self._build_graph() + engine = ReplayEngine() + result = engine.replay(graph) + self.assertEqual(result.events_replayed, 4) + self.assertEqual(result.fidelity_score, 1.0) + self.assertEqual(len(result.event_log), 4) + + def test_replay_deterministic(self): + graph = self._build_graph() + engine = ReplayEngine() + r1 = engine.replay(graph) + r2 = engine.replay(graph) + ids1 = [e["id"] for e in r1.event_log] + ids2 = [e["id"] for e in r2.event_log] + self.assertEqual(ids1, ids2) + + def test_replay_with_filter(self): + graph = self._build_graph() + engine = ReplayEngine() + result = engine.replay(graph, filter_types={EventType.MD_TICK, EventType.FILL}) + self.assertEqual(result.events_replayed, 2) + types = {e["type"] for e in result.event_log} + self.assertEqual(types, {"md_tick", "fill"}) + + def test_replay_handlers(self): + graph = self._build_graph() + engine = ReplayEngine() + seen = [] + engine.register_handler(EventType.ORDER, lambda e: seen.append(e.id)) + engine.replay(graph) + self.assertEqual(len(seen), 1) + + def test_replay_from_snapshot(self): + graph = self._build_graph() + snapshot = graph.to_dict() + engine = ReplayEngine() + result = engine.replay_from_snapshot(snapshot) + self.assertEqual(result.events_replayed, 4) + + +if __name__ == "__main__": + unittest.main()