From 7c8b89a7343690dc8c63ca5bf35048a23c8a2fd1 Mon Sep 17 00:00:00 2001 From: hamin Date: Wed, 13 May 2026 20:33:17 +0900 Subject: [PATCH] feat: add high-level wrappers for liquidation / open-interest / cex-symbol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the new endpoints shipped in 0.27.0 were only reachable via the lower-level `client..query(path, params)` escape hatch — the auto-regen only updated `_endpoints.py` metadata, which the hand-curated category classes don't actually consume. This adds hand-written wrappers that match the existing class layout: client.liquidation.heatmap(window="1h", topN=10) client.liquidation.map(base="BTC") client.liquidation.symbol_history(symbol="BTC", interval="5m") client.open_interest.overview(limit=20) client.open_interest.summary(topN=10) client.open_interest.history_aggregated(token_id="bitcoin") client.cex.symbol.oi(base="BTC") client.cex.symbol.oi_stats(base="BTC", currency="USD") client.cex.symbol.liquidation(base="BTC", window="24h") Top-level `Liquidation` / `OpenInterest` mirror the REST grouping under `/api/v1/{liquidation,open-interest}/*` and align with the Rust SDK's typed wrappers in `datamaxi::generated::{Liquidation, OpenInterest}`. `CexSymbol` is nested under `Cex` to match the REST path layout (`/api/v1/cex/symbol/*`). Each method validates the same params the BE enforces (window enum, topN range, sort enum, currency enum) and surfaces ValueError early rather than letting the call go through and 400. --- datamaxi/datamaxi/__init__.py | 13 +++ datamaxi/datamaxi/cex.py | 7 ++ datamaxi/datamaxi/cex_symbol.py | 126 +++++++++++++++++++++++++++++ datamaxi/datamaxi/liquidation.py | 117 +++++++++++++++++++++++++++ datamaxi/datamaxi/open_interest.py | 113 ++++++++++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 datamaxi/datamaxi/cex_symbol.py create mode 100644 datamaxi/datamaxi/liquidation.py create mode 100644 datamaxi/datamaxi/open_interest.py diff --git a/datamaxi/datamaxi/__init__.py b/datamaxi/datamaxi/__init__.py index 327a0ba..95ff44b 100644 --- a/datamaxi/datamaxi/__init__.py +++ b/datamaxi/datamaxi/__init__.py @@ -5,6 +5,8 @@ from datamaxi.datamaxi.funding_rate import FundingRate from datamaxi.datamaxi.forex import Forex from datamaxi.datamaxi.premium import Premium +from datamaxi.datamaxi.liquidation import Liquidation +from datamaxi.datamaxi.open_interest import OpenInterest from datamaxi.datamaxi.cex_candle import CexCandle # used in documentation # noqa:F401 from datamaxi.datamaxi.cex_ticker import ( # used in documentation # noqa:F401 CexTicker, @@ -21,6 +23,9 @@ from datamaxi.datamaxi.cex_token import ( # used in documentation # noqa:F401 CexToken, ) +from datamaxi.datamaxi.cex_symbol import ( # used in documentation # noqa:F401 + CexSymbol, +) class Datamaxi: @@ -41,3 +46,11 @@ def __init__(self, api_key=None, **kwargs: Any): self.funding_rate = FundingRate(api_key, **kwargs) self.forex = Forex(api_key, **kwargs) self.premium = Premium(api_key, **kwargs) + # Futures-only surfaces. Top-level on the client so callers + # reach them via `client.liquidation.heatmap(...)` / + # `client.open_interest.summary(...)` — matches the + # `/api/v1/{liquidation,open-interest}/*` REST grouping and + # mirrors the equivalent typed wrappers in the Rust SDK + # (`datamaxi::generated::{Liquidation, OpenInterest}`). + self.liquidation = Liquidation(api_key, **kwargs) + self.open_interest = OpenInterest(api_key, **kwargs) diff --git a/datamaxi/datamaxi/cex.py b/datamaxi/datamaxi/cex.py index 43818ae..c3416c4 100644 --- a/datamaxi/datamaxi/cex.py +++ b/datamaxi/datamaxi/cex.py @@ -6,6 +6,7 @@ from datamaxi.datamaxi.cex_wallet_status import CexWalletStatus from datamaxi.datamaxi.cex_announcement import CexAnnouncement from datamaxi.datamaxi.cex_token import CexToken +from datamaxi.datamaxi.cex_symbol import CexSymbol class Cex(API): @@ -26,3 +27,9 @@ def __init__(self, api_key=None, **kwargs: Any): self.wallet_status = CexWalletStatus(api_key, **kwargs) self.announcement = CexAnnouncement(api_key, **kwargs) self.token = CexToken(api_key, **kwargs) + # Per-base / per-symbol surfaces (metadata, tags, cautions, + # delistings, volume, OI / OI-stats / liquidation aggregates). + # Grouped under `cex.symbol` to mirror the REST path layout + # (`/api/v1/cex/symbol/*`) and to keep the top-level `Cex` + # surface flat. + self.symbol = CexSymbol(api_key, **kwargs) diff --git a/datamaxi/datamaxi/cex_symbol.py b/datamaxi/datamaxi/cex_symbol.py new file mode 100644 index 0000000..80d7907 --- /dev/null +++ b/datamaxi/datamaxi/cex_symbol.py @@ -0,0 +1,126 @@ +from typing import Any, Dict, Optional +from datamaxi.api import API + + +class CexSymbol(API): + """Client to fetch per-base / per-symbol CEX metadata + aggregates.""" + + def __init__(self, api_key=None, **kwargs: Any): + """Initialize the object. + + Args: + api_key (str): The DataMaxi+ API key + **kwargs: Keyword arguments used by `datamaxi.api.API`. + """ + super().__init__(api_key, **kwargs) + + def metadata( + self, exchange: Optional[str] = None, base: Optional[str] = None + ) -> Dict[str, Any]: + """Trading status + caution + tags + delisting metadata. + + `GET /api/v1/cex/symbol/metadata` + """ + params: Dict[str, Any] = {} + if exchange is not None: + params["exchange"] = exchange + if base is not None: + params["base"] = base + return self.query("/api/v1/cex/symbol/metadata", params) + + def tags( + self, exchange: Optional[str] = None, base: Optional[str] = None + ) -> Dict[str, Any]: + """Exchange-assigned tags (e.g. seed, alpha) per symbol. + + `GET /api/v1/cex/symbol/tags` + """ + params: Dict[str, Any] = {} + if exchange is not None: + params["exchange"] = exchange + if base is not None: + params["base"] = base + return self.query("/api/v1/cex/symbol/tags", params) + + def cautions( + self, exchange: Optional[str] = None, base: Optional[str] = None + ) -> Dict[str, Any]: + """Active caution / investment-warning flags per symbol. + + `GET /api/v1/cex/symbol/cautions` + """ + params: Dict[str, Any] = {} + if exchange is not None: + params["exchange"] = exchange + if base is not None: + params["base"] = base + return self.query("/api/v1/cex/symbol/cautions", params) + + def delistings( + self, exchange: Optional[str] = None, base: Optional[str] = None + ) -> Dict[str, Any]: + """Scheduled delistings with timestamps. + + `GET /api/v1/cex/symbol/delistings` + """ + params: Dict[str, Any] = {} + if exchange is not None: + params["exchange"] = exchange + if base is not None: + params["base"] = base + return self.query("/api/v1/cex/symbol/delistings", params) + + def volume(self, base: str, exchange: Optional[str] = None) -> Dict[str, Any]: + """Per-exchange 24h volume for a single base asset. + + `GET /api/v1/cex/symbol/volume` + """ + params: Dict[str, Any] = {"base": base} + if exchange is not None: + params["exchange"] = exchange + return self.query("/api/v1/cex/symbol/volume", params) + + def oi(self, base: str, exchange: Optional[str] = None) -> Dict[str, Any]: + """Per-exchange Open Interest for a single base asset. + + `GET /api/v1/cex/symbol/oi` + """ + params: Dict[str, Any] = {"base": base} + if exchange is not None: + params["exchange"] = exchange + return self.query("/api/v1/cex/symbol/oi", params) + + def oi_stats( + self, + base: str, + exchange: Optional[str] = None, + currency: str = "USD", + ) -> Dict[str, Any]: + """Per-exchange OI snapshot with 1h / 4h / 24h deltas. + + `GET /api/v1/cex/symbol/oi-stats` + + Args: + base (str): Base asset (e.g. ``BTC``). + exchange (str): Optional single-exchange filter. + currency (str): ``USD`` or ``KRW``. + """ + if currency not in ("USD", "KRW"): + raise ValueError("currency must be either USD or KRW") + params: Dict[str, Any] = {"base": base, "currency": currency} + if exchange is not None: + params["exchange"] = exchange + return self.query("/api/v1/cex/symbol/oi-stats", params) + + def liquidation(self, base: str, window: str = "24h") -> Dict[str, Any]: + """Per-exchange long / short liquidation aggregates over a window. + + `GET /api/v1/cex/symbol/liquidation` + + Args: + base (str): Base asset (e.g. ``BTC``). + window (str): Time window (``1h``, ``24h``, ``7d`` — server caps at 30d). + """ + return self.query( + "/api/v1/cex/symbol/liquidation", {"base": base, "window": window} + ) diff --git a/datamaxi/datamaxi/liquidation.py b/datamaxi/datamaxi/liquidation.py new file mode 100644 index 0000000..a6d7f8c --- /dev/null +++ b/datamaxi/datamaxi/liquidation.py @@ -0,0 +1,117 @@ +from typing import Any, Dict, Optional +from datamaxi.api import API + + +class Liquidation(API): + """Client to fetch CEX futures liquidation data from DataMaxi+ API.""" + + def __init__(self, api_key=None, **kwargs: Any): + """Initialize the object. + + Args: + api_key (str): The DataMaxi+ API key + **kwargs: Keyword arguments used by `datamaxi.api.API`. + """ + super().__init__(api_key, **kwargs) + + def __call__( + self, + exchange: str, + symbol: str, + limit: int = 100, + ) -> Dict[str, Any]: + """Recent liquidation events for a specific futures symbol. + + `GET /api/v1/liquidation` + + Args: + exchange (str): Exchange name (e.g. ``binance``). + symbol (str): Exchange-native API symbol (e.g. ``BTC-USDT``). + limit (int): Max events to return (server caps). + + Returns: + Liquidation events response. + """ + if limit < 1: + raise ValueError("limit must be greater than 0") + params = {"exchange": exchange, "symbol": symbol, "limit": limit} + return self.query("/api/v1/liquidation", params) + + def feed(self, limit: int = 100) -> Dict[str, Any]: + """Firehose: most recent liquidation events across every symbol. + + `GET /api/v1/liquidation/feed` + + Args: + limit (int): Max events to return. + """ + if limit < 1: + raise ValueError("limit must be greater than 0") + return self.query("/api/v1/liquidation/feed", {"limit": limit}) + + def heatmap( + self, + window: str = "1h", + topN: int = 10, + ) -> Dict[str, Any]: + """Token × exchange liquidation heatmap over a rolling window. + + `GET /api/v1/liquidation/heatmap` + + Args: + window (str): Rolling window (``1h``, ``4h``, or ``24h``). + topN (int): Top N tokens by total (1-30). + """ + if topN < 1 or topN > 30: + raise ValueError("topN must be between 1 and 30") + return self.query( + "/api/v1/liquidation/heatmap", {"window": window, "topN": topN} + ) + + def map( + self, + base: str, + exchange: str = "binance", + quote: str = "USDT", + ) -> Dict[str, Any]: + """Coinglass-style liquidation map (price × leverage tier). + + `GET /api/v1/liquidation/map` + + Args: + base (str): Base asset (e.g. ``BTC``). + exchange (str): Exchange (default ``binance``). + quote (str): Quote asset (default ``USDT``). + """ + params = {"base": base, "exchange": exchange, "quote": quote} + return self.query("/api/v1/liquidation/map", params) + + def symbol_history( + self, + symbol: str, + quote: str = "USDT", + exchange: Optional[str] = None, + interval: str = "5m", + window: str = "24h", + ) -> Dict[str, Any]: + """Bucketed long / short liquidation USD time series + price line. + + `GET /api/v1/liquidation/symbol-history` + + Args: + symbol (str): Base asset (e.g. ``BTC``). + quote (str): Quote asset (default ``USDT``). + exchange (str): Optional exchange filter for the liquidation + aggregation. Price line stays on Binance unless set. + interval (str): Bucket interval (``5m``, ``15m``, or ``1h``). + window (str): Lookback window (``24h``, ``72h``, or ``7d``). + """ + params = { + "symbol": symbol, + "quote": quote, + "interval": interval, + "window": window, + } + if exchange is not None: + params["exchange"] = exchange + return self.query("/api/v1/liquidation/symbol-history", params) diff --git a/datamaxi/datamaxi/open_interest.py b/datamaxi/datamaxi/open_interest.py new file mode 100644 index 0000000..0a299f5 --- /dev/null +++ b/datamaxi/datamaxi/open_interest.py @@ -0,0 +1,113 @@ +from typing import Any, Dict, Optional +from datamaxi.api import API + + +class OpenInterest(API): + """Client to fetch CEX futures Open Interest data from DataMaxi+ API.""" + + def __init__(self, api_key=None, **kwargs: Any): + """Initialize the object. + + Args: + api_key (str): The DataMaxi+ API key + **kwargs: Keyword arguments used by `datamaxi.api.API`. + """ + super().__init__(api_key, **kwargs) + + def __call__(self, exchange: str, symbol: str) -> Dict[str, Any]: + """Latest Open Interest snapshot for a single futures symbol. + + `GET /api/v1/open-interest` + + Args: + exchange (str): Exchange (e.g. ``binance``). + symbol (str): Exchange-native API symbol (e.g. ``BTC-USDT``). + """ + return self.query( + "/api/v1/open-interest", {"exchange": exchange, "symbol": symbol} + ) + + def list(self, exchange: Optional[str] = None) -> Dict[str, Any]: + """List all (exchange, symbol) pairs currently reporting OI. + + `GET /api/v1/open-interest/list` + + Args: + exchange (str): Optional filter to one exchange. + """ + params = {} + if exchange is not None: + params["exchange"] = exchange + return self.query("/api/v1/open-interest/list", params) + + def overview( + self, + page: int = 1, + limit: int = 20, + key: str = "binance", + sort: str = "desc", + query: Optional[str] = None, + ) -> Dict[str, Any]: + """Paginated token × exchange OI matrix. + + `GET /api/v1/open-interest/overview` + + Args: + page (int): Page number. + limit (int): Page size. + key (str): Exchange to sort by. + sort (str): Sort direction (``asc`` or ``desc``). + query (str): Optional base-symbol search filter. + """ + if page < 1: + raise ValueError("page must be greater than 0") + if limit < 1: + raise ValueError("limit must be greater than 0") + if sort not in ("asc", "desc"): + raise ValueError("sort must be either asc or desc") + params = {"page": page, "limit": limit, "key": key, "sort": sort} + if query is not None: + params["query"] = query + return self.query("/api/v1/open-interest/overview", params) + + def summary(self, topN: int = 10) -> Dict[str, Any]: + """Top-line OI aggregates (total USD, top tokens, top exchanges). + + `GET /api/v1/open-interest/summary` + + Args: + topN (int): Top N tokens to return (1-30). + """ + if topN < 1 or topN > 30: + raise ValueError("topN must be between 1 and 30") + return self.query("/api/v1/open-interest/summary", {"topN": topN}) + + def history_aggregated( + self, + token_id: str, + interval: str = "1h", + from_: Optional[int] = None, + to: Optional[int] = None, + ) -> Dict[str, Any]: + """Per-exchange aggregated OI history for a single token. + + `GET /api/v1/open-interest/history-aggregated` + + Args: + token_id (str): Token id (e.g. ``bitcoin``). + interval (str): Aggregation interval (``5m``, ``15m``, ``1h``, + ``4h``, or ``1d``). + from_ (int): Start unix-ms. Default depends on interval + (7d for 1h, 30d for 4h, 1y for 1d). + to (int): End unix-ms. Defaults to now. + + Note: + ``from_`` is named with a trailing underscore because ``from`` + is a Python keyword. The wire-level query param remains ``from``. + """ + params: Dict[str, Any] = {"token_id": token_id, "interval": interval} + if from_ is not None: + params["from"] = from_ + if to is not None: + params["to"] = to + return self.query("/api/v1/open-interest/history-aggregated", params) diff --git a/pyproject.toml b/pyproject.toml index 2ff087a..9904679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "datamaxi" -version = "0.27.0" +version = "0.28.0" authors = [ { name="Bisonai", email="business@bisonai.com" }, ]