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..39e944a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# AGENTS + +- This repository contains a lightweight, test-driven Python prototype for ArbSphere's federated cross-arbitrage primitives. +- Core concepts provided: LocalArbProblem, SharedSignals, and a deterministic admm_step that outputs a PlanDelta. +- Tests verify deterministic plan generation and basic structure. +- How to run: + - bash test.sh +- Important: This document is a stub for onboarding agents and does not describe a full production architecture. diff --git a/README.md b/README.md index 1100696..5f4a49f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ -# idea159-arbsphere-federated-cross +# ArbSphere Federated Cross (Toy) -Source logic for Idea #159 \ No newline at end of file +This repository provides a minimal, test-focused Python package that defines +the canonical LocalArbProblem and SharedSignals primitives and a simple ADMM-like +step function used by unit tests. It serves as a starting point for a more +comprehensive federated arbitrage prototype. + +Note: This is a lightweight, educational scaffold designed to satisfy tests and +CI checks in this kata. It is not a production-grade implementation. diff --git a/idea159_arbsphere_federated_cross/__init__.py b/idea159_arbsphere_federated_cross/__init__.py new file mode 100644 index 0000000..6441210 --- /dev/null +++ b/idea159_arbsphere_federated_cross/__init__.py @@ -0,0 +1,8 @@ +"""Idea159 ArbSphere Federated Cross (toy) package. + +This lightweight package provides the core primitives used by the tests and +minimal bridging utilities to enable interoperability with a canonical IR +via EnergiBridge. +""" + +__all__ = ["core", "solver", "energi_bridge"] diff --git a/idea159_arbsphere_federated_cross/adapters/broker_adapter.py b/idea159_arbsphere_federated_cross/adapters/broker_adapter.py new file mode 100644 index 0000000..97a6eda --- /dev/null +++ b/idea159_arbsphere_federated_cross/adapters/broker_adapter.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import List + +from ..core import PlanDelta + + +class MockBrokerAdapter: + def __init__(self): + self.decisions: List[PlanDelta] = [] + + def consume(self, plan: PlanDelta) -> None: + # In a real adapter, this would route orders. Here we record for testability. + self.decisions.append(plan) diff --git a/idea159_arbsphere_federated_cross/adapters/price_feed_adapter.py b/idea159_arbsphere_federated_cross/adapters/price_feed_adapter.py new file mode 100644 index 0000000..b7dca78 --- /dev/null +++ b/idea159_arbsphere_federated_cross/adapters/price_feed_adapter.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime + +from ..core import LocalArbProblem, SharedSignals + + +@dataclass +class MockPriceFeedAdapter: + version: int = 1 + + def emit(self) -> tuple[LocalArbProblem, SharedSignals]: + # Minimal synthetic data for MVP + local = LocalArbProblem( + id="lp-01", + venue="NYSE", + asset_pair="AAPL/GOOG", + target_misprice=0.5, + max_exposure=1000000.0, + latency_budget_ms=50, + ) + signals = SharedSignals( + version=self.version, + price_delta=0.1, + cross_venue_corr=0.8, + liquidity=1000000.0, + ) + return local, signals diff --git a/idea159_arbsphere_federated_cross/core.py b/idea159_arbsphere_federated_cross/core.py new file mode 100644 index 0000000..f08392e --- /dev/null +++ b/idea159_arbsphere_federated_cross/core.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class LocalArbProblem: + id: str + venue: str + asset_pair: str + target_misprice: float + max_exposure: float + latency_budget_ms: int + + +@dataclass +class SharedSignals: + version: int + price_delta: float + cross_venue_corr: float + liquidity: float + + +__all__ = ["LocalArbProblem", "SharedSignals"] diff --git a/idea159_arbsphere_federated_cross/energi_bridge.py b/idea159_arbsphere_federated_cross/energi_bridge.py new file mode 100644 index 0000000..a718db1 --- /dev/null +++ b/idea159_arbsphere_federated_cross/energi_bridge.py @@ -0,0 +1,79 @@ +"""EnergiBridge: Canonical IR translator for ArbSphere primitives. + +This module provides a lightweight, vendor-agnostic translation layer that +maps ArbSphere primitives (LocalArbProblem, SharedSignals, PlanDelta) into a +canonical IR suitable for adapters/bridges to cross-exchange data feeds and +execution venues. The goal is minimal, deterministic, and easy to extend. +""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import Any, Dict, List + +from .core import LocalArbProblem, SharedSignals +from .solver import PlanDelta + + +class EnergiBridge: + """Static translator utilities for ArbSphere primitives to a canonical IR.""" + + @staticmethod + def to_ir(local: LocalArbProblem, signals: SharedSignals, delta: PlanDelta | None = None) -> Dict[str, Any]: + """Serialize a LocalArbProblem and SharedSignals (and optional PlanDelta) to IR. + + The IR schema is intentionally simple and versioned via the top-level keys. + It is designed to be extended by adapters without coupling to internal + Python types. + """ + payload: Dict[str, Any] = { + "IRVersion": 1, + "Object": { + "id": local.id, + "venue": local.venue, + "asset_pair": local.asset_pair, + "target_misprice": local.target_misprice, + "max_exposure": local.max_exposure, + "latency_budget_ms": local.latency_budget_ms, + }, + "SharedSignals": asdict(signals), + } + + if delta is not None: + # Include a lightweight delta snapshot with actions for replay + payload["PlanDelta"] = { + "actions": delta.actions, + "timestamp": delta.timestamp.isoformat(), + } + + return payload + + @staticmethod + def from_ir(ir: Dict[str, Any]) -> Dict[str, Any]: + """Deserialize a canonical IR payload back into a structured dict. + + This helper is intentionally permissive to avoid tight coupling with + Python types in adapters. It is suitable for simple round-trips and + can be extended for full bidirectional mapping. + """ + return ir + + @staticmethod + def merge_deltas(base: PlanDelta, new: PlanDelta) -> PlanDelta: + """Deterministic merge of two PlanDelta objects. + + For this toy MVP, we concatenate actions and keep the latest timestamp. + A real CRDT-like merge would deduplicate and order actions, but this + keeps the implementation small and deterministic for replay. + """ + merged_actions: List[Dict[str, Any]] = [] + if isinstance(base.actions, list): + merged_actions.extend(base.actions) + if isinstance(new.actions, list): + merged_actions.extend(new.actions) + + latest_ts = max(base.timestamp, new.timestamp) + return PlanDelta(actions=merged_actions, timestamp=latest_ts) + + +__all__ = ["EnergiBridge"] diff --git a/idea159_arbsphere_federated_cross/registry.py b/idea159_arbsphere_federated_cross/registry.py new file mode 100644 index 0000000..526b6ba --- /dev/null +++ b/idea159_arbsphere_federated_cross/registry.py @@ -0,0 +1,22 @@ +"""Graph-of-Contracts (GoC) placeholders for adapters and data schemas.""" + +from dataclasses import dataclass +from typing import Dict + + +@dataclass +class ContractMetadata: + name: str + version: str + endpoint: str | None = None + + +class GraphOfContracts: + def __init__(self): + self._registry: Dict[str, ContractMetadata] = {} + + def register(self, key: str, meta: ContractMetadata): + self._registry[key] = meta + + def get(self, key: str) -> ContractMetadata | None: + return self._registry.get(key) diff --git a/idea159_arbsphere_federated_cross/solver.py b/idea159_arbsphere_federated_cross/solver.py new file mode 100644 index 0000000..927b5c0 --- /dev/null +++ b/idea159_arbsphere_federated_cross/solver.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from datetime import datetime +from .core import LocalArbProblem, SharedSignals + + +class PlanDelta: + def __init__(self, actions, timestamp: datetime | None = None): + self.actions = actions + self.timestamp = timestamp or datetime.utcnow() + + +def admm_step(local: LocalArbProblem, signals: SharedSignals) -> PlanDelta: + """Deterministic, minimal ADMM-like step producing a single PlanDelta. + + The plan contains a single action that routes a hedge-like size from the + local venue to a cross-venue placeholder, using available liquidity and + respecting the local exposure cap. + """ + # Deterministic sizing based on available liquidity and max exposure + available = max(0.0, float(signals.liquidity)) + size = min(float(local.max_exposure), available * 0.5) + + action = { + "venue_from": local.venue, + "venue_to": "CROSS-VENUE", + "instrument": local.asset_pair, + "size": size, + "time": datetime.utcnow().isoformat() + "Z", + } + + return PlanDelta(actions=[action]) + + +__all__ = ["PlanDelta", "admm_step"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d36645e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea159-arbsphere-federated-cross" +version = "0.1.0" +description = "Toy primitives for ArbSphere federated cross-arb (tests only)." +readme = "README.md" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c0c5e50 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages + +setup( + name="idea159-arbsphere-federated-cross", + version="0.1.0", + packages=find_packages(), + description="Toy primitives for ArbSphere federated cross-arb (tests only).", +) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..6f0c43b --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run Python tests +pytest -q + +# Build the package (verifies packaging metadata and structure) +python3 -m build diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..23ab4a8 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,29 @@ +import datetime + +import pytest + +from idea159_arbsphere_federated_cross.core import LocalArbProblem, SharedSignals +from idea159_arbsphere_federated_cross.solver import admm_step + + +def test_admm_step_produces_plan_delta_deterministically(): + local = LocalArbProblem( + id="test-1", + venue="NYSE", + asset_pair="AAPL/GOOG", + target_misprice=0.5, + max_exposure=10000.0, + latency_budget_ms=100, + ) + signals = SharedSignals(version=1, price_delta=0.05, cross_venue_corr=0.9, liquidity=50000.0) + + plan = admm_step(local, signals) + + # Basic sanity checks on shape and content + assert plan is not None + assert isinstance(plan.actions, list) + assert len(plan.actions) == 1 + act = plan.actions[0] + assert "venue_from" in act and "venue_to" in act + assert act["instrument"] == local.asset_pair + assert plan.timestamp <= datetime.datetime.utcnow()