debyko-ccxt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- debyko_ccxt-0.1.0/LICENSE +21 -0
- debyko_ccxt-0.1.0/MANIFEST.in +2 -0
- debyko_ccxt-0.1.0/PKG-INFO +83 -0
- debyko_ccxt-0.1.0/README.md +64 -0
- debyko_ccxt-0.1.0/pyproject.toml +36 -0
- debyko_ccxt-0.1.0/setup.cfg +4 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt/__init__.py +13 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt/client.py +316 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt/mapping.py +229 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt/transport.py +114 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt.egg-info/PKG-INFO +83 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt.egg-info/SOURCES.txt +23 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt.egg-info/dependency_links.txt +1 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt.egg-info/requires.txt +7 -0
- debyko_ccxt-0.1.0/src/debyko_ccxt.egg-info/top_level.txt +1 -0
- debyko_ccxt-0.1.0/tests/conftest.py +48 -0
- debyko_ccxt-0.1.0/tests/fixtures/instruments.json +405 -0
- debyko_ccxt-0.1.0/tests/fixtures/liquidations.json +1 -0
- debyko_ccxt-0.1.0/tests/fixtures/oi.json +204 -0
- debyko_ccxt-0.1.0/tests/fixtures/okx-instruments.json +377 -0
- debyko_ccxt-0.1.0/tests/fixtures/okx-oi.json +204 -0
- debyko_ccxt-0.1.0/tests/fixtures/settlements.json +129 -0
- debyko_ccxt-0.1.0/tests/fixtures/snapshots.json +76 -0
- debyko_ccxt-0.1.0/tests/test_transport.py +163 -0
- debyko_ccxt-0.1.0/tests/test_unified.py +412 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DEBYKO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debyko-ccxt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only CCXT-compatible funding, open interest and liquidations from DEBYKO
|
|
5
|
+
Author: DEBYKO
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/debyko/debyko-ccxt
|
|
8
|
+
Project-URL: Issues, https://github.com/debyko/debyko-ccxt/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
15
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
17
|
+
Requires-Dist: ccxt==4.5.92; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# debyko-ccxt
|
|
21
|
+
|
|
22
|
+
Four synchronous, read-only methods with [CCXT unified structures](https://github.com/ccxt/ccxt/wiki/Manual), backed by [DEBYKO](https://docs.debyko.com/): funding settlements, current funding rates, sampled open interest and liquidation events. MIT. Python ≥3.10; **zero runtime dependencies**. Contract tests use CCXT 4.5.92.
|
|
23
|
+
|
|
24
|
+
## Keep CCXT for orders; swap one line for history
|
|
25
|
+
|
|
26
|
+
After the owner publishes: `pip install debyko-ccxt`. Before publication: `pip install git+https://github.com/debyko/debyko-ccxt.git@main`.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
import ccxt
|
|
31
|
+
from debyko_ccxt import DebykoCCXT
|
|
32
|
+
|
|
33
|
+
orders = ccxt.binance() # retain your existing exchange/order configuration
|
|
34
|
+
history = DebykoCCXT(api_key=os.environ["DEBYKO_API_KEY"], venue="BINANCE-USDM")
|
|
35
|
+
symbol = "BTC/USDT:USDT"
|
|
36
|
+
since = orders.parse8601("2026-11-08T00:00:00Z")
|
|
37
|
+
funding = history.fetchFundingRateHistory(symbol, since, limit=100)
|
|
38
|
+
oi = history.fetchOpenInterestHistory(symbol, "5m", since, limit=100)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
CCXT is needed by your order code, not by this package. There are no order, balance or withdrawal methods.
|
|
42
|
+
|
|
43
|
+
| Method (snake_case aliases also available) | DEBYKO endpoint |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `fetchFundingRateHistory(symbol, since=None, limit=None, params=None)` | `GET /v2/funding/settlements` |
|
|
46
|
+
| `fetchFundingRates(symbols=None, params=None)` | Read-only `POST /v2/snapshots` |
|
|
47
|
+
| `fetchOpenInterestHistory(symbol, timeframe="5m", since=None, limit=None, params=None)` | Read-only `POST /v2/snapshots/history` |
|
|
48
|
+
| `fetchLiquidations(symbol, since=None, limit=None, params=None)` | Read-only `POST /v2/liquidations` |
|
|
49
|
+
|
|
50
|
+
Native perpetual identities come from `GET /v2/instruments`, fetched per call. Unified symbols keep quote/settlement distinctions, e.g. `BTC/USDT:USDT` versus `BTC/USDC:USDC`. An unknown settlement identity or ambiguous mapping raises `BadSymbol`; it is never guessed.
|
|
51
|
+
|
|
52
|
+
`since` is inclusive Unix milliseconds; `params={"until": ...}` sets the inclusive historical end. Default limit: 1,000 rows. Without `since`, funding uses the API's default settlement window (currently seven days), OI uses the latest requested number of buckets, liquidations use the last hour. Current funding rejects `until`. OI intervals: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `1d`. Pagination/chunking is automatic, capped at **100 HTTP requests per method call**, including registry reads; change via `params={"max_requests": ...}`. Budget exhaustion raises instead of returning a silent partial answer. HTTP timeout defaults to 20 seconds.
|
|
53
|
+
|
|
54
|
+
## Structure and units
|
|
55
|
+
|
|
56
|
+
- Numbers are floats; timestamps are integer milliseconds and `datetime` is UTC ISO 8601. Every result's `info` retains DEBYKO's `venue_ts` and `status`, with source metadata. Funding history uses settlement time; OI uses `bucket_end`, retaining the actual source time separately in `info`.
|
|
57
|
+
- Funding is the published **fraction for the native interval**, never an 8h-normalized rate. `fundingTimestamp` is the upcoming payment time; a subsequent period/rate is not invented.
|
|
58
|
+
- OI amount and liquidation `contracts` are contracts, not base currency. Conversion uses the registry's contract size; base and quote values stay distinct. No historical price is invented to fill `openInterestValue`. Unknown fields are `None`.
|
|
59
|
+
- Non-present funding/OI retain their status and have `None` values, not zero. OI is an as-of sample, not an exchange aggregate. Liquidation side is the forced order's `buy`/`sell`, not the liquidated position's side. Event gaps remain in `info`; an empty window with known gaps raises `DataUnavailable`, as `[]` would hide them.
|
|
60
|
+
- These are CCXT-shaped answers, not an Exchange subclass or CCXT's exception hierarchy. API refusals raise `DebykoError` with `.status` and `.response`; there are no automatic retries of 429/5xx and no exchange fallback.
|
|
61
|
+
|
|
62
|
+
## Authentication / explicit x402
|
|
63
|
+
|
|
64
|
+
`DebykoCCXT()` reads `DEBYKO_API_KEY`. Keys go only in the Authorization header, never URLs. Redirects are refused. No key means an authentication error **before any network call** by default.
|
|
65
|
+
|
|
66
|
+
Keyless access requires `DebykoCCXT(api_key="", x402=True, payment_handler=sign)`. `sign(challenge)` must validate an **x402 v2** challenge, enforce your network/asset/payee/spending policy, and return the encoded `PAYMENT-SIGNATURE` header. The package retries that request once. It contains no wallet or signer, never pays implicitly, and never falls back from an invalid key to payment. Registry/page requests can each require payment: budget these in your signer. No real payments are made by tests.
|
|
67
|
+
|
|
68
|
+
## Which exchange windows does this avoid?
|
|
69
|
+
|
|
70
|
+
The adapter reads DEBYKO's stored observations instead of the venue's historical endpoint. It avoids the **native API** constraints below only where DEBYKO actually retained the requested data and your plan permits it. It does not extend DEBYKO retention, recover pre-collection history, or guarantee more history than the exchange has today.
|
|
71
|
+
|
|
72
|
+
| Native endpoint constraint (checked 8 Nov 2026) | When using this adapter |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| [OKX funding history: up to three months](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history) | Read retained DEBYKO settlements, independent of OKX's rolling window. No promise of >3 months currently recorded. |
|
|
75
|
+
| [Binance USD-M OI statistics: latest one month, ≤500 per call](https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest-Statistics) | Read retained DEBYKO OI samples with automatic chunking; sample semantics differ from native statistics. |
|
|
76
|
+
| [Bybit funding history: ≤200 per call](https://bybit-exchange.github.io/docs/v5/market/history-fund-rate) | Automatic DEBYKO settlement pagination. This is a page limit, not a claimed retention limit. |
|
|
77
|
+
|
|
78
|
+
## Develop / publish
|
|
79
|
+
|
|
80
|
+
`pip install -e '.[dev]'`, then `ruff check .`, `ruff format --check .`, `pytest`, `python -m build`, `twine check dist/*`.
|
|
81
|
+
Fixtures are recorded public market responses from 8 Nov 2026, with no credentials. Every test blocks external connections. CI tests Python 3.10/3.12/3.14 and installs the built wheel without dependencies.
|
|
82
|
+
|
|
83
|
+
**Not published automatically.** The owner configures PyPI Trusted Publishing for this repository, workflow `publish.yml`, environment `pypi` (prefer a required reviewer), then manually dispatches **Publish to PyPI** on `main`. The workflow reruns CI and publishes its exact built artifacts, without repository API tokens. It does not run on pushes or tags. Python first; TypeScript is deferred.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# debyko-ccxt
|
|
2
|
+
|
|
3
|
+
Four synchronous, read-only methods with [CCXT unified structures](https://github.com/ccxt/ccxt/wiki/Manual), backed by [DEBYKO](https://docs.debyko.com/): funding settlements, current funding rates, sampled open interest and liquidation events. MIT. Python ≥3.10; **zero runtime dependencies**. Contract tests use CCXT 4.5.92.
|
|
4
|
+
|
|
5
|
+
## Keep CCXT for orders; swap one line for history
|
|
6
|
+
|
|
7
|
+
After the owner publishes: `pip install debyko-ccxt`. Before publication: `pip install git+https://github.com/debyko/debyko-ccxt.git@main`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import os
|
|
11
|
+
import ccxt
|
|
12
|
+
from debyko_ccxt import DebykoCCXT
|
|
13
|
+
|
|
14
|
+
orders = ccxt.binance() # retain your existing exchange/order configuration
|
|
15
|
+
history = DebykoCCXT(api_key=os.environ["DEBYKO_API_KEY"], venue="BINANCE-USDM")
|
|
16
|
+
symbol = "BTC/USDT:USDT"
|
|
17
|
+
since = orders.parse8601("2026-11-08T00:00:00Z")
|
|
18
|
+
funding = history.fetchFundingRateHistory(symbol, since, limit=100)
|
|
19
|
+
oi = history.fetchOpenInterestHistory(symbol, "5m", since, limit=100)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
CCXT is needed by your order code, not by this package. There are no order, balance or withdrawal methods.
|
|
23
|
+
|
|
24
|
+
| Method (snake_case aliases also available) | DEBYKO endpoint |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `fetchFundingRateHistory(symbol, since=None, limit=None, params=None)` | `GET /v2/funding/settlements` |
|
|
27
|
+
| `fetchFundingRates(symbols=None, params=None)` | Read-only `POST /v2/snapshots` |
|
|
28
|
+
| `fetchOpenInterestHistory(symbol, timeframe="5m", since=None, limit=None, params=None)` | Read-only `POST /v2/snapshots/history` |
|
|
29
|
+
| `fetchLiquidations(symbol, since=None, limit=None, params=None)` | Read-only `POST /v2/liquidations` |
|
|
30
|
+
|
|
31
|
+
Native perpetual identities come from `GET /v2/instruments`, fetched per call. Unified symbols keep quote/settlement distinctions, e.g. `BTC/USDT:USDT` versus `BTC/USDC:USDC`. An unknown settlement identity or ambiguous mapping raises `BadSymbol`; it is never guessed.
|
|
32
|
+
|
|
33
|
+
`since` is inclusive Unix milliseconds; `params={"until": ...}` sets the inclusive historical end. Default limit: 1,000 rows. Without `since`, funding uses the API's default settlement window (currently seven days), OI uses the latest requested number of buckets, liquidations use the last hour. Current funding rejects `until`. OI intervals: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `1d`. Pagination/chunking is automatic, capped at **100 HTTP requests per method call**, including registry reads; change via `params={"max_requests": ...}`. Budget exhaustion raises instead of returning a silent partial answer. HTTP timeout defaults to 20 seconds.
|
|
34
|
+
|
|
35
|
+
## Structure and units
|
|
36
|
+
|
|
37
|
+
- Numbers are floats; timestamps are integer milliseconds and `datetime` is UTC ISO 8601. Every result's `info` retains DEBYKO's `venue_ts` and `status`, with source metadata. Funding history uses settlement time; OI uses `bucket_end`, retaining the actual source time separately in `info`.
|
|
38
|
+
- Funding is the published **fraction for the native interval**, never an 8h-normalized rate. `fundingTimestamp` is the upcoming payment time; a subsequent period/rate is not invented.
|
|
39
|
+
- OI amount and liquidation `contracts` are contracts, not base currency. Conversion uses the registry's contract size; base and quote values stay distinct. No historical price is invented to fill `openInterestValue`. Unknown fields are `None`.
|
|
40
|
+
- Non-present funding/OI retain their status and have `None` values, not zero. OI is an as-of sample, not an exchange aggregate. Liquidation side is the forced order's `buy`/`sell`, not the liquidated position's side. Event gaps remain in `info`; an empty window with known gaps raises `DataUnavailable`, as `[]` would hide them.
|
|
41
|
+
- These are CCXT-shaped answers, not an Exchange subclass or CCXT's exception hierarchy. API refusals raise `DebykoError` with `.status` and `.response`; there are no automatic retries of 429/5xx and no exchange fallback.
|
|
42
|
+
|
|
43
|
+
## Authentication / explicit x402
|
|
44
|
+
|
|
45
|
+
`DebykoCCXT()` reads `DEBYKO_API_KEY`. Keys go only in the Authorization header, never URLs. Redirects are refused. No key means an authentication error **before any network call** by default.
|
|
46
|
+
|
|
47
|
+
Keyless access requires `DebykoCCXT(api_key="", x402=True, payment_handler=sign)`. `sign(challenge)` must validate an **x402 v2** challenge, enforce your network/asset/payee/spending policy, and return the encoded `PAYMENT-SIGNATURE` header. The package retries that request once. It contains no wallet or signer, never pays implicitly, and never falls back from an invalid key to payment. Registry/page requests can each require payment: budget these in your signer. No real payments are made by tests.
|
|
48
|
+
|
|
49
|
+
## Which exchange windows does this avoid?
|
|
50
|
+
|
|
51
|
+
The adapter reads DEBYKO's stored observations instead of the venue's historical endpoint. It avoids the **native API** constraints below only where DEBYKO actually retained the requested data and your plan permits it. It does not extend DEBYKO retention, recover pre-collection history, or guarantee more history than the exchange has today.
|
|
52
|
+
|
|
53
|
+
| Native endpoint constraint (checked 8 Nov 2026) | When using this adapter |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| [OKX funding history: up to three months](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history) | Read retained DEBYKO settlements, independent of OKX's rolling window. No promise of >3 months currently recorded. |
|
|
56
|
+
| [Binance USD-M OI statistics: latest one month, ≤500 per call](https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest-Statistics) | Read retained DEBYKO OI samples with automatic chunking; sample semantics differ from native statistics. |
|
|
57
|
+
| [Bybit funding history: ≤200 per call](https://bybit-exchange.github.io/docs/v5/market/history-fund-rate) | Automatic DEBYKO settlement pagination. This is a page limit, not a claimed retention limit. |
|
|
58
|
+
|
|
59
|
+
## Develop / publish
|
|
60
|
+
|
|
61
|
+
`pip install -e '.[dev]'`, then `ruff check .`, `ruff format --check .`, `pytest`, `python -m build`, `twine check dist/*`.
|
|
62
|
+
Fixtures are recorded public market responses from 8 Nov 2026, with no credentials. Every test blocks external connections. CI tests Python 3.10/3.12/3.14 and installs the built wheel without dependencies.
|
|
63
|
+
|
|
64
|
+
**Not published automatically.** The owner configures PyPI Trusted Publishing for this repository, workflow `publish.yml`, environment `pypi` (prefer a required reviewer), then manually dispatches **Publish to PyPI** on `main`. The workflow reruns CI and publishes its exact built artifacts, without repository API tokens. It does not run on pushes or tags. Python first; TypeScript is deferred.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "debyko-ccxt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only CCXT-compatible funding, open interest and liquidations from DEBYKO"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "DEBYKO"}]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/debyko/debyko-ccxt"
|
|
18
|
+
Issues = "https://github.com/debyko/debyko-ccxt/issues"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=8", "ruff>=0.8", "build>=1.2", "twine>=6", "ccxt==4.5.92"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
addopts = "-ra"
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
target-version = "py310"
|
|
32
|
+
line-length = 100
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
36
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Small read-only CCXT-shaped adapter. There is no runtime CCXT dependency."""
|
|
2
|
+
|
|
3
|
+
from .client import DebykoCCXT
|
|
4
|
+
from .transport import AuthenticationError, BadResponse, BadSymbol, DataUnavailable, DebykoError
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DebykoCCXT",
|
|
8
|
+
"DebykoError",
|
|
9
|
+
"AuthenticationError",
|
|
10
|
+
"BadResponse",
|
|
11
|
+
"BadSymbol",
|
|
12
|
+
"DataUnavailable",
|
|
13
|
+
]
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Four read-only methods; snake_case and CCXT's camelCase aliases."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from . import mapping as m
|
|
8
|
+
from .transport import BadResponse, BadSymbol, DataUnavailable, Rest
|
|
9
|
+
|
|
10
|
+
TIMEFRAMES = {
|
|
11
|
+
"1m": 60_000,
|
|
12
|
+
"5m": 300_000,
|
|
13
|
+
"15m": 900_000,
|
|
14
|
+
"30m": 1_800_000,
|
|
15
|
+
"1h": 3_600_000,
|
|
16
|
+
"4h": 14_400_000,
|
|
17
|
+
"12h": 43_200_000,
|
|
18
|
+
"1d": 86_400_000,
|
|
19
|
+
}
|
|
20
|
+
SYMBOL = re.compile(r"^[A-Z0-9._-]+/[A-Z0-9._-]+:[A-Z0-9._-]+$")
|
|
21
|
+
DAY = 86_400_000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def integer(value, name, minimum=0):
|
|
25
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
26
|
+
raise ValueError(f"{name} must be an integer >= {minimum}")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _symbol(value):
|
|
31
|
+
if not isinstance(value, str) or not SYMBOL.fullmatch(value):
|
|
32
|
+
raise BadSymbol("Use a unified perpetual symbol, e.g. BTC/USDT:USDT")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DebykoCCXT:
|
|
37
|
+
def __init__(
|
|
38
|
+
self, api_key=None, venue="BINANCE-USDM", *, x402=False, payment_handler=None, timeout=20
|
|
39
|
+
):
|
|
40
|
+
if not isinstance(venue, str) or not re.fullmatch(r"[A-Z0-9-]+", venue):
|
|
41
|
+
raise ValueError("venue must be a DEBYKO venue code")
|
|
42
|
+
if (
|
|
43
|
+
isinstance(timeout, bool)
|
|
44
|
+
or not isinstance(timeout, (int, float))
|
|
45
|
+
or not 0 < timeout <= 120
|
|
46
|
+
):
|
|
47
|
+
raise ValueError("timeout must be in (0, 120] seconds")
|
|
48
|
+
self.venue = venue
|
|
49
|
+
self._rest = Rest(api_key, x402=x402, payment_handler=payment_handler, timeout=timeout)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _now():
|
|
53
|
+
return time.time_ns() // 1_000_000
|
|
54
|
+
|
|
55
|
+
def _call(self, params):
|
|
56
|
+
params = {} if params is None else dict(params)
|
|
57
|
+
if set(params) - {"until", "max_requests"}:
|
|
58
|
+
raise ValueError("Supported params: until (inclusive ms), max_requests")
|
|
59
|
+
until = integer(params.get("until", self._now()), "until")
|
|
60
|
+
maximum = integer(params.get("max_requests", 100), "max_requests", 1)
|
|
61
|
+
count = 0
|
|
62
|
+
|
|
63
|
+
def call(method, path, **kwargs):
|
|
64
|
+
nonlocal count
|
|
65
|
+
count += 1
|
|
66
|
+
if count > maximum:
|
|
67
|
+
raise DataUnavailable(
|
|
68
|
+
"Request budget reached; narrow the window or increase max_requests"
|
|
69
|
+
)
|
|
70
|
+
return self._rest.request(method, path, **kwargs)
|
|
71
|
+
|
|
72
|
+
return call, until
|
|
73
|
+
|
|
74
|
+
def _markets(self, call, requested):
|
|
75
|
+
"""Paginated registry, fetched per call; no cached data or cross-key access."""
|
|
76
|
+
query = {
|
|
77
|
+
"where": "venue = " + self.venue,
|
|
78
|
+
"kind": "perpetual",
|
|
79
|
+
"include_raw": "true",
|
|
80
|
+
"number_format": "string",
|
|
81
|
+
"limit": 500,
|
|
82
|
+
}
|
|
83
|
+
result, cursors, unsupported = {}, set(), False
|
|
84
|
+
while True:
|
|
85
|
+
page = call("GET", "/v2/instruments", query=query.copy())
|
|
86
|
+
for row in page["items"]:
|
|
87
|
+
if row.get("venue") != self.venue or not row.get("instrument"):
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
listing = m.market(row)
|
|
91
|
+
except BadSymbol:
|
|
92
|
+
unsupported = True
|
|
93
|
+
continue
|
|
94
|
+
key = listing["symbol"]
|
|
95
|
+
if key in result and result[key]["id"] != listing["id"]:
|
|
96
|
+
raise BadSymbol("Ambiguous unified symbol: " + key)
|
|
97
|
+
result[key] = listing
|
|
98
|
+
cursor = page.get("next_cursor")
|
|
99
|
+
if not cursor:
|
|
100
|
+
break
|
|
101
|
+
if cursor in cursors:
|
|
102
|
+
raise BadResponse("Registry cursor repeated")
|
|
103
|
+
cursors.add(cursor)
|
|
104
|
+
query["cursor"] = cursor
|
|
105
|
+
if requested is None:
|
|
106
|
+
if unsupported:
|
|
107
|
+
raise BadSymbol(
|
|
108
|
+
"Some listings lack settlement identity; request explicit supported symbols"
|
|
109
|
+
)
|
|
110
|
+
return result
|
|
111
|
+
for key in requested:
|
|
112
|
+
if key not in result:
|
|
113
|
+
raise BadSymbol(f"No unambiguous supported {key} listing on {self.venue}")
|
|
114
|
+
return {key: result[key] for key in requested}
|
|
115
|
+
|
|
116
|
+
def _where(self, listing):
|
|
117
|
+
return f"venue = {self.venue} and symbol = {json.dumps(listing['id'])}"
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _limit(limit):
|
|
121
|
+
return 1000 if limit is None else integer(limit, "limit", 1)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _item(payload, listing, venue):
|
|
125
|
+
rows = [
|
|
126
|
+
row
|
|
127
|
+
for row in payload["items"]
|
|
128
|
+
if row.get("venue") == venue and row.get("venue_symbol") == listing["id"]
|
|
129
|
+
]
|
|
130
|
+
if len(rows) != 1:
|
|
131
|
+
raise DataUnavailable("Expected exactly one selected listing", response=payload)
|
|
132
|
+
return rows[0]
|
|
133
|
+
|
|
134
|
+
def fetch_funding_rates(self, symbols=None, params=None):
|
|
135
|
+
if params and "until" in params:
|
|
136
|
+
raise ValueError("Current funding rates do not accept a historical until")
|
|
137
|
+
if symbols is not None:
|
|
138
|
+
if isinstance(symbols, str):
|
|
139
|
+
raise BadSymbol("symbols must be a sequence of unified symbols")
|
|
140
|
+
symbols = [_symbol(s) for s in symbols]
|
|
141
|
+
if not symbols:
|
|
142
|
+
return {}
|
|
143
|
+
call, _ = self._call(params)
|
|
144
|
+
markets = self._markets(call, symbols)
|
|
145
|
+
result = {}
|
|
146
|
+
# Bounded snapshot selections, including when a venue lists >500 markets.
|
|
147
|
+
listings = list(markets.values())
|
|
148
|
+
for offset in range(0, len(listings), 100):
|
|
149
|
+
batch = listings[offset : offset + 100]
|
|
150
|
+
ids = ", ".join(json.dumps(x["id"]) for x in batch)
|
|
151
|
+
data = call(
|
|
152
|
+
"POST",
|
|
153
|
+
"/v2/snapshots",
|
|
154
|
+
body={
|
|
155
|
+
"where": f"venue = {self.venue} and symbol in ({ids})",
|
|
156
|
+
"kind": "perpetual",
|
|
157
|
+
"layers": ["funding", "mark"],
|
|
158
|
+
"units": "published",
|
|
159
|
+
"number_format": "string",
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
for listing in batch:
|
|
163
|
+
row = self._item(data, listing, self.venue)
|
|
164
|
+
result[listing["symbol"]] = m.funding_rate(row, listing)
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
def fetch_funding_rate_history(self, symbol, since=None, limit=None, params=None):
|
|
168
|
+
key, limit = _symbol(symbol), self._limit(limit)
|
|
169
|
+
if since is not None:
|
|
170
|
+
since = integer(since, "since")
|
|
171
|
+
call, until = self._call(params)
|
|
172
|
+
if since is not None and since > until:
|
|
173
|
+
raise ValueError("since must be <= until")
|
|
174
|
+
listing = self._markets(call, [key])[key]
|
|
175
|
+
query = {
|
|
176
|
+
"where": self._where(listing),
|
|
177
|
+
"to": m.iso(until),
|
|
178
|
+
"order": "asc" if since is not None else "desc",
|
|
179
|
+
"number_format": "string",
|
|
180
|
+
}
|
|
181
|
+
if since is not None:
|
|
182
|
+
query["from"] = m.iso(since - 1) # API (from,to]; CCXT since is inclusive
|
|
183
|
+
rows, cursors = [], set()
|
|
184
|
+
while len(rows) < limit:
|
|
185
|
+
query["limit"] = min(1000, limit - len(rows))
|
|
186
|
+
data = call("GET", "/v2/funding/settlements", query=query.copy())
|
|
187
|
+
if any(r.get("venue") == self.venue for r in data.get("not_recorded", [])):
|
|
188
|
+
raise DataUnavailable("Settlements are not recorded for this venue", response=data)
|
|
189
|
+
for row in data["items"]:
|
|
190
|
+
if row.get("venue") != self.venue or row.get("venue_symbol") != listing["id"]:
|
|
191
|
+
raise BadResponse("Settlement page includes an unexpected listing")
|
|
192
|
+
item = m.funding_history(row, key)
|
|
193
|
+
if (since is None or since <= item["timestamp"]) and item["timestamp"] <= until:
|
|
194
|
+
rows.append(item)
|
|
195
|
+
cursor = data.get("next_cursor")
|
|
196
|
+
if not cursor:
|
|
197
|
+
break
|
|
198
|
+
if cursor in cursors:
|
|
199
|
+
raise BadResponse("Settlement cursor repeated")
|
|
200
|
+
cursors.add(cursor)
|
|
201
|
+
query["cursor"] = cursor
|
|
202
|
+
return sorted(rows[:limit], key=lambda r: r["timestamp"])
|
|
203
|
+
|
|
204
|
+
def fetch_open_interest_history(
|
|
205
|
+
self, symbol, timeframe="5m", since=None, limit=None, params=None
|
|
206
|
+
):
|
|
207
|
+
key, limit = _symbol(symbol), self._limit(limit)
|
|
208
|
+
if timeframe not in TIMEFRAMES:
|
|
209
|
+
raise ValueError("Unsupported DEBYKO sample interval")
|
|
210
|
+
step = TIMEFRAMES[timeframe]
|
|
211
|
+
call, until = self._call(params)
|
|
212
|
+
end = until // step * step
|
|
213
|
+
first = end - (limit - 1) * step if since is None else integer(since, "since")
|
|
214
|
+
if first > until:
|
|
215
|
+
raise ValueError("since must be <= until")
|
|
216
|
+
start = ((first + step - 1) // step) * step
|
|
217
|
+
listing = self._markets(call, [key])[key]
|
|
218
|
+
result = []
|
|
219
|
+
while start <= end and len(result) < limit:
|
|
220
|
+
last = min(end, start + (min(2000, limit - len(result)) - 1) * step)
|
|
221
|
+
data = call(
|
|
222
|
+
"POST",
|
|
223
|
+
"/v2/snapshots/history",
|
|
224
|
+
body={
|
|
225
|
+
"where": self._where(listing),
|
|
226
|
+
"from": m.iso(start - 1),
|
|
227
|
+
"to": m.iso(last),
|
|
228
|
+
"interval": timeframe,
|
|
229
|
+
"layers": ["oi"],
|
|
230
|
+
"units": "published",
|
|
231
|
+
"number_format": "string",
|
|
232
|
+
"include_gaps": True,
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
layer = self._item(data, listing, self.venue).get("layers", {}).get("oi")
|
|
236
|
+
if not isinstance(layer, dict) or not isinstance(layer.get("rows"), list):
|
|
237
|
+
raise BadResponse("Missing OI sample rows")
|
|
238
|
+
rows = layer["rows"]
|
|
239
|
+
expected = (last - start) // step + 1
|
|
240
|
+
if len(rows) != expected:
|
|
241
|
+
raise BadResponse(
|
|
242
|
+
"OI response omitted expected buckets; refusing an incomplete history"
|
|
243
|
+
)
|
|
244
|
+
for index, row in enumerate(rows):
|
|
245
|
+
if m.milliseconds(row.get("bucket_end")) != start + index * step:
|
|
246
|
+
raise BadResponse("OI sample grid changed")
|
|
247
|
+
# Historical units belong to the returned layer, not today's instrument.
|
|
248
|
+
result.append(m.open_interest(row, layer.get("units", {}).get("oi"), listing))
|
|
249
|
+
start = last + step
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def fetch_liquidations(self, symbol, since=None, limit=None, params=None):
|
|
253
|
+
key, limit = _symbol(symbol), self._limit(limit)
|
|
254
|
+
call, until = self._call(params)
|
|
255
|
+
start = until - 3_600_000 if since is None else integer(since, "since") - 1
|
|
256
|
+
if start >= until:
|
|
257
|
+
raise ValueError("since must be <= until")
|
|
258
|
+
listing = self._markets(call, [key])[key]
|
|
259
|
+
result, seen = [], set()
|
|
260
|
+
window = DAY
|
|
261
|
+
while start < until and (since is None or len(result) < limit):
|
|
262
|
+
end = min(until, start + window)
|
|
263
|
+
data = call(
|
|
264
|
+
"POST",
|
|
265
|
+
"/v2/liquidations",
|
|
266
|
+
body={
|
|
267
|
+
"where": self._where(listing),
|
|
268
|
+
"from": m.iso(start),
|
|
269
|
+
"to": m.iso(end),
|
|
270
|
+
"limit": 1000,
|
|
271
|
+
"units": "published",
|
|
272
|
+
"number_format": "string",
|
|
273
|
+
"include_gaps": True,
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
item = self._item(data, listing, self.venue)
|
|
277
|
+
if item.get("status") != "present":
|
|
278
|
+
raise DataUnavailable(
|
|
279
|
+
"Liquidations are unavailable for this listing", response=data
|
|
280
|
+
)
|
|
281
|
+
if item.get("truncated"):
|
|
282
|
+
if end - start <= 1:
|
|
283
|
+
raise DataUnavailable(
|
|
284
|
+
"Liquidations cannot be paged without losing tied events", response=data
|
|
285
|
+
)
|
|
286
|
+
window = max(1, (end - start) // 2)
|
|
287
|
+
continue
|
|
288
|
+
if item.get("gaps") and not item.get("events"):
|
|
289
|
+
raise DataUnavailable("Empty liquidation window has data gaps", response=data)
|
|
290
|
+
for event in item.get("events", []):
|
|
291
|
+
ts = m.milliseconds(event.get("event_time"))
|
|
292
|
+
if (
|
|
293
|
+
not m.instant(m.iso(start))
|
|
294
|
+
< m.instant(event["event_time"])
|
|
295
|
+
<= m.instant(m.iso(end))
|
|
296
|
+
):
|
|
297
|
+
raise BadResponse("Liquidation event is outside the requested window")
|
|
298
|
+
if since is not None and ts < since:
|
|
299
|
+
continue
|
|
300
|
+
# Only venue IDs are identities. Distinct equal-size/time events
|
|
301
|
+
# without IDs must not be deduplicated by their values.
|
|
302
|
+
uid = event.get("venue_uid")
|
|
303
|
+
if uid is not None and uid in seen:
|
|
304
|
+
continue
|
|
305
|
+
if uid is not None:
|
|
306
|
+
seen.add(uid)
|
|
307
|
+
result.append(m.liquidation(event, item, listing))
|
|
308
|
+
start = end
|
|
309
|
+
window = DAY
|
|
310
|
+
result.sort(key=lambda r: r["timestamp"])
|
|
311
|
+
return result[:limit] if since is not None else result[-limit:]
|
|
312
|
+
|
|
313
|
+
fetchFundingRateHistory = fetch_funding_rate_history
|
|
314
|
+
fetchFundingRates = fetch_funding_rates
|
|
315
|
+
fetchOpenInterestHistory = fetch_open_interest_history
|
|
316
|
+
fetchLiquidations = fetch_liquidations
|