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..18aa359 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# EdgeOptionX Agents Guide + +This repository contains a pragmatic MVP scaffold for EdgeOptionX: an edge-first latency-aware options hedging studio. + +What this repo provides: +- A lightweight Python-based core for local hedging problems (HedgeObject, LocalProblem, SharedVariables, PlanDelta, AuditLog). +- A minimal delta-sync ledger with tamper-evident logging for governance/audit trails. +- Starter adapters for broker/data feed integration (two adapters as MVP). +- A tiny DSL parser to express hedging tasks in a friendly syntax. +- A provisional testing/packaging setup to verify production readiness. + +How to use: +- Implement and wire adapters for your venues; the MVP focuses on local problem declaration and a basic solver skeleton. +- Run tests and build using the provided test.sh script (see below). + +Development workflow: +- Write/adjust DSL or core primitives, implement adapters, and ensure the delta-sync ledger can replay confidently. +- Validate with unit tests; extend tests for new hedging objectives. +- Ensure packaging metadata remains consistent (pyproject.toml). + +Testing and packaging commands: +- Run tests: ./test.sh +- Build distribution: python3 -m build +- Check linting and type hints as needed (not enforced in this MVP). + +Contributing: +- Open a PR with a clear description of the feature or bug fix. +- Add/adjust tests to reflect new behavior. +- Update AGENTS.md if workflow or architecture changes are made. + +Note: This is a production-oriented scaffold. As the project evolves, components may be refined or split into more granular packages. diff --git a/README.md b/README.md index d67206f..107bbac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# edgeoptionx-edge-first-latency-aware-opt +# EdgeOptionX Edge-First Latency-Aware Options Hedging Studio -Gap: Centralized trading workstations and cloud-based tools introduce latency, data exposure risk, and vendor lock-in for cross-venue hedging with options. Traders and prop desks lack a portable, privacy-conscious, edge-friendly platform that can com \ No newline at end of file +This repository now includes a production-oriented MVP scaffold for EdgeOptionX, a portable hedging studio designed to run close to data sources with an edge-friendly footprint. The MVP provides core primitives, a delta-sync ledger, starter data adapters, a tiny DSL for hedging tasks, and a testable packaging setup. + +Gap: Centralized trading workstations and cloud-based tools introduce latency, data exposure risk, and vendor lock-in for cross-venue hedging with options. Traders and prop desks lack a portable, privacy-conscious, edge-friendly platform that can com diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5129b7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "edgeoptionx" +version = "0.0.1" +description = "Edge-first latency-aware options hedging studio (MVP scaffold)" +readme = "README.md" +requires-python = ">=3.8" +license = { text = "MIT" } +authors = [ { name = "OpenCode" } ] + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/edgeoptionx/__init__.py b/src/edgeoptionx/__init__.py new file mode 100644 index 0000000..7b4bdfb --- /dev/null +++ b/src/edgeoptionx/__init__.py @@ -0,0 +1,14 @@ +"""EdgeOptionX: edge-first hedging studio (MVP scaffold) +Public API surface is intentionally small for the MVP; core primitives are +exposed from submodules. +""" + +from .core import HedgeObject, LocalProblem, SharedVariables, PlanDelta, AuditLog + +__all__ = [ + "HedgeObject", + "LocalProblem", + "SharedVariables", + "PlanDelta", + "AuditLog", +] diff --git a/src/edgeoptionx/adapters/broker_adapter.py b/src/edgeoptionx/adapters/broker_adapter.py new file mode 100644 index 0000000..20c7566 --- /dev/null +++ b/src/edgeoptionx/adapters/broker_adapter.py @@ -0,0 +1,20 @@ +"""Starter broker adapter (mock). + +Provides a minimal interface to fetch positions and quotes from a broker-like API. +This is intentionally simple for MVP bootstrap and testing. +""" +from __future__ import annotations +from typing import Dict, List + + +class BrokerAdapter: + def __init__(self, name: str = "MockBroker"): # pragma: no cover + self.name = name + + def fetch_positions(self) -> List[Dict[str, float]]: + # Mock positions: list of instrument -> quantity + return [{"instrument": "AAPL", "qty": 10}, {"instrument": "SPY", "qty": 5}] + + def fetch_quotes(self, instruments: List[str]) -> Dict[str, float]: + # Mock quotes: instrument -> price + return {inst: 100.0 for inst in instruments} diff --git a/src/edgeoptionx/adapters/data_feed_adapter.py b/src/edgeoptionx/adapters/data_feed_adapter.py new file mode 100644 index 0000000..c64da0d --- /dev/null +++ b/src/edgeoptionx/adapters/data_feed_adapter.py @@ -0,0 +1,19 @@ +"""Starter data feed adapter (mock). + +Pretend to ingest options and equities data from a data feed. This module +provides a tiny interface compatible with the MVP's core primitives. +""" +from __future__ import annotations +from typing import Dict, List + + +class DataFeedAdapter: + def __init__(self): + pass + + def fetch_option_stream(self) -> List[Dict[str, float]]: + # Mock option stream: delta exposure signals + return [{"instrument": "AAPL_20240120_150C", "greeks": {"delta": 0.5}}] + + def fetch_equity_stream(self) -> List[Dict[str, float]]: + return [{"instrument": "AAPL", "price": 150.0}] diff --git a/src/edgeoptionx/core.py b/src/edgeoptionx/core.py new file mode 100644 index 0000000..b53934d --- /dev/null +++ b/src/edgeoptionx/core.py @@ -0,0 +1,52 @@ +"""Core primitives for EdgeOptionX MVP. + +Defines lightweight data structures for local hedging problems and a delta +log. This is intentionally compact but designed to be extended into a richer +solver layer later. +""" +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class HedgeObject: + name: str + description: str + assets: List[str] = field(default_factory=list) + + +@dataclass +class LocalProblem: + objective: str + assets: List[str] = field(default_factory=list) + budgets: Dict[str, float] = field(default_factory=dict) + constraints: List[str] = field(default_factory=list) + + def add_constraint(self, constraint: str) -> None: + self.constraints.append(constraint) + + +@dataclass +class SharedVariables: + values: Dict[str, Any] = field(default_factory=dict) + + def update(self, key: str, value: Any) -> None: + self.values[key] = value + + +@dataclass +class PlanDelta: + delta_id: str + changes: Dict[str, Any] = field(default_factory=dict) + + def add_change(self, key: str, value: Any) -> None: + self.changes[key] = value + + +@dataclass +class AuditLog: + entries: List[Dict[str, Any]] = field(default_factory=list) + + def append(self, entry: Dict[str, Any]) -> None: + self.entries.append(entry) diff --git a/src/edgeoptionx/dsl/__init__.py b/src/edgeoptionx/dsl/__init__.py new file mode 100644 index 0000000..6c01d8a --- /dev/null +++ b/src/edgeoptionx/dsl/__init__.py @@ -0,0 +1 @@ +"""DSL package for EdgeOptionX MVP.""" diff --git a/src/edgeoptionx/dsl/hedging_dsl.py b/src/edgeoptionx/dsl/hedging_dsl.py new file mode 100644 index 0000000..421e652 --- /dev/null +++ b/src/edgeoptionx/dsl/hedging_dsl.py @@ -0,0 +1,41 @@ +"""Tiny DSL for declaring hedging problems. + +This is intentionally small: supports a couple of phrases to build a LocalProblem +with budgets and constraints. It's designed to be extended, not a full DSL. +""" +from __future__ import annotations +from typing import Dict, List + +from edgeoptionx.core import LocalProblem + + +def parse_hedge_dsl(dsl_text: str) -> LocalProblem: + # Very small parser: looks for lines like + # hedge delta budgets: risk=0.1, margin=0.2 + # assets: AAPL, SPY + # constraint: max_loss <= 1.0 + assets: List[str] = [] + budgets: Dict[str, float] = {} + constraints: List[str] = [] + objective = "default" + + for line in dsl_text.strip().splitlines(): + line = line.strip() + if not line: + continue + if line.lower().startswith("assets:"): + parts = line.split(":", 1)[1].strip() + assets = [p.strip() for p in parts.split(",") if p.strip()] + elif line.lower().startswith("budgets:") or line.lower().startswith("budget:"): + # budgets: key=value pairs separated by comma + part = line.split(":", 1)[1].strip() + for kv in part.split(","): + if "=" in kv: + k, v = kv.split("=", 1) + budgets[k.strip()] = float(v.strip()) + elif line.lower().startswith("constraint:"): + constraints.append(line.split(":", 1)[1].strip()) + elif line.lower().startswith("objective:"): + objective = line.split(":", 1)[1].strip() + + return LocalProblem(objective=objective, assets=assets, budgets=budgets, constraints=constraints) diff --git a/src/edgeoptionx/ledger.py b/src/edgeoptionx/ledger.py new file mode 100644 index 0000000..cd7c47f --- /dev/null +++ b/src/edgeoptionx/ledger.py @@ -0,0 +1,56 @@ +"""Delta-Sync ledger: tamper-evident log with replay capability. + +The ledger is a lightweight, chain-logged store where each entry records the +payload hash and a chain hash of the previous entry to provide auditability and +replay verification in offline mode. +""" +from __future__ import annotations +import hashlib +import json +from dataclasses import dataclass, asdict +from typing import List, Dict, Any + + +@dataclass +class LedgerEntry: + seq: int + payload: Dict[str, Any] + prev_hash: str + hash: str + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +class DeltaSyncLedger: + def __init__(self) -> None: + self.entries: List[LedgerEntry] = [] + self._last_hash = "" # Genesis chain + + @staticmethod + def _hash(payload: Dict[str, Any], prev_hash: str) -> str: + h = hashlib.sha256() + h.update(json.dumps(payload, sort_keys=True).encode("utf-8")) + h.update(prev_hash.encode("utf-8")) + return h.hexdigest() + + def append(self, payload: Dict[str, Any]) -> LedgerEntry: + seq = len(self.entries) + 1 + entry_hash = self._hash(payload, self._last_hash) + entry = LedgerEntry(seq=seq, payload=payload, prev_hash=self._last_hash, hash=entry_hash) + self.entries.append(entry) + self._last_hash = entry_hash + return entry + + def verify(self) -> bool: + prev = "" + for e in self.entries: + if e.prev_hash != prev: + return False + if e.hash != self._hash(e.payload, e.prev_hash): + return False + prev = e.hash + return True + + def replay(self) -> List[Dict[str, Any]]: + return [e.to_dict() for e in self.entries] diff --git a/src/edgeoptionx/transport.py b/src/edgeoptionx/transport.py new file mode 100644 index 0000000..1ad5ec8 --- /dev/null +++ b/src/edgeoptionx/transport.py @@ -0,0 +1,28 @@ +"""Lightweight transport layer for MVP. + +This is a minimal, test-friendly stand-in for a TLS-over-WebSocket transport. +It exposes a small API that adapters can call, without pulling network dependencies +in the MVP. A real transport can be swapped in later without touching core logic. +""" +from __future__ import annotations +from typing import Callable, Optional + + +class Transport: + def __init__(self, on_message: Optional[Callable[[str], None]] = None): + self.on_message = on_message + self.connected = False + + def connect(self) -> None: + # In MVP, just mark as connected. + self.connected = True + + def send(self, message: str) -> None: + if not self.connected: + raise RuntimeError("Transport not connected") + # In MVP, echo back via callback to simulate a loopback channel. + if self.on_message: + self.on_message(message) + + def close(self) -> None: + self.connected = False diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..66a3e96 --- /dev/null +++ b/test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[Test] Setting up Python build environment..." +python3 -m venv venv +source venv/bin/activate +python -m pip install --upgrade pip +python -m pip install --upgrade build wheel setuptools + +echo "[Test] Installing package in editable mode for tests" +pip install -e . +pip install --upgrade pytest +echo "[Test] Running unit tests (unittest discover)" +pytest -q || (echo "Tests failed"; exit 1) + +echo "[Test] Building package with python -m build..." +python3 -m build + +echo "[Test] All tests passed and package built." diff --git a/tests/test_hedge_dsl.py b/tests/test_hedge_dsl.py new file mode 100644 index 0000000..8326b8f --- /dev/null +++ b/tests/test_hedge_dsl.py @@ -0,0 +1,24 @@ +import unittest + +from edgeoptionx.dsl.hedging_dsl import parse_hedge_dsl + + +class TestHedgeDSL(unittest.TestCase): + def test_parse_basic(self): + dsl = """ + assets: AAPL, SPY + budgets: risk=0.2, margin=0.1 + constraint: max_loss <= 1.0 + objective: minimize_risk + """ + lp = parse_hedge_dsl(dsl) + self.assertIsNotNone(lp) + self.assertIn("AAPL", lp.assets) + self.assertIn("SPY", lp.assets) + self.assertIn("risk", lp.budgets) + self.assertIn("margin", lp.budgets) + self.assertEqual(lp.objective, "minimize_risk" or "default") + + +if __name__ == '__main__': + unittest.main()