indicators and plots
This commit is contained in:
472
backend/src/exchange_kernel/state.py
Normal file
472
backend/src/exchange_kernel/state.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""
|
||||
State management for the Exchange Kernel.
|
||||
|
||||
Implements the storage and reconciliation logic for desired vs actual state.
|
||||
This is the "Kubernetes for orders" concept - maintaining intent and continuously
|
||||
reconciling reality to match intent.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
from collections import defaultdict
|
||||
|
||||
from .models import OrderIntent, OrderState, ReconciliationStatus
|
||||
from ..schema.order_spec import Uint64
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Intent State Store - Desired State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class IntentStateStore(ABC):
|
||||
"""
|
||||
Storage for order intents (desired state).
|
||||
|
||||
This represents what the strategy kernel wants. Intents are durable and
|
||||
persist across restarts. The reconciliation loop continuously works to
|
||||
make actual state match these intents.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def create_intent(self, intent: OrderIntent) -> None:
|
||||
"""
|
||||
Store a new order intent.
|
||||
|
||||
Args:
|
||||
intent: The order intent to store
|
||||
|
||||
Raises:
|
||||
AlreadyExistsError: If intent_id already exists
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_intent(self, intent_id: str) -> OrderIntent:
|
||||
"""
|
||||
Retrieve an order intent.
|
||||
|
||||
Args:
|
||||
intent_id: Intent ID to retrieve
|
||||
|
||||
Returns:
|
||||
The order intent
|
||||
|
||||
Raises:
|
||||
NotFoundError: If intent_id doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update_intent(self, intent: OrderIntent) -> None:
|
||||
"""
|
||||
Update an existing order intent.
|
||||
|
||||
Args:
|
||||
intent: Updated intent (intent_id must match existing)
|
||||
|
||||
Raises:
|
||||
NotFoundError: If intent_id doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_intent(self, intent_id: str) -> None:
|
||||
"""
|
||||
Delete an order intent.
|
||||
|
||||
Args:
|
||||
intent_id: Intent ID to delete
|
||||
|
||||
Raises:
|
||||
NotFoundError: If intent_id doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_intents(
|
||||
self,
|
||||
symbol_id: str | None = None,
|
||||
group_id: str | None = None,
|
||||
) -> list[OrderIntent]:
|
||||
"""
|
||||
List all order intents, optionally filtered.
|
||||
|
||||
Args:
|
||||
symbol_id: Filter by symbol
|
||||
group_id: Filter by OCO group
|
||||
|
||||
Returns:
|
||||
List of matching intents
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_intents_by_group(self, group_id: str) -> list[OrderIntent]:
|
||||
"""
|
||||
Get all intents in an OCO group.
|
||||
|
||||
Args:
|
||||
group_id: Group ID to query
|
||||
|
||||
Returns:
|
||||
List of intents in the group
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Actual State Store - Current Reality
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ActualStateStore(ABC):
|
||||
"""
|
||||
Storage for actual order state (reality on exchange).
|
||||
|
||||
This represents what actually exists on the exchange right now.
|
||||
Updated frequently from exchange feeds and order status queries.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def create_order_state(self, state: OrderState) -> None:
|
||||
"""
|
||||
Store a new order state.
|
||||
|
||||
Args:
|
||||
state: The order state to store
|
||||
|
||||
Raises:
|
||||
AlreadyExistsError: If order state for this intent_id already exists
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_order_state(self, intent_id: str) -> OrderState:
|
||||
"""
|
||||
Retrieve order state for an intent.
|
||||
|
||||
Args:
|
||||
intent_id: Intent ID to query
|
||||
|
||||
Returns:
|
||||
The current order state
|
||||
|
||||
Raises:
|
||||
NotFoundError: If no state exists for this intent
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_order_state_by_exchange_id(self, exchange_order_id: str) -> OrderState:
|
||||
"""
|
||||
Retrieve order state by exchange order ID.
|
||||
|
||||
Useful for processing exchange callbacks that only provide exchange_order_id.
|
||||
|
||||
Args:
|
||||
exchange_order_id: Exchange's order ID
|
||||
|
||||
Returns:
|
||||
The order state
|
||||
|
||||
Raises:
|
||||
NotFoundError: If no state exists for this exchange order ID
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update_order_state(self, state: OrderState) -> None:
|
||||
"""
|
||||
Update an existing order state.
|
||||
|
||||
Args:
|
||||
state: Updated state (intent_id must match existing)
|
||||
|
||||
Raises:
|
||||
NotFoundError: If state doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_order_state(self, intent_id: str) -> None:
|
||||
"""
|
||||
Delete an order state.
|
||||
|
||||
Args:
|
||||
intent_id: Intent ID whose state to delete
|
||||
|
||||
Raises:
|
||||
NotFoundError: If state doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_order_states(
|
||||
self,
|
||||
symbol_id: str | None = None,
|
||||
reconciliation_status: ReconciliationStatus | None = None,
|
||||
) -> list[OrderState]:
|
||||
"""
|
||||
List all order states, optionally filtered.
|
||||
|
||||
Args:
|
||||
symbol_id: Filter by symbol
|
||||
reconciliation_status: Filter by reconciliation status
|
||||
|
||||
Returns:
|
||||
List of matching order states
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_stale_orders(self, max_age_seconds: int) -> list[OrderState]:
|
||||
"""
|
||||
Find orders that haven't been synced recently.
|
||||
|
||||
Used to identify orders that need status updates from exchange.
|
||||
|
||||
Args:
|
||||
max_age_seconds: Maximum age since last sync
|
||||
|
||||
Returns:
|
||||
List of order states that need refresh
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# In-Memory Implementations (for testing/prototyping)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class InMemoryIntentStore(IntentStateStore):
|
||||
"""Simple in-memory implementation of IntentStateStore"""
|
||||
|
||||
def __init__(self):
|
||||
self._intents: dict[str, OrderIntent] = {}
|
||||
self._by_symbol: dict[str, set[str]] = defaultdict(set)
|
||||
self._by_group: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
async def create_intent(self, intent: OrderIntent) -> None:
|
||||
if intent.intent_id in self._intents:
|
||||
raise ValueError(f"Intent {intent.intent_id} already exists")
|
||||
self._intents[intent.intent_id] = intent
|
||||
self._by_symbol[intent.order.symbol_id].add(intent.intent_id)
|
||||
if intent.group_id:
|
||||
self._by_group[intent.group_id].add(intent.intent_id)
|
||||
|
||||
async def get_intent(self, intent_id: str) -> OrderIntent:
|
||||
if intent_id not in self._intents:
|
||||
raise KeyError(f"Intent {intent_id} not found")
|
||||
return self._intents[intent_id]
|
||||
|
||||
async def update_intent(self, intent: OrderIntent) -> None:
|
||||
if intent.intent_id not in self._intents:
|
||||
raise KeyError(f"Intent {intent.intent_id} not found")
|
||||
old_intent = self._intents[intent.intent_id]
|
||||
|
||||
# Update indices if symbol or group changed
|
||||
if old_intent.order.symbol_id != intent.order.symbol_id:
|
||||
self._by_symbol[old_intent.order.symbol_id].discard(intent.intent_id)
|
||||
self._by_symbol[intent.order.symbol_id].add(intent.intent_id)
|
||||
|
||||
if old_intent.group_id != intent.group_id:
|
||||
if old_intent.group_id:
|
||||
self._by_group[old_intent.group_id].discard(intent.intent_id)
|
||||
if intent.group_id:
|
||||
self._by_group[intent.group_id].add(intent.intent_id)
|
||||
|
||||
self._intents[intent.intent_id] = intent
|
||||
|
||||
async def delete_intent(self, intent_id: str) -> None:
|
||||
if intent_id not in self._intents:
|
||||
raise KeyError(f"Intent {intent_id} not found")
|
||||
intent = self._intents[intent_id]
|
||||
self._by_symbol[intent.order.symbol_id].discard(intent_id)
|
||||
if intent.group_id:
|
||||
self._by_group[intent.group_id].discard(intent_id)
|
||||
del self._intents[intent_id]
|
||||
|
||||
async def list_intents(
|
||||
self,
|
||||
symbol_id: str | None = None,
|
||||
group_id: str | None = None,
|
||||
) -> list[OrderIntent]:
|
||||
if symbol_id and group_id:
|
||||
# Intersection of both filters
|
||||
symbol_ids = self._by_symbol.get(symbol_id, set())
|
||||
group_ids = self._by_group.get(group_id, set())
|
||||
intent_ids = symbol_ids & group_ids
|
||||
elif symbol_id:
|
||||
intent_ids = self._by_symbol.get(symbol_id, set())
|
||||
elif group_id:
|
||||
intent_ids = self._by_group.get(group_id, set())
|
||||
else:
|
||||
intent_ids = self._intents.keys()
|
||||
|
||||
return [self._intents[iid] for iid in intent_ids]
|
||||
|
||||
async def get_intents_by_group(self, group_id: str) -> list[OrderIntent]:
|
||||
intent_ids = self._by_group.get(group_id, set())
|
||||
return [self._intents[iid] for iid in intent_ids]
|
||||
|
||||
|
||||
class InMemoryActualStateStore(ActualStateStore):
|
||||
"""Simple in-memory implementation of ActualStateStore"""
|
||||
|
||||
def __init__(self):
|
||||
self._states: dict[str, OrderState] = {}
|
||||
self._by_exchange_id: dict[str, str] = {} # exchange_order_id -> intent_id
|
||||
self._by_symbol: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
async def create_order_state(self, state: OrderState) -> None:
|
||||
if state.intent_id in self._states:
|
||||
raise ValueError(f"Order state for intent {state.intent_id} already exists")
|
||||
self._states[state.intent_id] = state
|
||||
self._by_exchange_id[state.exchange_order_id] = state.intent_id
|
||||
self._by_symbol[state.status.order.symbol_id].add(state.intent_id)
|
||||
|
||||
async def get_order_state(self, intent_id: str) -> OrderState:
|
||||
if intent_id not in self._states:
|
||||
raise KeyError(f"Order state for intent {intent_id} not found")
|
||||
return self._states[intent_id]
|
||||
|
||||
async def get_order_state_by_exchange_id(self, exchange_order_id: str) -> OrderState:
|
||||
if exchange_order_id not in self._by_exchange_id:
|
||||
raise KeyError(f"Order state for exchange order {exchange_order_id} not found")
|
||||
intent_id = self._by_exchange_id[exchange_order_id]
|
||||
return self._states[intent_id]
|
||||
|
||||
async def update_order_state(self, state: OrderState) -> None:
|
||||
if state.intent_id not in self._states:
|
||||
raise KeyError(f"Order state for intent {state.intent_id} not found")
|
||||
old_state = self._states[state.intent_id]
|
||||
|
||||
# Update exchange_id index if it changed
|
||||
if old_state.exchange_order_id != state.exchange_order_id:
|
||||
del self._by_exchange_id[old_state.exchange_order_id]
|
||||
self._by_exchange_id[state.exchange_order_id] = state.intent_id
|
||||
|
||||
# Update symbol index if it changed
|
||||
old_symbol = old_state.status.order.symbol_id
|
||||
new_symbol = state.status.order.symbol_id
|
||||
if old_symbol != new_symbol:
|
||||
self._by_symbol[old_symbol].discard(state.intent_id)
|
||||
self._by_symbol[new_symbol].add(state.intent_id)
|
||||
|
||||
self._states[state.intent_id] = state
|
||||
|
||||
async def delete_order_state(self, intent_id: str) -> None:
|
||||
if intent_id not in self._states:
|
||||
raise KeyError(f"Order state for intent {intent_id} not found")
|
||||
state = self._states[intent_id]
|
||||
del self._by_exchange_id[state.exchange_order_id]
|
||||
self._by_symbol[state.status.order.symbol_id].discard(intent_id)
|
||||
del self._states[intent_id]
|
||||
|
||||
async def list_order_states(
|
||||
self,
|
||||
symbol_id: str | None = None,
|
||||
reconciliation_status: ReconciliationStatus | None = None,
|
||||
) -> list[OrderState]:
|
||||
if symbol_id:
|
||||
intent_ids = self._by_symbol.get(symbol_id, set())
|
||||
states = [self._states[iid] for iid in intent_ids]
|
||||
else:
|
||||
states = list(self._states.values())
|
||||
|
||||
if reconciliation_status:
|
||||
states = [s for s in states if s.reconciliation_status == reconciliation_status]
|
||||
|
||||
return states
|
||||
|
||||
async def get_stale_orders(self, max_age_seconds: int) -> list[OrderState]:
|
||||
import time
|
||||
current_time = int(time.time())
|
||||
threshold = current_time - max_age_seconds
|
||||
|
||||
return [
|
||||
state
|
||||
for state in self._states.values()
|
||||
if state.last_sync_at < threshold
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reconciliation Engine (framework only, no implementation)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ReconciliationEngine:
|
||||
"""
|
||||
Reconciliation engine that continuously works to make actual state match intent.
|
||||
|
||||
This is the heart of the "Kubernetes for orders" concept. It:
|
||||
1. Compares desired state (intents) with actual state (exchange orders)
|
||||
2. Computes necessary actions (place, modify, cancel)
|
||||
3. Executes those actions via the exchange API
|
||||
4. Handles retries, errors, and edge cases
|
||||
|
||||
This is a framework class - concrete implementations will be exchange-specific.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
intent_store: IntentStateStore,
|
||||
actual_store: ActualStateStore,
|
||||
):
|
||||
"""
|
||||
Initialize the reconciliation engine.
|
||||
|
||||
Args:
|
||||
intent_store: Store for desired state
|
||||
actual_store: Store for actual state
|
||||
"""
|
||||
self.intent_store = intent_store
|
||||
self.actual_store = actual_store
|
||||
self._running = False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the reconciliation loop"""
|
||||
self._running = True
|
||||
# Implementation would start async reconciliation loop here
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the reconciliation loop"""
|
||||
self._running = False
|
||||
# Implementation would stop reconciliation loop here
|
||||
pass
|
||||
|
||||
async def reconcile_intent(self, intent_id: str) -> None:
|
||||
"""
|
||||
Reconcile a specific intent.
|
||||
|
||||
Compares the intent with actual state and takes necessary actions.
|
||||
|
||||
Args:
|
||||
intent_id: Intent to reconcile
|
||||
"""
|
||||
# Framework only - concrete implementation needed
|
||||
pass
|
||||
|
||||
async def reconcile_all(self) -> None:
|
||||
"""
|
||||
Reconcile all intents.
|
||||
|
||||
Full reconciliation pass over all orders.
|
||||
"""
|
||||
# Framework only - concrete implementation needed
|
||||
pass
|
||||
|
||||
def get_metrics(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get reconciliation metrics.
|
||||
|
||||
Returns:
|
||||
Metrics about reconciliation performance, errors, etc.
|
||||
"""
|
||||
return {
|
||||
"running": self._running,
|
||||
"reconciliation_lag_ms": 0, # Framework only
|
||||
"pending_reconciliations": 0, # Framework only
|
||||
"error_count": 0, # Framework only
|
||||
"retry_count": 0, # Framework only
|
||||
}
|
||||
Reference in New Issue
Block a user