Skip to content

Commit 0024416

Browse files
feat: expose index_price, margin_borrow, liquidation.stats SDK methods (#126) (#127)
* feat(liquidation): add stats() for liquidation KPI stats (#126) * feat: expose margin_borrow + index_price top-level clients (#126) * test(audit): drop now-exposed allowlist entries; keep listings_historical excluded (#126) * style: drop redundant wiring comment in Datamaxi.__init__ (#126)
1 parent 18065de commit 0024416

12 files changed

Lines changed: 285 additions & 25 deletions

datamaxi/datamaxi/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from datamaxi.datamaxi.premium import Premium
77
from datamaxi.datamaxi.liquidation import Liquidation
88
from datamaxi.datamaxi.open_interest import OpenInterest
9+
from datamaxi.datamaxi.margin_borrow import MarginBorrow
10+
from datamaxi.datamaxi.index_price import IndexPrice
911
from datamaxi.datamaxi.cex_candle import CexCandle # used in documentation # noqa:F401
1012
from datamaxi.datamaxi.cex_ticker import ( # used in documentation # noqa:F401
1113
CexTicker,
@@ -52,3 +54,5 @@ def __init__(self, api_key=None, **kwargs: Any):
5254
# (`datamaxi::generated::{Liquidation, OpenInterest}`).
5355
self.liquidation = Liquidation(api_key, **kwargs)
5456
self.open_interest = OpenInterest(api_key, **kwargs)
57+
self.margin_borrow = MarginBorrow(api_key, **kwargs)
58+
self.index_price = IndexPrice(api_key, **kwargs)

datamaxi/datamaxi/index_price.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Any, Dict, Optional
2+
from datamaxi.api import API
3+
from datamaxi.lib.utils import check_required_parameter
4+
5+
6+
class IndexPrice(API):
7+
"""Client to fetch historical index price data from DataMaxi+ API."""
8+
9+
def __init__(self, api_key=None, **kwargs: Any):
10+
"""Initialize index price client.
11+
12+
Args:
13+
api_key (str): The DataMaxi+ API key
14+
**kwargs: Keyword arguments used by `datamaxi.api.API`.
15+
"""
16+
super().__init__(api_key, **kwargs)
17+
18+
self.__module__ = __name__
19+
self.__qualname__ = self.__class__.__qualname__
20+
21+
def __call__(
22+
self,
23+
asset: str,
24+
from_: Optional[str] = None,
25+
to: Optional[str] = None,
26+
interval: str = "5m",
27+
) -> Dict[str, Any]:
28+
"""Fetch historical index price data for a single asset.
29+
30+
`GET /api/v1/index-price`
31+
32+
Args:
33+
asset (str): Asset (e.g. ``BTC``).
34+
from_ (str): Start time. Defaults to ``now - 1 month``.
35+
to (str): End time. Defaults to ``now``.
36+
interval (str): Sampling interval (default ``5m``).
37+
38+
Note:
39+
``from_`` is named with a trailing underscore because ``from``
40+
is a Python keyword. The wire-level query param remains ``from``.
41+
42+
Returns:
43+
Historical index price response.
44+
"""
45+
check_required_parameter(asset, "asset")
46+
return self.request_endpoint(
47+
"index_price",
48+
asset=asset,
49+
interval=interval,
50+
**{"from": from_, "to": to},
51+
)

datamaxi/datamaxi/liquidation.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,30 @@ def map(
101101
"liquidation_map", base=base, exchange=exchange, quote=quote
102102
)
103103

104+
def stats(
105+
self,
106+
window: str = "1h",
107+
exchange: Optional[str] = None,
108+
min_volume_usd: Optional[float] = None,
109+
) -> Dict[str, Any]:
110+
"""Liquidation KPI stats over a rolling window.
111+
112+
`GET /api/v1/liquidation/stats`
113+
114+
Args:
115+
window (str): Rolling window (``1h``, ``4h``, or ``24h``).
116+
exchange (str): Optional exchange filter.
117+
min_volume_usd (float): Minimum ``VolumeUsd`` filter.
118+
"""
119+
if window not in ("1h", "4h", "24h"):
120+
raise ValueError("window must be one of 1h, 4h, or 24h")
121+
return self.request_endpoint(
122+
"liquidation_stats",
123+
window=window,
124+
exchange=exchange,
125+
min_volume_usd=min_volume_usd,
126+
)
127+
104128
def symbol_history(
105129
self,
106130
symbol: str,

datamaxi/datamaxi/margin_borrow.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Any, Dict
2+
from datamaxi.api import API
3+
from datamaxi.lib.utils import check_required_parameter
4+
5+
6+
class MarginBorrow(API):
7+
"""Client to fetch margin borrow data from DataMaxi+ API."""
8+
9+
def __init__(self, api_key=None, **kwargs: Any):
10+
"""Initialize margin borrow client.
11+
12+
Args:
13+
api_key (str): The DataMaxi+ API key
14+
**kwargs: Keyword arguments used by `datamaxi.api.API`.
15+
"""
16+
super().__init__(api_key, **kwargs)
17+
18+
self.__module__ = __name__
19+
self.__qualname__ = self.__class__.__qualname__
20+
21+
def __call__(self, asset: str) -> Dict[str, Any]:
22+
"""Fetch margin borrow data for a single asset.
23+
24+
`GET /api/v1/margin-borrow`
25+
26+
Args:
27+
asset (str): Token base asset (e.g. ``BTC``).
28+
29+
Returns:
30+
Margin borrow response.
31+
"""
32+
check_required_parameter(asset, "asset")
33+
return self.request_endpoint("margin_borrow", asset=asset)

docs/index-price.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Index Price
2+
3+
Historical index price time series for a single asset.
4+
5+
## Usage
6+
7+
```python
8+
from datamaxi import Datamaxi
9+
10+
maxi = Datamaxi(api_key="YOUR_API_KEY")
11+
12+
data = maxi.index_price(
13+
asset="BTC",
14+
from_="now - 1 month",
15+
to="now",
16+
interval="5m",
17+
)
18+
```
19+
20+
## Notes
21+
22+
- `from_` is spelled with a trailing underscore because `from` is a Python
23+
keyword; the wire-level query param remains `from`.
24+
25+
::: datamaxi.datamaxi.IndexPrice
26+
options:
27+
show_submodules: true
28+
show_source: false

docs/liquidation.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ feed = maxi.liquidation.feed(limit=100)
1818
# Token x exchange liquidation heatmap over a rolling window
1919
heatmap = maxi.liquidation.heatmap(window="1h", topN=10)
2020

21+
# Liquidation KPI stats over a rolling window
22+
stats = maxi.liquidation.stats(window="1h")
23+
2124
# Coinglass-style liquidation map (price x leverage tier)
2225
liq_map = maxi.liquidation.map(base="BTC", exchange="binance", quote="USDT")
2326

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

3437
## Notes
3538

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

3942
::: datamaxi.datamaxi.Liquidation

docs/margin-borrow.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Margin Borrow
2+
3+
Margin borrow data for a single asset.
4+
5+
## Usage
6+
7+
```python
8+
from datamaxi import Datamaxi
9+
10+
maxi = Datamaxi(api_key="YOUR_API_KEY")
11+
12+
data = maxi.margin_borrow(asset="BTC")
13+
```
14+
15+
::: datamaxi.datamaxi.MarginBorrow
16+
options:
17+
show_submodules: true
18+
show_source: false

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ nav:
4242
- Funding Rate: funding-rate.md
4343
- Liquidation: liquidation.md
4444
- Open Interest: open-interest.md
45+
- Margin Borrow: margin-borrow.md
46+
- Index Price: index-price.md
4547
- Premium: premium.md
4648
- Forex: forex.md
4749
- Naver Trend: naver-trend.md

tests/test_endpoint_param_coverage.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,15 @@
3636
# allow-listed, so to exempt a whole endpoint every one of its params must be
3737
# named here (an empty dict exempts nothing).
3838
#
39-
# NOTE FOR HUMAN REVIEW: the four endpoints below have NO client method in the
40-
# SDK at all. Exposing them is not a param forward-through — it needs a brand
41-
# new client method (name, return-shape handling, docs, dedicated tests), which
42-
# is a product/design decision out of scope for this param-coverage audit.
43-
# Tracked for follow-up; see PR body.
39+
# NOTE FOR HUMAN REVIEW: #126 exposed index_price, margin_borrow, and
40+
# liquidation_stats with dedicated client methods (they are no longer here).
41+
# listings_historical remains intentionally SDK-excluded — its param is
42+
# allow-listed below with that rationale.
4443
_ALLOWLIST = {
45-
# No client method — needs a new OHLC-style method + response shaping.
46-
"index_price": {
47-
"asset": "no client method yet — needs new Index-Price client",
48-
"from": "no client method yet — needs new Index-Price client",
49-
"to": "no client method yet — needs new Index-Price client",
50-
"interval": "no client method yet — needs new Index-Price client",
51-
},
52-
# No client method — needs a new Margin-Borrow client.
53-
"margin_borrow": {
54-
"asset": "no client method yet — needs new Margin-Borrow client",
55-
},
56-
# No client method — needs a new Liquidation.stats() method + shaping.
57-
"liquidation_stats": {
58-
"window": "no client method yet — needs new Liquidation.stats()",
59-
"exchange": "no client method yet — needs new Liquidation.stats()",
60-
"min_volume_usd": "no client method yet — needs new Liquidation.stats()",
61-
},
62-
# No client method — needs a new Listings.historical() method + shaping.
44+
# Intentionally SDK-excluded (#126): not surfaced in the public data-api.
6345
"listings_historical": {
64-
"refresh": "no client method yet — needs new Listings.historical()",
46+
"refresh": "intentionally SDK-excluded — not surfaced in the public "
47+
"data-api (see #126)",
6548
},
6649
}
6750

tests/test_index_price.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Local (mocked) tests for the IndexPrice client."""
2+
3+
import re
4+
import responses
5+
import pytest
6+
from urllib.parse import urlparse, parse_qs
7+
8+
from datamaxi.datamaxi.index_price import IndexPrice
9+
from datamaxi.error import ParameterRequiredError
10+
from tests.util import mock_http_response
11+
12+
BASE_URL = "https://api.datamaxiplus.com"
13+
14+
15+
def _ip():
16+
return IndexPrice(api_key="key", base_url=BASE_URL)
17+
18+
19+
def _qs(call):
20+
return parse_qs(urlparse(call.request.url).query)
21+
22+
23+
@mock_http_response(responses.GET, "/api/v1/index-price", {"data": []})
24+
def test_index_price_returns_dict():
25+
assert _ip()(asset="BTC") == {"data": []}
26+
27+
28+
@responses.activate
29+
def test_index_price_forwards_params_with_from_to():
30+
responses.add(
31+
responses.GET,
32+
re.compile(".*/api/v1/index-price.*"),
33+
json={"data": []},
34+
status=200,
35+
)
36+
_ip()(asset="BTC", from_="2024-01-01", to="2024-02-01", interval="15m")
37+
qs = _qs(responses.calls[0])
38+
assert qs["asset"] == ["BTC"]
39+
assert qs["from"] == ["2024-01-01"]
40+
assert qs["to"] == ["2024-02-01"]
41+
assert qs["interval"] == ["15m"]
42+
43+
44+
def test_index_price_missing_asset_raises():
45+
with pytest.raises(ParameterRequiredError):
46+
_ip()(asset="")

0 commit comments

Comments
 (0)