idea144-crossvenuearbx-fede.../crossvenue_arbx/coordinator.py

93 lines
3.7 KiB
Python

from __future__ import annotations
from typing import Dict, List, Any
from .core import LocalArbProblem, SharedSignals, PlanDelta
from .governance import GraphOfContracts
from .bridge import EnergiBridge
class CentralCoordinator:
"""A lightweight, async-ADMM-like coordinator with deterministic replay.
This MVP keeps a registry of latest SharedSignals per venue and merges inputs
deterministically to produce PlanDelta actions.
"""
def __init__(self) -> None:
self.shared_signals_by_venue: Dict[str, SharedSignals] = {}
self.version: int = 0
self.contracts = GraphOfContracts()
# Each element is a list of delta action dicts produced by a venue
self.pending_delta_actions: List[List[Dict[str, Any]]] = []
self.last_plan: PlanDelta | None = None
# Optional bridge for canonical IR interoperability (can be used by adapters)
self.bridge = EnergiBridge()
self.last_catopt: Dict[str, Any] | None = None
self.replay_log: List[str] = []
def _other_venue(self, venue: str) -> str:
venues = sorted(self.shared_signals_by_venue)
if len(venues) < 2:
return "VenueB" if venue != "VenueB" else "VenueA"
for candidate in venues:
if candidate != venue:
return candidate
return venue
def ingest_local(self, risk_source: LocalArbProblem, signals: SharedSignals) -> PlanDelta | None:
self.version += 1
self.shared_signals_by_venue[risk_source.venue] = signals
# Produce a canonical IR representation for interoperability
try:
self.last_catopt = self.bridge.to_catopt(risk_source, signals)
except Exception:
# Non-critical for MVP; retain existing behavior on bridge failure
self.last_catopt = None
delta = []
for asset in sorted(signals.price_delta_by_asset):
delta_price = signals.price_delta_by_asset[asset]
if abs(delta_price) > 0.0005:
action = {
"from_venue": risk_source.venue,
"to_venue": self._other_venue(risk_source.venue),
"asset": asset,
"size": min(risk_source.max_exposure, 10_000.0 * abs(delta_price)),
"time": float(self.version),
}
delta.append(action)
if delta:
contract_id = risk_source.fingerprint()
plan = PlanDelta(
delta_actions=delta,
timestamp=float(self.version),
contract_id=contract_id,
)
self.last_plan = plan
self.pending_delta_actions.append(delta)
self.contracts.register(contract_id, adapter_version=str(signals.version), schema_hash=plan.signature)
self.replay_log.append(plan.signature)
return plan
return None
def reconcile(self) -> PlanDelta | None:
if not self.pending_delta_actions:
return None
merged = []
for d in self.pending_delta_actions:
merged.extend(d)
merged.sort(key=lambda a: (a.get("from_venue"), a.get("to_venue"), a.get("asset")))
contract_id = "merged:" + ",".join(
sorted({action.get("asset", "") for action in merged})
)
plan = PlanDelta(
delta_actions=merged,
timestamp=float(self.version),
contract_id=contract_id,
)
self.pending_delta_actions.clear()
self.last_plan = plan
self.contracts.register(contract_id, adapter_version="reconciled", schema_hash=plan.signature)
self.replay_log.append(plan.signature)
return plan