oxarchive 0.5.4__tar.gz → 0.6.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.
- {oxarchive-0.5.4 → oxarchive-0.6.0}/PKG-INFO +61 -1
- {oxarchive-0.5.4 → oxarchive-0.6.0}/README.md +60 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/__init__.py +1 -1
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/client.py +5 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/__init__.py +2 -0
- oxarchive-0.6.0/oxarchive/resources/data_quality.py +336 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/types.py +350 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/pyproject.toml +1 -1
- {oxarchive-0.5.4 → oxarchive-0.6.0}/.gitignore +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/exchanges.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/http.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/candles.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/funding.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/instruments.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/liquidations.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/openinterest.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/orderbook.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/resources/trades.py +0 -0
- {oxarchive-0.5.4 → oxarchive-0.6.0}/oxarchive/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oxarchive
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Official Python SDK for 0xarchive - Hyperliquid Historical Data API
|
|
5
5
|
Project-URL: Homepage, https://0xarchive.io
|
|
6
6
|
Project-URL: Documentation, https://0xarchive.io/docs/sdks
|
|
@@ -406,6 +406,66 @@ candles = await client.hyperliquid.candles.ahistory("BTC", start=..., end=..., i
|
|
|
406
406
|
| `1d` | 1 day |
|
|
407
407
|
| `1w` | 1 week |
|
|
408
408
|
|
|
409
|
+
### Data Quality Monitoring
|
|
410
|
+
|
|
411
|
+
Monitor data coverage, incidents, latency, and SLA compliance across all exchanges.
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
# Get overall system health status
|
|
415
|
+
status = client.data_quality.status()
|
|
416
|
+
print(f"System status: {status.status}")
|
|
417
|
+
for exchange, info in status.exchanges.items():
|
|
418
|
+
print(f" {exchange}: {info.status}")
|
|
419
|
+
|
|
420
|
+
# Get data coverage summary for all exchanges
|
|
421
|
+
coverage = client.data_quality.coverage()
|
|
422
|
+
for exchange in coverage.exchanges:
|
|
423
|
+
print(f"{exchange.exchange}:")
|
|
424
|
+
for dtype, info in exchange.data_types.items():
|
|
425
|
+
print(f" {dtype}: {info.total_records:,} records, {info.completeness}% complete")
|
|
426
|
+
|
|
427
|
+
# Get symbol-specific coverage with gap detection
|
|
428
|
+
btc = client.data_quality.symbol_coverage("hyperliquid", "BTC")
|
|
429
|
+
oi = btc.data_types["open_interest"]
|
|
430
|
+
print(f"BTC OI completeness: {oi.completeness}%")
|
|
431
|
+
print(f"Gaps found: {len(oi.gaps)}")
|
|
432
|
+
for gap in oi.gaps[:5]:
|
|
433
|
+
print(f" {gap.duration_minutes} min gap: {gap.start} -> {gap.end}")
|
|
434
|
+
|
|
435
|
+
# List incidents with filtering
|
|
436
|
+
result = client.data_quality.list_incidents(status="open")
|
|
437
|
+
for incident in result.incidents:
|
|
438
|
+
print(f"[{incident.severity}] {incident.title}")
|
|
439
|
+
|
|
440
|
+
# Get latency metrics
|
|
441
|
+
latency = client.data_quality.latency()
|
|
442
|
+
for exchange, metrics in latency.exchanges.items():
|
|
443
|
+
print(f"{exchange}: OB lag {metrics.data_freshness.orderbook_lag_ms}ms")
|
|
444
|
+
|
|
445
|
+
# Get SLA compliance metrics for a specific month
|
|
446
|
+
sla = client.data_quality.sla(year=2026, month=1)
|
|
447
|
+
print(f"Period: {sla.period}")
|
|
448
|
+
print(f"Uptime: {sla.actual.uptime}% ({sla.actual.uptime_status})")
|
|
449
|
+
print(f"API P99: {sla.actual.api_latency_p99_ms}ms ({sla.actual.latency_status})")
|
|
450
|
+
|
|
451
|
+
# Async versions available for all methods
|
|
452
|
+
status = await client.data_quality.astatus()
|
|
453
|
+
coverage = await client.data_quality.acoverage()
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### Data Quality Endpoints
|
|
457
|
+
|
|
458
|
+
| Method | Description |
|
|
459
|
+
|--------|-------------|
|
|
460
|
+
| `status()` | Overall system health and per-exchange status |
|
|
461
|
+
| `coverage()` | Data coverage summary for all exchanges |
|
|
462
|
+
| `exchange_coverage(exchange)` | Coverage details for a specific exchange |
|
|
463
|
+
| `symbol_coverage(exchange, symbol)` | Coverage with gap detection for specific symbol |
|
|
464
|
+
| `list_incidents(...)` | List incidents with filtering and pagination |
|
|
465
|
+
| `get_incident(incident_id)` | Get specific incident details |
|
|
466
|
+
| `latency()` | Current latency metrics (WebSocket, REST, data freshness) |
|
|
467
|
+
| `sla(year, month)` | SLA compliance metrics for a specific month |
|
|
468
|
+
|
|
409
469
|
### Legacy API (Deprecated)
|
|
410
470
|
|
|
411
471
|
The following legacy methods are deprecated and will be removed in v2.0. They default to Hyperliquid data:
|
|
@@ -369,6 +369,66 @@ candles = await client.hyperliquid.candles.ahistory("BTC", start=..., end=..., i
|
|
|
369
369
|
| `1d` | 1 day |
|
|
370
370
|
| `1w` | 1 week |
|
|
371
371
|
|
|
372
|
+
### Data Quality Monitoring
|
|
373
|
+
|
|
374
|
+
Monitor data coverage, incidents, latency, and SLA compliance across all exchanges.
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
# Get overall system health status
|
|
378
|
+
status = client.data_quality.status()
|
|
379
|
+
print(f"System status: {status.status}")
|
|
380
|
+
for exchange, info in status.exchanges.items():
|
|
381
|
+
print(f" {exchange}: {info.status}")
|
|
382
|
+
|
|
383
|
+
# Get data coverage summary for all exchanges
|
|
384
|
+
coverage = client.data_quality.coverage()
|
|
385
|
+
for exchange in coverage.exchanges:
|
|
386
|
+
print(f"{exchange.exchange}:")
|
|
387
|
+
for dtype, info in exchange.data_types.items():
|
|
388
|
+
print(f" {dtype}: {info.total_records:,} records, {info.completeness}% complete")
|
|
389
|
+
|
|
390
|
+
# Get symbol-specific coverage with gap detection
|
|
391
|
+
btc = client.data_quality.symbol_coverage("hyperliquid", "BTC")
|
|
392
|
+
oi = btc.data_types["open_interest"]
|
|
393
|
+
print(f"BTC OI completeness: {oi.completeness}%")
|
|
394
|
+
print(f"Gaps found: {len(oi.gaps)}")
|
|
395
|
+
for gap in oi.gaps[:5]:
|
|
396
|
+
print(f" {gap.duration_minutes} min gap: {gap.start} -> {gap.end}")
|
|
397
|
+
|
|
398
|
+
# List incidents with filtering
|
|
399
|
+
result = client.data_quality.list_incidents(status="open")
|
|
400
|
+
for incident in result.incidents:
|
|
401
|
+
print(f"[{incident.severity}] {incident.title}")
|
|
402
|
+
|
|
403
|
+
# Get latency metrics
|
|
404
|
+
latency = client.data_quality.latency()
|
|
405
|
+
for exchange, metrics in latency.exchanges.items():
|
|
406
|
+
print(f"{exchange}: OB lag {metrics.data_freshness.orderbook_lag_ms}ms")
|
|
407
|
+
|
|
408
|
+
# Get SLA compliance metrics for a specific month
|
|
409
|
+
sla = client.data_quality.sla(year=2026, month=1)
|
|
410
|
+
print(f"Period: {sla.period}")
|
|
411
|
+
print(f"Uptime: {sla.actual.uptime}% ({sla.actual.uptime_status})")
|
|
412
|
+
print(f"API P99: {sla.actual.api_latency_p99_ms}ms ({sla.actual.latency_status})")
|
|
413
|
+
|
|
414
|
+
# Async versions available for all methods
|
|
415
|
+
status = await client.data_quality.astatus()
|
|
416
|
+
coverage = await client.data_quality.acoverage()
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Data Quality Endpoints
|
|
420
|
+
|
|
421
|
+
| Method | Description |
|
|
422
|
+
|--------|-------------|
|
|
423
|
+
| `status()` | Overall system health and per-exchange status |
|
|
424
|
+
| `coverage()` | Data coverage summary for all exchanges |
|
|
425
|
+
| `exchange_coverage(exchange)` | Coverage details for a specific exchange |
|
|
426
|
+
| `symbol_coverage(exchange, symbol)` | Coverage with gap detection for specific symbol |
|
|
427
|
+
| `list_incidents(...)` | List incidents with filtering and pagination |
|
|
428
|
+
| `get_incident(incident_id)` | Get specific incident details |
|
|
429
|
+
| `latency()` | Current latency metrics (WebSocket, REST, data freshness) |
|
|
430
|
+
| `sla(year, month)` | SLA compliance metrics for a specific month |
|
|
431
|
+
|
|
372
432
|
### Legacy API (Deprecated)
|
|
373
433
|
|
|
374
434
|
The following legacy methods are deprecated and will be removed in v2.0. They default to Hyperliquid data:
|
|
@@ -12,6 +12,7 @@ from .resources import (
|
|
|
12
12
|
InstrumentsResource,
|
|
13
13
|
FundingResource,
|
|
14
14
|
OpenInterestResource,
|
|
15
|
+
DataQualityResource,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
DEFAULT_BASE_URL = "https://api.0xarchive.io"
|
|
@@ -92,6 +93,10 @@ class Client:
|
|
|
92
93
|
self.lighter = LighterClient(self._http)
|
|
93
94
|
"""Lighter.xyz exchange data (August 2025+)"""
|
|
94
95
|
|
|
96
|
+
# Data quality monitoring (cross-exchange)
|
|
97
|
+
self.data_quality = DataQualityResource(self._http)
|
|
98
|
+
"""Data quality metrics: status, coverage, incidents, latency, SLA"""
|
|
99
|
+
|
|
95
100
|
# Legacy resource namespaces (deprecated - use client.hyperliquid.* instead)
|
|
96
101
|
# These will be removed in v2.0
|
|
97
102
|
# Note: Using /v1/hyperliquid base path for backward compatibility
|
|
@@ -7,6 +7,7 @@ from .funding import FundingResource
|
|
|
7
7
|
from .openinterest import OpenInterestResource
|
|
8
8
|
from .candles import CandlesResource
|
|
9
9
|
from .liquidations import LiquidationsResource
|
|
10
|
+
from .data_quality import DataQualityResource
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"OrderBookResource",
|
|
@@ -17,4 +18,5 @@ __all__ = [
|
|
|
17
18
|
"OpenInterestResource",
|
|
18
19
|
"CandlesResource",
|
|
19
20
|
"LiquidationsResource",
|
|
21
|
+
"DataQualityResource",
|
|
20
22
|
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Data quality API resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
from ..http import HttpClient
|
|
9
|
+
from ..types import (
|
|
10
|
+
CoverageResponse,
|
|
11
|
+
ExchangeCoverage,
|
|
12
|
+
Incident,
|
|
13
|
+
IncidentsResponse,
|
|
14
|
+
LatencyResponse,
|
|
15
|
+
SlaResponse,
|
|
16
|
+
StatusResponse,
|
|
17
|
+
SymbolCoverageResponse,
|
|
18
|
+
Timestamp,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataQualityResource:
|
|
23
|
+
"""
|
|
24
|
+
Data quality API resource.
|
|
25
|
+
|
|
26
|
+
Provides endpoints for monitoring data quality, coverage, incidents, and SLA metrics.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> # Get system status
|
|
30
|
+
>>> status = client.data_quality.status()
|
|
31
|
+
>>> print(f"System status: {status.status}")
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Get coverage for all exchanges
|
|
34
|
+
>>> coverage = client.data_quality.coverage()
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Get symbol-specific coverage with gap detection
|
|
37
|
+
>>> btc = client.data_quality.symbol_coverage("hyperliquid", "BTC")
|
|
38
|
+
>>> print(f"BTC completeness: {btc.data_types['orderbook'].completeness}%")
|
|
39
|
+
>>> for gap in btc.data_types['orderbook'].gaps[:5]:
|
|
40
|
+
... print(f"Gap: {gap.start} - {gap.end} ({gap.duration_minutes} min)")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, http: HttpClient, base_path: str = "/v1/data-quality"):
|
|
44
|
+
self._http = http
|
|
45
|
+
self._base_path = base_path
|
|
46
|
+
|
|
47
|
+
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
48
|
+
"""Convert timestamp to Unix milliseconds."""
|
|
49
|
+
if ts is None:
|
|
50
|
+
return None
|
|
51
|
+
if isinstance(ts, int):
|
|
52
|
+
return ts
|
|
53
|
+
if isinstance(ts, datetime):
|
|
54
|
+
return int(ts.timestamp() * 1000)
|
|
55
|
+
if isinstance(ts, str):
|
|
56
|
+
try:
|
|
57
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
58
|
+
return int(dt.timestamp() * 1000)
|
|
59
|
+
except ValueError:
|
|
60
|
+
return int(ts)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# =========================================================================
|
|
64
|
+
# Status Endpoints
|
|
65
|
+
# =========================================================================
|
|
66
|
+
|
|
67
|
+
def status(self) -> StatusResponse:
|
|
68
|
+
"""
|
|
69
|
+
Get overall system health status.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
StatusResponse with overall status, per-exchange status,
|
|
73
|
+
per-data-type status, and active incident count.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> status = client.data_quality.status()
|
|
77
|
+
>>> print(f"Overall: {status.status}")
|
|
78
|
+
>>> for exchange, info in status.exchanges.items():
|
|
79
|
+
... print(f"{exchange}: {info.status}")
|
|
80
|
+
"""
|
|
81
|
+
data = self._http.get(f"{self._base_path}/status")
|
|
82
|
+
return StatusResponse.model_validate(data)
|
|
83
|
+
|
|
84
|
+
async def astatus(self) -> StatusResponse:
|
|
85
|
+
"""Async version of status()."""
|
|
86
|
+
data = await self._http.aget(f"{self._base_path}/status")
|
|
87
|
+
return StatusResponse.model_validate(data)
|
|
88
|
+
|
|
89
|
+
# =========================================================================
|
|
90
|
+
# Coverage Endpoints
|
|
91
|
+
# =========================================================================
|
|
92
|
+
|
|
93
|
+
def coverage(self) -> CoverageResponse:
|
|
94
|
+
"""
|
|
95
|
+
Get data coverage summary for all exchanges.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
CoverageResponse with coverage info for all exchanges and data types.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> coverage = client.data_quality.coverage()
|
|
102
|
+
>>> for exchange in coverage.exchanges:
|
|
103
|
+
... print(f"{exchange.exchange}:")
|
|
104
|
+
... for dtype, info in exchange.data_types.items():
|
|
105
|
+
... print(f" {dtype}: {info.total_records} records")
|
|
106
|
+
"""
|
|
107
|
+
data = self._http.get(f"{self._base_path}/coverage")
|
|
108
|
+
return CoverageResponse.model_validate(data)
|
|
109
|
+
|
|
110
|
+
async def acoverage(self) -> CoverageResponse:
|
|
111
|
+
"""Async version of coverage()."""
|
|
112
|
+
data = await self._http.aget(f"{self._base_path}/coverage")
|
|
113
|
+
return CoverageResponse.model_validate(data)
|
|
114
|
+
|
|
115
|
+
def exchange_coverage(self, exchange: str) -> ExchangeCoverage:
|
|
116
|
+
"""
|
|
117
|
+
Get data coverage for a specific exchange.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
exchange: Exchange name ('hyperliquid' or 'lighter')
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
ExchangeCoverage with coverage info for all data types on this exchange.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> hl = client.data_quality.exchange_coverage("hyperliquid")
|
|
127
|
+
>>> print(f"Orderbook earliest: {hl.data_types['orderbook'].earliest}")
|
|
128
|
+
"""
|
|
129
|
+
data = self._http.get(f"{self._base_path}/coverage/{exchange.lower()}")
|
|
130
|
+
return ExchangeCoverage.model_validate(data)
|
|
131
|
+
|
|
132
|
+
async def aexchange_coverage(self, exchange: str) -> ExchangeCoverage:
|
|
133
|
+
"""Async version of exchange_coverage()."""
|
|
134
|
+
data = await self._http.aget(f"{self._base_path}/coverage/{exchange.lower()}")
|
|
135
|
+
return ExchangeCoverage.model_validate(data)
|
|
136
|
+
|
|
137
|
+
def symbol_coverage(self, exchange: str, symbol: str) -> SymbolCoverageResponse:
|
|
138
|
+
"""
|
|
139
|
+
Get data coverage for a specific symbol on an exchange.
|
|
140
|
+
|
|
141
|
+
Includes gap detection showing periods where data may be missing.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
exchange: Exchange name ('hyperliquid' or 'lighter')
|
|
145
|
+
symbol: Symbol name (e.g., 'BTC', 'ETH')
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
SymbolCoverageResponse with per-data-type coverage including gaps.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> btc = client.data_quality.symbol_coverage("hyperliquid", "BTC")
|
|
152
|
+
>>> oi = btc.data_types["open_interest"]
|
|
153
|
+
>>> print(f"OI completeness: {oi.completeness}%")
|
|
154
|
+
>>> print(f"Gaps found: {len(oi.gaps)}")
|
|
155
|
+
>>> for gap in oi.gaps[:3]:
|
|
156
|
+
... print(f" {gap.duration_minutes} min gap at {gap.start}")
|
|
157
|
+
"""
|
|
158
|
+
data = self._http.get(
|
|
159
|
+
f"{self._base_path}/coverage/{exchange.lower()}/{symbol.upper()}"
|
|
160
|
+
)
|
|
161
|
+
return SymbolCoverageResponse.model_validate(data)
|
|
162
|
+
|
|
163
|
+
async def asymbol_coverage(self, exchange: str, symbol: str) -> SymbolCoverageResponse:
|
|
164
|
+
"""Async version of symbol_coverage()."""
|
|
165
|
+
data = await self._http.aget(
|
|
166
|
+
f"{self._base_path}/coverage/{exchange.lower()}/{symbol.upper()}"
|
|
167
|
+
)
|
|
168
|
+
return SymbolCoverageResponse.model_validate(data)
|
|
169
|
+
|
|
170
|
+
# =========================================================================
|
|
171
|
+
# Incidents Endpoints
|
|
172
|
+
# =========================================================================
|
|
173
|
+
|
|
174
|
+
def list_incidents(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
status: Optional[Literal["open", "investigating", "identified", "monitoring", "resolved"]] = None,
|
|
178
|
+
exchange: Optional[str] = None,
|
|
179
|
+
since: Optional[Timestamp] = None,
|
|
180
|
+
limit: Optional[int] = None,
|
|
181
|
+
offset: Optional[int] = None,
|
|
182
|
+
) -> IncidentsResponse:
|
|
183
|
+
"""
|
|
184
|
+
List incidents with filtering and pagination.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
status: Filter by incident status
|
|
188
|
+
exchange: Filter by exchange
|
|
189
|
+
since: Only show incidents starting after this timestamp
|
|
190
|
+
limit: Maximum results per page (default: 20, max: 100)
|
|
191
|
+
offset: Pagination offset
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
IncidentsResponse with list of incidents and pagination info.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> # Get all open incidents
|
|
198
|
+
>>> result = client.data_quality.list_incidents(status="open")
|
|
199
|
+
>>> for incident in result.incidents:
|
|
200
|
+
... print(f"{incident.severity}: {incident.title}")
|
|
201
|
+
"""
|
|
202
|
+
data = self._http.get(
|
|
203
|
+
f"{self._base_path}/incidents",
|
|
204
|
+
params={
|
|
205
|
+
"status": status,
|
|
206
|
+
"exchange": exchange,
|
|
207
|
+
"since": self._convert_timestamp(since),
|
|
208
|
+
"limit": limit,
|
|
209
|
+
"offset": offset,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
return IncidentsResponse.model_validate(data)
|
|
213
|
+
|
|
214
|
+
async def alist_incidents(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
status: Optional[Literal["open", "investigating", "identified", "monitoring", "resolved"]] = None,
|
|
218
|
+
exchange: Optional[str] = None,
|
|
219
|
+
since: Optional[Timestamp] = None,
|
|
220
|
+
limit: Optional[int] = None,
|
|
221
|
+
offset: Optional[int] = None,
|
|
222
|
+
) -> IncidentsResponse:
|
|
223
|
+
"""Async version of list_incidents()."""
|
|
224
|
+
data = await self._http.aget(
|
|
225
|
+
f"{self._base_path}/incidents",
|
|
226
|
+
params={
|
|
227
|
+
"status": status,
|
|
228
|
+
"exchange": exchange,
|
|
229
|
+
"since": self._convert_timestamp(since),
|
|
230
|
+
"limit": limit,
|
|
231
|
+
"offset": offset,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
return IncidentsResponse.model_validate(data)
|
|
235
|
+
|
|
236
|
+
def get_incident(self, incident_id: str) -> Incident:
|
|
237
|
+
"""
|
|
238
|
+
Get a specific incident by ID.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
incident_id: The incident ID
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Incident details.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
>>> incident = client.data_quality.get_incident("inc_123")
|
|
248
|
+
>>> print(f"Status: {incident.status}")
|
|
249
|
+
>>> print(f"Root cause: {incident.root_cause}")
|
|
250
|
+
"""
|
|
251
|
+
data = self._http.get(f"{self._base_path}/incidents/{incident_id}")
|
|
252
|
+
return Incident.model_validate(data)
|
|
253
|
+
|
|
254
|
+
async def aget_incident(self, incident_id: str) -> Incident:
|
|
255
|
+
"""Async version of get_incident()."""
|
|
256
|
+
data = await self._http.aget(f"{self._base_path}/incidents/{incident_id}")
|
|
257
|
+
return Incident.model_validate(data)
|
|
258
|
+
|
|
259
|
+
# =========================================================================
|
|
260
|
+
# Latency Endpoints
|
|
261
|
+
# =========================================================================
|
|
262
|
+
|
|
263
|
+
def latency(self) -> LatencyResponse:
|
|
264
|
+
"""
|
|
265
|
+
Get current latency metrics for all exchanges.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
LatencyResponse with WebSocket, REST API, and data freshness metrics.
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
>>> latency = client.data_quality.latency()
|
|
272
|
+
>>> for exchange, metrics in latency.exchanges.items():
|
|
273
|
+
... print(f"{exchange}:")
|
|
274
|
+
... if metrics.websocket:
|
|
275
|
+
... print(f" WS current: {metrics.websocket.current_ms}ms")
|
|
276
|
+
... print(f" OB lag: {metrics.data_freshness.orderbook_lag_ms}ms")
|
|
277
|
+
"""
|
|
278
|
+
data = self._http.get(f"{self._base_path}/latency")
|
|
279
|
+
return LatencyResponse.model_validate(data)
|
|
280
|
+
|
|
281
|
+
async def alatency(self) -> LatencyResponse:
|
|
282
|
+
"""Async version of latency()."""
|
|
283
|
+
data = await self._http.aget(f"{self._base_path}/latency")
|
|
284
|
+
return LatencyResponse.model_validate(data)
|
|
285
|
+
|
|
286
|
+
# =========================================================================
|
|
287
|
+
# SLA Endpoints
|
|
288
|
+
# =========================================================================
|
|
289
|
+
|
|
290
|
+
def sla(
|
|
291
|
+
self,
|
|
292
|
+
*,
|
|
293
|
+
year: Optional[int] = None,
|
|
294
|
+
month: Optional[int] = None,
|
|
295
|
+
) -> SlaResponse:
|
|
296
|
+
"""
|
|
297
|
+
Get SLA compliance metrics for a specific month.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
year: Year (defaults to current year)
|
|
301
|
+
month: Month 1-12 (defaults to current month)
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
SlaResponse with SLA targets, actual metrics, and compliance status.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> sla = client.data_quality.sla(year=2026, month=1)
|
|
308
|
+
>>> print(f"Period: {sla.period}")
|
|
309
|
+
>>> print(f"Uptime: {sla.actual.uptime}% ({sla.actual.uptime_status})")
|
|
310
|
+
>>> print(f"Completeness: {sla.actual.data_completeness.overall}%")
|
|
311
|
+
>>> print(f"API P99: {sla.actual.api_latency_p99_ms}ms")
|
|
312
|
+
"""
|
|
313
|
+
data = self._http.get(
|
|
314
|
+
f"{self._base_path}/sla",
|
|
315
|
+
params={
|
|
316
|
+
"year": year,
|
|
317
|
+
"month": month,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
return SlaResponse.model_validate(data)
|
|
321
|
+
|
|
322
|
+
async def asla(
|
|
323
|
+
self,
|
|
324
|
+
*,
|
|
325
|
+
year: Optional[int] = None,
|
|
326
|
+
month: Optional[int] = None,
|
|
327
|
+
) -> SlaResponse:
|
|
328
|
+
"""Async version of sla()."""
|
|
329
|
+
data = await self._http.aget(
|
|
330
|
+
f"{self._base_path}/sla",
|
|
331
|
+
params={
|
|
332
|
+
"year": year,
|
|
333
|
+
"month": month,
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
return SlaResponse.model_validate(data)
|
|
@@ -593,3 +593,353 @@ class CursorResponse(BaseModel, Generic[T]):
|
|
|
593
593
|
# Type alias for timestamp parameters
|
|
594
594
|
Timestamp = Union[int, str, datetime]
|
|
595
595
|
"""Timestamp can be Unix ms (int), ISO string, or datetime object."""
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# =============================================================================
|
|
599
|
+
# Data Quality Types
|
|
600
|
+
# =============================================================================
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class SystemStatus(BaseModel):
|
|
604
|
+
"""System status values: operational, degraded, outage, maintenance."""
|
|
605
|
+
|
|
606
|
+
status: Literal["operational", "degraded", "outage", "maintenance"]
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class ExchangeStatus(BaseModel):
|
|
610
|
+
"""Status of a single exchange."""
|
|
611
|
+
|
|
612
|
+
status: Literal["operational", "degraded", "outage", "maintenance"]
|
|
613
|
+
"""Current status."""
|
|
614
|
+
|
|
615
|
+
last_data_at: Optional[datetime] = None
|
|
616
|
+
"""Timestamp of last received data."""
|
|
617
|
+
|
|
618
|
+
latency_ms: Optional[int] = None
|
|
619
|
+
"""Current latency in milliseconds."""
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class DataTypeStatus(BaseModel):
|
|
623
|
+
"""Status of a data type (orderbook, fills, etc.)."""
|
|
624
|
+
|
|
625
|
+
status: Literal["operational", "degraded", "outage", "maintenance"]
|
|
626
|
+
"""Current status."""
|
|
627
|
+
|
|
628
|
+
completeness_24h: float
|
|
629
|
+
"""Data completeness over last 24 hours (0-100)."""
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class StatusResponse(BaseModel):
|
|
633
|
+
"""Overall system status response."""
|
|
634
|
+
|
|
635
|
+
status: Literal["operational", "degraded", "outage", "maintenance"]
|
|
636
|
+
"""Overall system status."""
|
|
637
|
+
|
|
638
|
+
updated_at: datetime
|
|
639
|
+
"""When this status was computed."""
|
|
640
|
+
|
|
641
|
+
exchanges: dict[str, ExchangeStatus]
|
|
642
|
+
"""Per-exchange status."""
|
|
643
|
+
|
|
644
|
+
data_types: dict[str, DataTypeStatus]
|
|
645
|
+
"""Per-data-type status."""
|
|
646
|
+
|
|
647
|
+
active_incidents: int
|
|
648
|
+
"""Number of active incidents."""
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class DataTypeCoverage(BaseModel):
|
|
652
|
+
"""Coverage information for a specific data type."""
|
|
653
|
+
|
|
654
|
+
earliest: datetime
|
|
655
|
+
"""Earliest available data timestamp."""
|
|
656
|
+
|
|
657
|
+
latest: datetime
|
|
658
|
+
"""Latest available data timestamp."""
|
|
659
|
+
|
|
660
|
+
total_records: int
|
|
661
|
+
"""Total number of records."""
|
|
662
|
+
|
|
663
|
+
symbols: int
|
|
664
|
+
"""Number of symbols with data."""
|
|
665
|
+
|
|
666
|
+
resolution: Optional[str] = None
|
|
667
|
+
"""Data resolution (e.g., '1.2s', '1m')."""
|
|
668
|
+
|
|
669
|
+
lag: Optional[str] = None
|
|
670
|
+
"""Current data lag."""
|
|
671
|
+
|
|
672
|
+
completeness: float
|
|
673
|
+
"""Completeness percentage (0-100)."""
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class ExchangeCoverage(BaseModel):
|
|
677
|
+
"""Coverage for a single exchange."""
|
|
678
|
+
|
|
679
|
+
exchange: str
|
|
680
|
+
"""Exchange name."""
|
|
681
|
+
|
|
682
|
+
data_types: dict[str, DataTypeCoverage]
|
|
683
|
+
"""Coverage per data type."""
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
class CoverageResponse(BaseModel):
|
|
687
|
+
"""Overall coverage response."""
|
|
688
|
+
|
|
689
|
+
exchanges: list[ExchangeCoverage]
|
|
690
|
+
"""Coverage for all exchanges."""
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class CoverageGap(BaseModel):
|
|
694
|
+
"""Gap information for per-symbol coverage."""
|
|
695
|
+
|
|
696
|
+
start: datetime
|
|
697
|
+
"""Start of the gap (last data before gap)."""
|
|
698
|
+
|
|
699
|
+
end: datetime
|
|
700
|
+
"""End of the gap (first data after gap)."""
|
|
701
|
+
|
|
702
|
+
duration_minutes: int
|
|
703
|
+
"""Duration of the gap in minutes."""
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class SymbolDataTypeCoverage(BaseModel):
|
|
707
|
+
"""Coverage for a specific symbol and data type."""
|
|
708
|
+
|
|
709
|
+
earliest: datetime
|
|
710
|
+
"""Earliest available data timestamp."""
|
|
711
|
+
|
|
712
|
+
latest: datetime
|
|
713
|
+
"""Latest available data timestamp."""
|
|
714
|
+
|
|
715
|
+
total_records: int
|
|
716
|
+
"""Total number of records."""
|
|
717
|
+
|
|
718
|
+
completeness: float
|
|
719
|
+
"""Completeness percentage (0-100)."""
|
|
720
|
+
|
|
721
|
+
gaps: list[CoverageGap]
|
|
722
|
+
"""Detected data gaps."""
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class SymbolCoverageResponse(BaseModel):
|
|
726
|
+
"""Per-symbol coverage response."""
|
|
727
|
+
|
|
728
|
+
exchange: str
|
|
729
|
+
"""Exchange name."""
|
|
730
|
+
|
|
731
|
+
symbol: str
|
|
732
|
+
"""Symbol name."""
|
|
733
|
+
|
|
734
|
+
data_types: dict[str, SymbolDataTypeCoverage]
|
|
735
|
+
"""Coverage per data type."""
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class Incident(BaseModel):
|
|
739
|
+
"""Data quality incident."""
|
|
740
|
+
|
|
741
|
+
id: str
|
|
742
|
+
"""Unique incident ID."""
|
|
743
|
+
|
|
744
|
+
status: str
|
|
745
|
+
"""Status: open, investigating, identified, monitoring, resolved."""
|
|
746
|
+
|
|
747
|
+
severity: str
|
|
748
|
+
"""Severity: minor, major, critical."""
|
|
749
|
+
|
|
750
|
+
exchange: Optional[str] = None
|
|
751
|
+
"""Affected exchange (if specific to one)."""
|
|
752
|
+
|
|
753
|
+
data_types: list[str]
|
|
754
|
+
"""Affected data types."""
|
|
755
|
+
|
|
756
|
+
symbols_affected: list[str]
|
|
757
|
+
"""Affected symbols."""
|
|
758
|
+
|
|
759
|
+
started_at: datetime
|
|
760
|
+
"""When the incident started."""
|
|
761
|
+
|
|
762
|
+
resolved_at: Optional[datetime] = None
|
|
763
|
+
"""When the incident was resolved."""
|
|
764
|
+
|
|
765
|
+
duration_minutes: Optional[int] = None
|
|
766
|
+
"""Total duration in minutes."""
|
|
767
|
+
|
|
768
|
+
title: str
|
|
769
|
+
"""Incident title."""
|
|
770
|
+
|
|
771
|
+
description: Optional[str] = None
|
|
772
|
+
"""Detailed description."""
|
|
773
|
+
|
|
774
|
+
root_cause: Optional[str] = None
|
|
775
|
+
"""Root cause analysis."""
|
|
776
|
+
|
|
777
|
+
resolution: Optional[str] = None
|
|
778
|
+
"""Resolution details."""
|
|
779
|
+
|
|
780
|
+
records_affected: Optional[int] = None
|
|
781
|
+
"""Number of records affected."""
|
|
782
|
+
|
|
783
|
+
records_recovered: Optional[int] = None
|
|
784
|
+
"""Number of records recovered."""
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class Pagination(BaseModel):
|
|
788
|
+
"""Pagination info for incident list."""
|
|
789
|
+
|
|
790
|
+
total: int
|
|
791
|
+
"""Total number of incidents."""
|
|
792
|
+
|
|
793
|
+
limit: int
|
|
794
|
+
"""Page size limit."""
|
|
795
|
+
|
|
796
|
+
offset: int
|
|
797
|
+
"""Current offset."""
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class IncidentsResponse(BaseModel):
|
|
801
|
+
"""Incidents list response."""
|
|
802
|
+
|
|
803
|
+
incidents: list[Incident]
|
|
804
|
+
"""List of incidents."""
|
|
805
|
+
|
|
806
|
+
pagination: Pagination
|
|
807
|
+
"""Pagination info."""
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
class WebSocketLatency(BaseModel):
|
|
811
|
+
"""WebSocket latency metrics."""
|
|
812
|
+
|
|
813
|
+
current_ms: int
|
|
814
|
+
"""Current latency."""
|
|
815
|
+
|
|
816
|
+
avg_1h_ms: int
|
|
817
|
+
"""1-hour average latency."""
|
|
818
|
+
|
|
819
|
+
avg_24h_ms: int
|
|
820
|
+
"""24-hour average latency."""
|
|
821
|
+
|
|
822
|
+
p99_24h_ms: Optional[int] = None
|
|
823
|
+
"""24-hour P99 latency."""
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class ApiLatency(BaseModel):
|
|
827
|
+
"""REST API latency metrics."""
|
|
828
|
+
|
|
829
|
+
current_ms: int
|
|
830
|
+
"""Current latency."""
|
|
831
|
+
|
|
832
|
+
avg_1h_ms: int
|
|
833
|
+
"""1-hour average latency."""
|
|
834
|
+
|
|
835
|
+
avg_24h_ms: int
|
|
836
|
+
"""24-hour average latency."""
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class DataFreshness(BaseModel):
|
|
840
|
+
"""Data freshness metrics (lag from source)."""
|
|
841
|
+
|
|
842
|
+
orderbook_lag_ms: Optional[int] = None
|
|
843
|
+
"""Orderbook data lag."""
|
|
844
|
+
|
|
845
|
+
fills_lag_ms: Optional[int] = None
|
|
846
|
+
"""Fills/trades data lag."""
|
|
847
|
+
|
|
848
|
+
funding_lag_ms: Optional[int] = None
|
|
849
|
+
"""Funding rate data lag."""
|
|
850
|
+
|
|
851
|
+
oi_lag_ms: Optional[int] = None
|
|
852
|
+
"""Open interest data lag."""
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
class ExchangeLatency(BaseModel):
|
|
856
|
+
"""Latency metrics for a single exchange."""
|
|
857
|
+
|
|
858
|
+
websocket: Optional[WebSocketLatency] = None
|
|
859
|
+
"""WebSocket latency metrics."""
|
|
860
|
+
|
|
861
|
+
rest_api: Optional[ApiLatency] = None
|
|
862
|
+
"""REST API latency metrics."""
|
|
863
|
+
|
|
864
|
+
data_freshness: DataFreshness
|
|
865
|
+
"""Data freshness metrics."""
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class LatencyResponse(BaseModel):
|
|
869
|
+
"""Overall latency response."""
|
|
870
|
+
|
|
871
|
+
measured_at: datetime
|
|
872
|
+
"""When these metrics were measured."""
|
|
873
|
+
|
|
874
|
+
exchanges: dict[str, ExchangeLatency]
|
|
875
|
+
"""Per-exchange latency metrics."""
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
class SlaTargets(BaseModel):
|
|
879
|
+
"""SLA targets."""
|
|
880
|
+
|
|
881
|
+
uptime: float
|
|
882
|
+
"""Uptime target percentage."""
|
|
883
|
+
|
|
884
|
+
data_completeness: float
|
|
885
|
+
"""Data completeness target percentage."""
|
|
886
|
+
|
|
887
|
+
api_latency_p99_ms: int
|
|
888
|
+
"""API P99 latency target in milliseconds."""
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class CompletenessMetrics(BaseModel):
|
|
892
|
+
"""Completeness metrics per data type."""
|
|
893
|
+
|
|
894
|
+
orderbook: float
|
|
895
|
+
"""Orderbook completeness percentage."""
|
|
896
|
+
|
|
897
|
+
fills: float
|
|
898
|
+
"""Fills completeness percentage."""
|
|
899
|
+
|
|
900
|
+
funding: float
|
|
901
|
+
"""Funding rate completeness percentage."""
|
|
902
|
+
|
|
903
|
+
overall: float
|
|
904
|
+
"""Overall completeness percentage."""
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class SlaActual(BaseModel):
|
|
908
|
+
"""Actual SLA metrics."""
|
|
909
|
+
|
|
910
|
+
uptime: float
|
|
911
|
+
"""Actual uptime percentage."""
|
|
912
|
+
|
|
913
|
+
uptime_status: str
|
|
914
|
+
"""'met' or 'missed'."""
|
|
915
|
+
|
|
916
|
+
data_completeness: CompletenessMetrics
|
|
917
|
+
"""Actual completeness metrics."""
|
|
918
|
+
|
|
919
|
+
completeness_status: str
|
|
920
|
+
"""'met' or 'missed'."""
|
|
921
|
+
|
|
922
|
+
api_latency_p99_ms: int
|
|
923
|
+
"""Actual API P99 latency."""
|
|
924
|
+
|
|
925
|
+
latency_status: str
|
|
926
|
+
"""'met' or 'missed'."""
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class SlaResponse(BaseModel):
|
|
930
|
+
"""SLA compliance response."""
|
|
931
|
+
|
|
932
|
+
period: str
|
|
933
|
+
"""Period covered (e.g., '2026-01')."""
|
|
934
|
+
|
|
935
|
+
sla_targets: SlaTargets
|
|
936
|
+
"""Target SLA metrics."""
|
|
937
|
+
|
|
938
|
+
actual: SlaActual
|
|
939
|
+
"""Actual SLA metrics."""
|
|
940
|
+
|
|
941
|
+
incidents_this_period: int
|
|
942
|
+
"""Number of incidents in this period."""
|
|
943
|
+
|
|
944
|
+
total_downtime_minutes: int
|
|
945
|
+
"""Total downtime in minutes."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|