"""OKX 交易所适配器:只负责行情与合约,不含策略选约。""" from __future__ import annotations import logging from typing import Any, Sequence from ...config import Settings, get_settings from ..book_cache import BookCache from ..types import BookLevel, MarketSnapshot, OptionPair, Quote from .parse import rows_to_option_contracts, safe_float from .rest import OkxRestClient from .ws import OkxPublicWs logger = logging.getLogger(__name__) class OkxExchange: name = "okx" def __init__(self, settings: Settings | None = None) -> None: self.settings = settings or get_settings() self.cache = BookCache() proxy = self.settings.okx_http_proxy or None self.rest = OkxRestClient(self.settings.okx_rest_base, proxy=proxy) self.ws = OkxPublicWs(self.settings.okx_ws_public, self.cache, proxy=proxy) self._started = False self._ct_cache: dict[str, float] = {} async def start(self) -> None: if self._started: return self._started = True await self.ws.start() logger.info("OKX exchange started") async def stop(self) -> None: self._started = False await self.ws.stop() self.rest.close() logger.info("OKX exchange stopped") def list_option_contracts(self, family: str) -> list[dict[str, Any]]: rows = self.rest.fetch_option_instruments(family) contracts = rows_to_option_contracts(rows) for c in contracts: if c.get("ct_mult"): self._ct_cache[str(c["inst_id"])] = float(c["ct_mult"]) return contracts def fetch_index(self, index_id: str) -> float | None: return self.rest.fetch_index_ticker(index_id) def fetch_mark(self, inst_id: str) -> float | None: return self.rest.fetch_mark_price(inst_id) def fetch_book( self, inst_id: str, depth: int = 5 ) -> tuple[list[BookLevel], list[BookLevel], int | None]: return self.rest.fetch_books(inst_id, sz=depth) def get_ct_mult(self, option_inst_id: str, family: str, default: float) -> float: if option_inst_id in self._ct_cache: return self._ct_cache[option_inst_id] try: rows = self.rest.fetch_instruments(inst_type="OPTION", inst_family=family) for r in rows: if str(r.get("instId")) == option_inst_id: m = safe_float(r.get("ctMult")) if m and m > 0: self._ct_cache[option_inst_id] = float(m) return float(m) except Exception: pass return float(default) def set_pair(self, pair: OptionPair | None) -> None: self.cache.set_pair(pair) def warm_and_subscribe(self, inst_ids: Sequence[str]) -> None: ids = [i for i in inst_ids if i] for inst in ids: bids, asks, ts = self.rest.fetch_books(inst, sz=5) self.cache.upsert_book(inst, bids=bids, asks=asks, ts_ms=ts) mp = self.rest.fetch_mark_price(inst) if mp: self.cache.set_mark_px(inst, mp) keep = set(ids) self.cache.drop_except(keep) self.ws.set_instruments(ids) async def resubscribe(self, inst_ids: Sequence[str]) -> None: await self.ws.resubscribe([i for i in inst_ids if i]) def quote(self, inst_id: str) -> Quote | None: return self.cache.get(inst_id) def snapshot(self, perp_inst_id: str) -> MarketSnapshot: return self.cache.snapshot(perp_inst_id) def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]: return self.snapshot(perp_inst_id).to_dict() def set_index_px(self, px: float | None) -> None: self.cache.set_index_px(px) def set_mark_px(self, inst_id: str, mark_px: float | None) -> None: self.cache.set_mark_px(inst_id, mark_px)