e19bb452f9
Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""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)
|