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
This commit is contained in:
agent-tmlr7wo3s0 2026-04-24 20:44:00 +02:00
parent 4f4bb65d3d
commit bba46497cc
26 changed files with 942 additions and 2 deletions

0
=41.0.0 Normal file
View File

View File

@ -1,3 +1,29 @@
# idea105-deltatrace
# DeltaTrace
DeltaTrace: Deterministic Replayable Latency & Compliance Tracing for Live Market-Execution Pipelines
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

3
deltatrace/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""DeltaTrace: Deterministic Replayable Latency & Compliance Tracing."""
__version__ = "0.1.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

169
deltatrace/adapters.py Normal file
View File

@ -0,0 +1,169 @@
"""Vendor-agnostic adapters for market data feeds and exchange gateways.
Provides a pluggable adapter interface with two starter implementations:
- FIXFeedAdapter: Parses FIX-protocol market data messages
- SandboxExchangeAdapter: Simulates an exchange gateway for testing
"""
from __future__ import annotations
import abc
import random
import time
from typing import Optional
from .event_graph import Event, EventGraph, EventType
class BaseAdapter(abc.ABC):
"""Base adapter interface for market data sources."""
@property
@abc.abstractmethod
def name(self) -> 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

129
deltatrace/audit_log.py Normal file
View File

@ -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]}

71
deltatrace/demo.py Normal file
View File

@ -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()

156
deltatrace/event_graph.py Normal file
View File

@ -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

View File

@ -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)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
cryptography>=41.0.0

18
test.sh Executable file
View File

@ -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 ==="

0
tests/__init__.py Normal file
View File

Binary file not shown.

76
tests/test_adapters.py Normal file
View File

@ -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()

60
tests/test_audit_log.py Normal file
View File

@ -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()

91
tests/test_event_graph.py Normal file
View File

@ -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()

View File

@ -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()