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..a64990d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md + +Architecture and contribution rules for ExoRoute MVP (Deterministic Replay). + +Overview +- ExoRoute is a cross-venue order routing orchestrator prototype with deterministic replay capabilities. +- The design emphasizes privacy by design, governance provenance, and modular adapters. + +Tech Stack (current): +- Language: Python 3.9+ (production-ready scaffolding) +- Core concepts: LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, GraphOfContractsRegistry +- Adapters: exoroute.adapters.* (starter adapters: FIX/WebSocket feed, simulated venue) +- Canonical bridge: exoroute.energi_bridge ( EnergiBridge-style interoperability layer) +- Planning: exoroute.planner (naive ADMM-lite placeholder for MVP) +- Delta storage: exoroute.delta_store (filesystem-based log of deltas) +- API: exoroute.api (FastAPI endpoints) and exoroute.server (runner) + +Contribution Rules +- Follow the coding style in current repo (Python with type hints, dataclasses). +- Add unit tests where feasible; ensure test.sh passes. +- Update AGENTS.md if architecture or testing requirements change. + +Testing and Local Run +- To run API locally: python -m exoroute.server # via entrypoint or uvicorn +- Use test.sh (in repo root) to validate packaging and basic checks (to be added in patch). + +Decision Log +- This document should be kept up to date with architectural shifts and interface changes. diff --git a/README.md b/README.md index ae40dcb..f5454b5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # exoroute-cross-venue-order-routing-orche -A vendor-agnostic, cross-venue order routing and hedging orchestrator for equities and derivatives that minimizes data exposure while maximizing execution quality. ExoRoute ingests venue feeds (FIX/WebSocket/REST), latency proxies, fees, spread, and \ No newline at end of file +A vendor-agnostic, cross-venue order routing and hedging orchestrator for equities and derivatives that minimizes data exposure while maximizing execution quality. ExoRoute ingests venue feeds (FIX/WebSocket/REST), latency proxies, fees, spread, and diff --git a/exoroute/__init__.py b/exoroute/__init__.py new file mode 100644 index 0000000..612881f --- /dev/null +++ b/exoroute/__init__.py @@ -0,0 +1,9 @@ +from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog +__all__ = [ + "LocalProblem", + "SharedVariables", + "PlanDelta", + "DualVariables", + "PrivacyBudget", + "AuditLog", +] diff --git a/exoroute/adapters/__init__.py b/exoroute/adapters/__init__.py new file mode 100644 index 0000000..b77dccd --- /dev/null +++ b/exoroute/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapters package for ExoRoute: starter adapters for price feeds and simulated venues.""" diff --git a/exoroute/adapters/fix_ws_feed.py b/exoroute/adapters/fix_ws_feed.py new file mode 100644 index 0000000..4c1abf7 --- /dev/null +++ b/exoroute/adapters/fix_ws_feed.py @@ -0,0 +1,25 @@ +from __future__ import annotations +import time +from typing import Dict, Any + +class FIXWSFeedAdapter: + """Minimal FIX/WebSocket feed adapter scaffold. + Produces deterministic synthetic price signals for testing. + """ + def __init__(self, endpoint: str = "wss://dummy-feed.example", symbol: str = "ABC"): + self.endpoint = endpoint + self.symbol = symbol + self._start = time.time() + + def get_latest_signal(self) -> Dict[str, Any]: + t = time.time() - self._start + # Deterministic pseudo-price signal based on time + base = 100.0 + price = base + (t * 0.5) + ((hash(self.symbol) & 0xFFFF) * 0.0001) + signal = { + "source": "FIXWS", + "symbol": self.symbol, + "price": round(price, 4), + "ts": time.time(), + } + return signal diff --git a/exoroute/adapters/simulated_venue.py b/exoroute/adapters/simulated_venue.py new file mode 100644 index 0000000..0f7f2ff --- /dev/null +++ b/exoroute/adapters/simulated_venue.py @@ -0,0 +1,18 @@ +from __future__ import annotations +import time +from typing import Dict, Any + +class SimulatedVenueAdapter: + """A simple simulated venue adapter that emits synthetic data.""" + def __init__(self, venue_id: str = "venue-1"): + self.venue_id = venue_id + self._start = time.time() + + def get_latest_signal(self) -> Dict[str, Any]: + t = time.time() - self._start + price = 50.0 + (t * 0.3) + ((hash(self.venue_id) & 0xFFFF) * 0.00007) + return { + "venue": self.venue_id, + "price": round(price, 4), + "ts": time.time(), + } diff --git a/exoroute/api.py b/exoroute/api.py new file mode 100644 index 0000000..49a2e55 --- /dev/null +++ b/exoroute/api.py @@ -0,0 +1,64 @@ +from __future__ import annotations +import time +from typing import Dict, Any + +from fastapi import FastAPI +from pydantic import BaseModel + +from .delta_store import DeltaStore +from .core import LocalProblem, SharedVariables, PlanDelta +from .planner import naive_admm_lite_plan +from .energi_bridge import EnergiBridge + +app = FastAPI(title="ExoRoute MVP API") + +delta_store = DeltaStore() + + +class LocalProblemInput(BaseModel): + id: str + domain: str + assets: list[str] + objective: dict[str, object] + constraints: dict[str, object] + + +class SignalPayload(BaseModel): + venue: str + price: float + ts: float + + +@app.get("/") +def read_root(): + return {"status": "ok", "time": time.time()} + + +@app.post("/signal") +def submit_signal(payload: SignalPayload): + # For MVP, store signals in delta_store as part of a delta + delta = PlanDelta(delta={"venue": payload.venue, "price": payload.price, "ts": payload.ts}) + delta_store.append_delta(delta) + return {"stored": True} + + +@app.post("/local-problem") +def create_local_problem(lp: LocalProblemInput): + lp_obj = LocalProblem( + id=lp.id, + domain=lp.domain, + assets=lp.assets, + objective=lp.objective, + constraints=lp.constraints, + ) + # Create a minimal shared variable placeholder + shared = SharedVariables(forecasts={}, priors={}, version=1, timestamp=time.time()) + delta = naive_admm_lite_plan({lp.id: {"price": 0.0}}, lp_obj, shared) + delta_store.append_delta(delta) + return delta.delta + + +@app.get("/plan") +def get_latest_plan(): + deltas = delta_store.read_deltas() + return {"deltas": deltas[-5:]} # last few for visibility diff --git a/exoroute/core.py b/exoroute/core.py new file mode 100644 index 0000000..609a753 --- /dev/null +++ b/exoroute/core.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Any +import time + + +@dataclass +class LocalProblem: + id: str + domain: str + assets: List[str] + objective: Dict[str, Any] + constraints: Dict[str, Any] + solver_hint: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SharedVariables: + forecasts: Dict[str, Any] # per-venue forecasts + priors: Dict[str, Any] # prior solutions or priors + version: int = 0 + timestamp: float = field(default_factory=lambda: time.time()) + + +@dataclass +class PlanDelta: + delta: Dict[str, Any] + timestamp: float = field(default_factory=lambda: time.time()) + author: str = "system" + contract_id: str = "default" + signature: str | None = None + + +@dataclass +class DualVariables: + multipliers: Dict[str, Any] # Lagrange multipliers or dual vars + + +@dataclass +class PrivacyBudget: + signal: str + budget: float + expiry: float + + +@dataclass +class AuditLog: + entry: str + signer: str + timestamp: float + contract_id: str + version: int + + +@dataclass +class GraphOfContractsRegistryEntry: + adapter_id: str + supported_domains: List[str] + contract_version: str + + +@dataclass +class GraphOfContractsRegistry: + entries: Dict[str, GraphOfContractsRegistryEntry] = field(default_factory=dict) diff --git a/exoroute/delta_store.py b/exoroute/delta_store.py new file mode 100644 index 0000000..8057a1b --- /dev/null +++ b/exoroute/delta_store.py @@ -0,0 +1,45 @@ +import os +import json +from pathlib import Path +from typing import List, Dict, Any +from datetime import datetime + +from .core import PlanDelta + + +class DeltaStore: + def __init__(self, path: str = ".exoroute_deltas"): + self.path = Path(path) + self.path.mkdir(parents=True, exist_ok=True) + self.log_file = self.path / "deltas.log" + if not self.log_file.exists(): + self.log_file.write_text("") + + def append_delta(self, delta: PlanDelta) -> None: + entry = { + "delta": delta.delta, + "timestamp": delta.timestamp, + "author": delta.author, + "contract_id": delta.contract_id, + "signature": delta.signature, + } + with self.log_file.open("a", encoding="utf-8") as f: + f.write(json.dumps(entry, sort_keys=True) + "\n") + + def read_deltas(self) -> List[Dict[str, Any]]: + if not self.log_file.exists(): + return [] + deltas = [] + with self.log_file.open("r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + deltas.append(json.loads(line)) + except json.JSONDecodeError: + continue + return deltas + + def clear(self) -> None: + self.log_file.write_text("") diff --git a/exoroute/energi_bridge.py b/exoroute/energi_bridge.py new file mode 100644 index 0000000..e1aa046 --- /dev/null +++ b/exoroute/energi_bridge.py @@ -0,0 +1,33 @@ +from __future__ import annotations +from typing import Dict, Any, Optional +from .core import LocalProblem, SharedVariables, PlanDelta + +class EnergiBridge: + """Minimal bridge translating ExoRoute primitives to a canonical IR (EnergiBridge-style). + This is a lightweight, extensible mapping scaffold for interoperability with CatOpt-like ecosystems. + """ + @staticmethod + def to_canonical(local: LocalProblem, shared: SharedVariables, delta: Optional[PlanDelta] = None) -> Dict[str, Any]: + can = { + "Objects": { + "LocalProblem": { + "id": local.id, + "domain": local.domain, + "assets": local.assets, + "objective": local.objective, + "constraints": local.constraints, + "solver_hint": local.solver_hint, + } + }, + "Morphisms": { + "SharedVariables": { + "forecasts": getattr(shared, "forecasts", {}), + "priors": getattr(shared, "priors", {}), + "version": getattr(shared, "version", 0), + "timestamp": getattr(shared, "timestamp", 0.0), + }, + "DualVariables": {}, + }, + "PlanDelta": delta.delta if delta else {}, + } + return can diff --git a/exoroute/planner.py b/exoroute/planner.py new file mode 100644 index 0000000..7dd331a --- /dev/null +++ b/exoroute/planner.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from typing import Dict, Any +from .core import LocalProblem, SharedVariables, PlanDelta + + +def naive_admm_lite_plan(per_venue_signals: Dict[str, Any], local: LocalProblem, shared: SharedVariables) -> PlanDelta: + """A tiny, deterministic placeholder ADMM-lite planner. + It computes a simple routing/hedging delta based on signals and the local objective. + This is NOT a production-grade optimizer; it's a seed for MVP wiring. + """ + # Build a toy delta that encodes the sum of signals and a rough budget heuristic + total_signal = 0.0 + for v, s in per_venue_signals.items(): + price = s.get("price", 0.0) + total_signal += price + + delta = { + "type": "demo-plan", + "local_problem_id": local.id, + "objective_summary": local.objective, + "total_signal": total_signal, + "venues_considered": list(per_venue_signals.keys()), + "timestamp": __import__('time').time(), + } + return PlanDelta(delta=delta, author="planner", contract_id=local.id) diff --git a/exoroute/server.py b/exoroute/server.py new file mode 100644 index 0000000..edde5d9 --- /dev/null +++ b/exoroute/server.py @@ -0,0 +1,5 @@ +import uvicorn +from .api import app + +def run(host: str = "0.0.0.0", port: int = 8000): + uvicorn.run(app, host=host, port=port) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ffae08 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "exoroute" +version = "0.1.0" +description = "Cross-venue order routing orchestrator prototype with deterministic replay" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +authors = [{name = "OpenCode"}] +dependencies = [ + "fastapi>=0.105.0", + "uvicorn[standard]>=0.22.0", + "pydantic>=1.10,<2", + "cryptography>=38.0.0", + "typing-extensions>=4.0", + "requests>=2.28.0" +] + +[tool.setuptools.packages.find] +where = ["exoroute"] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..f1c7f6b --- /dev/null +++ b/test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[TEST] Building package..." +python -m build --wheel --no-isolation + +echo "[TEST] Installing in editable mode..." +python -m pip install -e . + +echo "[TEST] Quick import sanity..." +python - << 'PY' +import exoroute.api as api +print('API import OK', api.__name__) +PY + +echo "[TEST] All basic tests passed."