1df4b11aef
CI / lint-test-build (push) Failing after 1m7s
- Introduced new HTML templates for the dashboard, metrics, overview, and backtesting functionalities. - Implemented partial templates for metrics, overview, audit, controls, and charts to enhance modularity. - Updated the Jinja2 template resolution logic to support different deployment environments. - Added a health check template to display the service status. - Included a test suite to verify the template resolution logic. - Updated `pyproject.toml` to include new HTML templates in the package data.
134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from typing import Any, Protocol
|
|
|
|
|
|
class SupportsOrderStatusPolling(Protocol):
|
|
async def query_order(
|
|
self, *, order_id: str, include_trades: bool = True
|
|
) -> dict[str, Any]: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class OrderFillState:
|
|
order_id: str
|
|
status: str
|
|
filled_volume: float | None
|
|
avg_price: float | None
|
|
updated_at: datetime
|
|
source: str
|
|
|
|
@property
|
|
def is_terminal(self) -> bool:
|
|
return self.status in {"closed", "canceled", "expired"}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FillMonitorResult:
|
|
order_id: str
|
|
timed_out: bool
|
|
terminal_state: OrderFillState | None
|
|
last_state: OrderFillState | None
|
|
elapsed_seconds: float
|
|
|
|
|
|
class FillMonitor:
|
|
def __init__(
|
|
self,
|
|
poll_client: SupportsOrderStatusPolling,
|
|
*,
|
|
poll_interval_seconds: float = 0.5,
|
|
max_wait_seconds: float = 10.0,
|
|
ws_status_provider: Callable[[str], OrderFillState | None] | None = None,
|
|
) -> None:
|
|
if poll_interval_seconds <= 0.0:
|
|
raise ValueError("poll_interval_seconds must be > 0.0")
|
|
if max_wait_seconds <= 0.0:
|
|
raise ValueError("max_wait_seconds must be > 0.0")
|
|
|
|
self._poll_client = poll_client
|
|
self._poll_interval_seconds = poll_interval_seconds
|
|
self._max_wait_seconds = max_wait_seconds
|
|
self._ws_status_provider = ws_status_provider
|
|
|
|
@staticmethod
|
|
def _to_float(value: Any) -> float | None:
|
|
if value is None:
|
|
return None
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
@classmethod
|
|
def _state_from_payload(
|
|
cls, order_id: str, payload: dict[str, Any], *, source: str
|
|
) -> OrderFillState:
|
|
status = str(payload.get("status", "unknown")).lower()
|
|
return OrderFillState(
|
|
order_id=order_id,
|
|
status=status,
|
|
filled_volume=cls._to_float(payload.get("vol_exec")),
|
|
avg_price=cls._to_float(payload.get("price") or payload.get("avg_price")),
|
|
updated_at=datetime.now(UTC),
|
|
source=source,
|
|
)
|
|
|
|
@classmethod
|
|
def _extract_order_payload(cls, order_id: str, response: dict[str, Any]) -> dict[str, Any]:
|
|
if order_id in response and isinstance(response[order_id], dict):
|
|
payload = response[order_id]
|
|
return {str(key): value for key, value in payload.items()}
|
|
return response
|
|
|
|
async def wait_for_terminal_fill(self, order_id: str) -> FillMonitorResult:
|
|
if not order_id.strip():
|
|
raise ValueError("order_id must be non-empty")
|
|
|
|
started = time.monotonic()
|
|
last_state: OrderFillState | None = None
|
|
|
|
while True:
|
|
elapsed = time.monotonic() - started
|
|
if elapsed >= self._max_wait_seconds:
|
|
return FillMonitorResult(
|
|
order_id=order_id,
|
|
timed_out=True,
|
|
terminal_state=None,
|
|
last_state=last_state,
|
|
elapsed_seconds=elapsed,
|
|
)
|
|
|
|
if self._ws_status_provider is not None:
|
|
ws_state = self._ws_status_provider(order_id)
|
|
if ws_state is not None:
|
|
last_state = ws_state
|
|
if ws_state.is_terminal:
|
|
return FillMonitorResult(
|
|
order_id=order_id,
|
|
timed_out=False,
|
|
terminal_state=ws_state,
|
|
last_state=ws_state,
|
|
elapsed_seconds=elapsed,
|
|
)
|
|
|
|
response = await self._poll_client.query_order(order_id=order_id, include_trades=True)
|
|
payload = self._extract_order_payload(order_id, response)
|
|
polled_state = self._state_from_payload(order_id, payload, source="rest_poll")
|
|
last_state = polled_state
|
|
if polled_state.is_terminal:
|
|
return FillMonitorResult(
|
|
order_id=order_id,
|
|
timed_out=False,
|
|
terminal_state=polled_state,
|
|
last_state=polled_state,
|
|
elapsed_seconds=time.monotonic() - started,
|
|
)
|
|
|
|
await asyncio.sleep(self._poll_interval_seconds)
|