93 lines
3.7 KiB
Python
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
|