Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions datamaxi/datamaxi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from datamaxi.datamaxi.premium import Premium
from datamaxi.datamaxi.liquidation import Liquidation
from datamaxi.datamaxi.open_interest import OpenInterest
from datamaxi.datamaxi.margin_borrow import MarginBorrow
from datamaxi.datamaxi.index_price import IndexPrice
from datamaxi.datamaxi.cex_candle import CexCandle # used in documentation # noqa:F401
from datamaxi.datamaxi.cex_ticker import ( # used in documentation # noqa:F401
CexTicker,
Expand Down Expand Up @@ -52,3 +54,5 @@ def __init__(self, api_key=None, **kwargs: Any):
# (`datamaxi::generated::{Liquidation, OpenInterest}`).
self.liquidation = Liquidation(api_key, **kwargs)
self.open_interest = OpenInterest(api_key, **kwargs)
self.margin_borrow = MarginBorrow(api_key, **kwargs)
self.index_price = IndexPrice(api_key, **kwargs)
51 changes: 51 additions & 0 deletions datamaxi/datamaxi/index_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Any, Dict, Optional
from datamaxi.api import API
from datamaxi.lib.utils import check_required_parameter


class IndexPrice(API):
"""Client to fetch historical index price data from DataMaxi+ API."""

def __init__(self, api_key=None, **kwargs: Any):
"""Initialize index price client.

Args:
api_key (str): The DataMaxi+ API key
**kwargs: Keyword arguments used by `datamaxi.api.API`.
"""
super().__init__(api_key, **kwargs)

self.__module__ = __name__
self.__qualname__ = self.__class__.__qualname__

def __call__(
self,
asset: str,
from_: Optional[str] = None,
to: Optional[str] = None,
interval: str = "5m",
) -> Dict[str, Any]:
"""Fetch historical index price data for a single asset.

`GET /api/v1/index-price`

Args:
asset (str): Asset (e.g. ``BTC``).
from_ (str): Start time. Defaults to ``now - 1 month``.
to (str): End time. Defaults to ``now``.
interval (str): Sampling interval (default ``5m``).

Note:
``from_`` is named with a trailing underscore because ``from``
is a Python keyword. The wire-level query param remains ``from``.

Returns:
Historical index price response.
"""
check_required_parameter(asset, "asset")
return self.request_endpoint(
"index_price",
asset=asset,
interval=interval,
**{"from": from_, "to": to},
)
24 changes: 24 additions & 0 deletions datamaxi/datamaxi/liquidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ def map(
"liquidation_map", base=base, exchange=exchange, quote=quote
)

def stats(
self,
window: str = "1h",
exchange: Optional[str] = None,
min_volume_usd: Optional[float] = None,
) -> Dict[str, Any]:
"""Liquidation KPI stats over a rolling window.

`GET /api/v1/liquidation/stats`

Args:
window (str): Rolling window (``1h``, ``4h``, or ``24h``).
exchange (str): Optional exchange filter.
min_volume_usd (float): Minimum ``VolumeUsd`` filter.
"""
if window not in ("1h", "4h", "24h"):
raise ValueError("window must be one of 1h, 4h, or 24h")
return self.request_endpoint(
"liquidation_stats",
window=window,
exchange=exchange,
min_volume_usd=min_volume_usd,
)

def symbol_history(
self,
symbol: str,
Expand Down
33 changes: 33 additions & 0 deletions datamaxi/datamaxi/margin_borrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Any, Dict
from datamaxi.api import API
from datamaxi.lib.utils import check_required_parameter


class MarginBorrow(API):
"""Client to fetch margin borrow data from DataMaxi+ API."""

def __init__(self, api_key=None, **kwargs: Any):
"""Initialize margin borrow client.

Args:
api_key (str): The DataMaxi+ API key
**kwargs: Keyword arguments used by `datamaxi.api.API`.
"""
super().__init__(api_key, **kwargs)

self.__module__ = __name__
self.__qualname__ = self.__class__.__qualname__

def __call__(self, asset: str) -> Dict[str, Any]:
"""Fetch margin borrow data for a single asset.

`GET /api/v1/margin-borrow`

Args:
asset (str): Token base asset (e.g. ``BTC``).

Returns:
Margin borrow response.
"""
check_required_parameter(asset, "asset")
return self.request_endpoint("margin_borrow", asset=asset)
28 changes: 28 additions & 0 deletions docs/index-price.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Index Price

Historical index price time series for a single asset.

## Usage

```python
from datamaxi import Datamaxi

maxi = Datamaxi(api_key="YOUR_API_KEY")

data = maxi.index_price(
asset="BTC",
from_="now - 1 month",
to="now",
interval="5m",
)
```

## Notes

- `from_` is spelled with a trailing underscore because `from` is a Python
keyword; the wire-level query param remains `from`.

::: datamaxi.datamaxi.IndexPrice
options:
show_submodules: true
show_source: false
5 changes: 4 additions & 1 deletion docs/liquidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ feed = maxi.liquidation.feed(limit=100)
# Token x exchange liquidation heatmap over a rolling window
heatmap = maxi.liquidation.heatmap(window="1h", topN=10)

# Liquidation KPI stats over a rolling window
stats = maxi.liquidation.stats(window="1h")

# Coinglass-style liquidation map (price x leverage tier)
liq_map = maxi.liquidation.map(base="BTC", exchange="binance", quote="USDT")

Expand All @@ -33,7 +36,7 @@ history = maxi.liquidation.symbol_history(

## Notes

- `heatmap` accepts `window` of `1h`, `4h`, or `24h`; `topN` must be between 1 and 30.
- `heatmap` and `stats` accept `window` of `1h`, `4h`, or `24h`; `heatmap`'s `topN` must be between 1 and 30.
- `symbol_history` accepts `interval` of `5m`, `15m`, or `1h` and `window` of `24h`, `72h`, or `7d`.

::: datamaxi.datamaxi.Liquidation
Expand Down
18 changes: 18 additions & 0 deletions docs/margin-borrow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Margin Borrow

Margin borrow data for a single asset.

## Usage

```python
from datamaxi import Datamaxi

maxi = Datamaxi(api_key="YOUR_API_KEY")

data = maxi.margin_borrow(asset="BTC")
```

::: datamaxi.datamaxi.MarginBorrow
options:
show_submodules: true
show_source: false
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ nav:
- Funding Rate: funding-rate.md
- Liquidation: liquidation.md
- Open Interest: open-interest.md
- Margin Borrow: margin-borrow.md
- Index Price: index-price.md
- Premium: premium.md
- Forex: forex.md
- Naver Trend: naver-trend.md
Expand Down
31 changes: 7 additions & 24 deletions tests/test_endpoint_param_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,15 @@
# allow-listed, so to exempt a whole endpoint every one of its params must be
# named here (an empty dict exempts nothing).
#
# NOTE FOR HUMAN REVIEW: the four endpoints below have NO client method in the
# SDK at all. Exposing them is not a param forward-through — it needs a brand
# new client method (name, return-shape handling, docs, dedicated tests), which
# is a product/design decision out of scope for this param-coverage audit.
# Tracked for follow-up; see PR body.
# NOTE FOR HUMAN REVIEW: #126 exposed index_price, margin_borrow, and
# liquidation_stats with dedicated client methods (they are no longer here).
# listings_historical remains intentionally SDK-excluded — its param is
# allow-listed below with that rationale.
_ALLOWLIST = {
# No client method — needs a new OHLC-style method + response shaping.
"index_price": {
"asset": "no client method yet — needs new Index-Price client",
"from": "no client method yet — needs new Index-Price client",
"to": "no client method yet — needs new Index-Price client",
"interval": "no client method yet — needs new Index-Price client",
},
# No client method — needs a new Margin-Borrow client.
"margin_borrow": {
"asset": "no client method yet — needs new Margin-Borrow client",
},
# No client method — needs a new Liquidation.stats() method + shaping.
"liquidation_stats": {
"window": "no client method yet — needs new Liquidation.stats()",
"exchange": "no client method yet — needs new Liquidation.stats()",
"min_volume_usd": "no client method yet — needs new Liquidation.stats()",
},
# No client method — needs a new Listings.historical() method + shaping.
# Intentionally SDK-excluded (#126): not surfaced in the public data-api.
"listings_historical": {
"refresh": "no client method yet — needs new Listings.historical()",
"refresh": "intentionally SDK-excluded — not surfaced in the public "
"data-api (see #126)",
},
}

Expand Down
46 changes: 46 additions & 0 deletions tests/test_index_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Local (mocked) tests for the IndexPrice client."""

import re
import responses
import pytest
from urllib.parse import urlparse, parse_qs

from datamaxi.datamaxi.index_price import IndexPrice
from datamaxi.error import ParameterRequiredError
from tests.util import mock_http_response

BASE_URL = "https://api.datamaxiplus.com"


def _ip():
return IndexPrice(api_key="key", base_url=BASE_URL)


def _qs(call):
return parse_qs(urlparse(call.request.url).query)


@mock_http_response(responses.GET, "/api/v1/index-price", {"data": []})
def test_index_price_returns_dict():
assert _ip()(asset="BTC") == {"data": []}


@responses.activate
def test_index_price_forwards_params_with_from_to():
responses.add(
responses.GET,
re.compile(".*/api/v1/index-price.*"),
json={"data": []},
status=200,
)
_ip()(asset="BTC", from_="2024-01-01", to="2024-02-01", interval="15m")
qs = _qs(responses.calls[0])
assert qs["asset"] == ["BTC"]
assert qs["from"] == ["2024-01-01"]
assert qs["to"] == ["2024-02-01"]
assert qs["interval"] == ["15m"]


def test_index_price_missing_asset_raises():
with pytest.raises(ParameterRequiredError):
_ip()(asset="")
25 changes: 25 additions & 0 deletions tests/test_liquidation_open_interest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ def test_liquidation_feed_forwards_new_params():
assert qs["min_volume_usd"] == ["1000.0"]


@mock_http_response(responses.GET, "/api/v1/liquidation/stats", {"data": {}})
def test_liquidation_stats_returns_dict():
assert _liq().stats(window="1h") == {"data": {}}


@responses.activate
def test_liquidation_stats_forwards_params():
responses.add(
responses.GET,
re.compile(".*/api/v1/liquidation/stats.*"),
json={"data": {}},
status=200,
)
_liq().stats(window="4h", exchange="binance", min_volume_usd=1000.0)
qs = _qs(responses.calls[0])
assert qs["window"] == ["4h"]
assert qs["exchange"] == ["binance"]
assert qs["min_volume_usd"] == ["1000.0"]


def test_liquidation_stats_invalid_window_raises_value_error():
with pytest.raises(ValueError):
_liq().stats(window="7d")


def test_liquidation_invalid_limit_raises_value_error():
with pytest.raises(ValueError):
_liq()(exchange="binance", symbol="BTC-USDT", limit=0)
Expand Down
43 changes: 43 additions & 0 deletions tests/test_margin_borrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Local (mocked) tests for the MarginBorrow client."""

import re
import responses
import pytest
from urllib.parse import urlparse, parse_qs

from datamaxi.datamaxi.margin_borrow import MarginBorrow
from datamaxi.error import ParameterRequiredError
from tests.util import mock_http_response

BASE_URL = "https://api.datamaxiplus.com"


def _mb():
return MarginBorrow(api_key="key", base_url=BASE_URL)


def _qs(call):
return parse_qs(urlparse(call.request.url).query)


@mock_http_response(responses.GET, "/api/v1/margin-borrow", {"data": {}})
def test_margin_borrow_returns_dict():
assert _mb()(asset="BTC") == {"data": {}}


@responses.activate
def test_margin_borrow_forwards_asset():
responses.add(
responses.GET,
re.compile(".*/api/v1/margin-borrow.*"),
json={"data": {}},
status=200,
)
_mb()(asset="BTC")
qs = _qs(responses.calls[0])
assert qs["asset"] == ["BTC"]


def test_margin_borrow_missing_asset_raises():
with pytest.raises(ParameterRequiredError):
_mb()(asset="")
Loading