bitvavo-api-upgraded 4.1.0__py3-none-any.whl → 4.1.1__py3-none-any.whl
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.
- {bitvavo_api_upgraded-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/METADATA +1 -1
- bitvavo_api_upgraded-4.1.1.dist-info/RECORD +38 -0
- bitvavo_client/__init__.py +9 -0
- bitvavo_client/adapters/__init__.py +1 -0
- bitvavo_client/adapters/returns_adapter.py +363 -0
- bitvavo_client/auth/__init__.py +1 -0
- bitvavo_client/auth/rate_limit.py +104 -0
- bitvavo_client/auth/signing.py +33 -0
- bitvavo_client/core/__init__.py +1 -0
- bitvavo_client/core/errors.py +17 -0
- bitvavo_client/core/model_preferences.py +33 -0
- bitvavo_client/core/private_models.py +886 -0
- bitvavo_client/core/public_models.py +1087 -0
- bitvavo_client/core/settings.py +52 -0
- bitvavo_client/core/types.py +11 -0
- bitvavo_client/core/validation_helpers.py +90 -0
- bitvavo_client/df/__init__.py +1 -0
- bitvavo_client/df/convert.py +86 -0
- bitvavo_client/endpoints/__init__.py +1 -0
- bitvavo_client/endpoints/common.py +88 -0
- bitvavo_client/endpoints/private.py +1090 -0
- bitvavo_client/endpoints/public.py +658 -0
- bitvavo_client/facade.py +66 -0
- bitvavo_client/py.typed +0 -0
- bitvavo_client/schemas/__init__.py +50 -0
- bitvavo_client/schemas/private_schemas.py +191 -0
- bitvavo_client/schemas/public_schemas.py +149 -0
- bitvavo_client/transport/__init__.py +1 -0
- bitvavo_client/transport/http.py +159 -0
- bitvavo_client/ws/__init__.py +1 -0
- bitvavo_api_upgraded-4.1.0.dist-info/RECORD +0 -10
- {bitvavo_api_upgraded-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,658 @@
|
|
1
|
+
"""Public API endpoints that don't require authentication."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
6
|
+
|
7
|
+
import polars as pl
|
8
|
+
from returns.result import Failure, Success
|
9
|
+
|
10
|
+
from bitvavo_client.adapters.returns_adapter import BitvavoError
|
11
|
+
from bitvavo_client.core import public_models
|
12
|
+
from bitvavo_client.core.model_preferences import ModelPreference
|
13
|
+
from bitvavo_client.endpoints.common import create_postfix
|
14
|
+
from bitvavo_client.schemas.public_schemas import DEFAULT_SCHEMAS
|
15
|
+
|
16
|
+
# Valid intervals for candlestick data according to Bitvavo API documentation
|
17
|
+
CandleInterval = Literal["1m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "1W", "1M"]
|
18
|
+
|
19
|
+
# API parameter limits according to Bitvavo documentation
|
20
|
+
MAX_CANDLE_LIMIT = 1440 # Maximum number of candlesticks that can be requested
|
21
|
+
MAX_TRADES_LIMIT = 1000 # Maximum number of trades that can be requested
|
22
|
+
MAX_24_HOUR_MS = 86400000 # 24 hours in milliseconds
|
23
|
+
MAX_END_TIMESTAMP = 8640000000000000 # Maximum end timestamp value
|
24
|
+
MAX_TIMESTAMP_VALUE = 8640000000000000 # Maximum allowed timestamp value
|
25
|
+
MAX_BOOK_DEPTH = 1000 # Maximum depth for order book
|
26
|
+
MAX_BOOK_REPORT_DEPTH = 1000 # Maximum depth for order book report
|
27
|
+
|
28
|
+
if TYPE_CHECKING: # pragma: no cover
|
29
|
+
from collections.abc import Mapping
|
30
|
+
|
31
|
+
import httpx
|
32
|
+
from returns.result import Result
|
33
|
+
|
34
|
+
from bitvavo_client.core.types import AnyDict
|
35
|
+
from bitvavo_client.transport.http import HTTPClient
|
36
|
+
|
37
|
+
T = TypeVar("T")
|
38
|
+
|
39
|
+
|
40
|
+
class PublicAPI:
|
41
|
+
"""Handles all public Bitvavo API endpoints."""
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
http_client: HTTPClient,
|
46
|
+
*,
|
47
|
+
preferred_model: ModelPreference | str | None = None,
|
48
|
+
default_schema: Mapping[str, object] | None = None,
|
49
|
+
) -> None:
|
50
|
+
"""Initialize public API handler.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
http_client: HTTP client for making requests
|
54
|
+
preferred_model: Preferred model format for responses
|
55
|
+
default_schema: Default schema for DataFrame conversion
|
56
|
+
"""
|
57
|
+
self.http: HTTPClient = http_client
|
58
|
+
self.preferred_model = ModelPreference(preferred_model) if preferred_model else None
|
59
|
+
|
60
|
+
# If using DATAFRAME preference without a default schema, we could provide sensible defaults
|
61
|
+
# But keep it explicit for now - users can import and use schemas as needed
|
62
|
+
self.default_schema = default_schema
|
63
|
+
|
64
|
+
def _get_effective_model(
|
65
|
+
self,
|
66
|
+
endpoint_type: str,
|
67
|
+
model: type[T] | Any | None,
|
68
|
+
schema: Mapping[str, object] | None,
|
69
|
+
) -> tuple[type[T] | Any | None, Mapping[str, object] | None]:
|
70
|
+
"""Get the effective model and schema to use for a request.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
endpoint_type: Type of endpoint (e.g., 'time', 'markets', 'assets')
|
74
|
+
model: Model explicitly passed to method (overrides preference)
|
75
|
+
schema: Schema explicitly passed to method
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Tuple of (effective_model, effective_schema)
|
79
|
+
"""
|
80
|
+
# If model is explicitly provided, use it
|
81
|
+
if model is not None:
|
82
|
+
return model, schema
|
83
|
+
|
84
|
+
# If no preferred model is set, return Any (raw response)
|
85
|
+
if self.preferred_model is None:
|
86
|
+
return Any, schema
|
87
|
+
|
88
|
+
# Apply preference based on enum value
|
89
|
+
if self.preferred_model == ModelPreference.RAW:
|
90
|
+
return Any, schema
|
91
|
+
|
92
|
+
if self.preferred_model == ModelPreference.DATAFRAME:
|
93
|
+
# Use the provided schema, fallback to instance default, then to endpoint-specific default
|
94
|
+
effective_schema = schema or self.default_schema or DEFAULT_SCHEMAS.get(endpoint_type)
|
95
|
+
return pl.DataFrame, effective_schema
|
96
|
+
|
97
|
+
if self.preferred_model == ModelPreference.PYDANTIC:
|
98
|
+
# Map endpoint types to appropriate Pydantic models
|
99
|
+
endpoint_model_map = {
|
100
|
+
"time": public_models.ServerTime,
|
101
|
+
"markets": public_models.Markets,
|
102
|
+
"assets": public_models.Assets,
|
103
|
+
"book": public_models.OrderBook,
|
104
|
+
"trades": public_models.Trades,
|
105
|
+
"candles": public_models.Candles,
|
106
|
+
"ticker_price": public_models.TickerPrices,
|
107
|
+
"ticker_book": public_models.TickerBooks,
|
108
|
+
"ticker_24h": public_models.Ticker24hs,
|
109
|
+
"report_book": public_models.OrderBookReport,
|
110
|
+
"report_trades": public_models.TradesReport,
|
111
|
+
}
|
112
|
+
return endpoint_model_map.get(endpoint_type, dict), schema
|
113
|
+
|
114
|
+
# Default case (AUTO or unknown)
|
115
|
+
return None, schema
|
116
|
+
|
117
|
+
def _convert_raw_result(
|
118
|
+
self,
|
119
|
+
raw_result: Result[Any, BitvavoError | httpx.HTTPError],
|
120
|
+
endpoint_type: str,
|
121
|
+
model: type[T] | Any | None,
|
122
|
+
schema: Mapping[str, object] | None,
|
123
|
+
) -> Result[Any, BitvavoError | httpx.HTTPError]:
|
124
|
+
"""Convert raw API result to the desired model format.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
raw_result: Raw result from HTTP client
|
128
|
+
endpoint_type: Type of endpoint (e.g., 'time', 'markets', 'assets')
|
129
|
+
model: Model explicitly passed to method (overrides preference)
|
130
|
+
schema: Schema explicitly passed to method
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Result with converted data or original error
|
134
|
+
"""
|
135
|
+
# If the raw result is an error, return it as-is
|
136
|
+
if isinstance(raw_result, Failure):
|
137
|
+
return raw_result
|
138
|
+
|
139
|
+
# Get the effective model and schema to use
|
140
|
+
effective_model, effective_schema = self._get_effective_model(endpoint_type, model, schema)
|
141
|
+
|
142
|
+
# If no conversion needed (raw data requested), return as-is
|
143
|
+
if effective_model is Any or effective_model is None:
|
144
|
+
return raw_result
|
145
|
+
|
146
|
+
# Extract the raw data
|
147
|
+
raw_data = raw_result.unwrap()
|
148
|
+
|
149
|
+
# Perform conversion using the same logic as the returns adapter
|
150
|
+
try:
|
151
|
+
# Handle different model types
|
152
|
+
if hasattr(effective_model, "model_validate"):
|
153
|
+
# Pydantic model
|
154
|
+
parsed = effective_model.model_validate(raw_data) # type: ignore[misc]
|
155
|
+
elif effective_schema is None:
|
156
|
+
# Simple constructor call - this handles dict and other simple types
|
157
|
+
parsed = effective_model(raw_data) # type: ignore[misc]
|
158
|
+
else:
|
159
|
+
# DataFrame with schema - use type ignoring for now to get working
|
160
|
+
parsed = effective_model(raw_data, schema=effective_schema, strict=False) # type: ignore[misc]
|
161
|
+
|
162
|
+
return Success(parsed)
|
163
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
164
|
+
# If conversion fails, return a structured error
|
165
|
+
error = BitvavoError(
|
166
|
+
http_status=500,
|
167
|
+
error_code=-1,
|
168
|
+
reason="Model conversion failed",
|
169
|
+
message=str(exc),
|
170
|
+
raw=raw_data if isinstance(raw_data, dict) else {"raw": raw_data},
|
171
|
+
)
|
172
|
+
return Failure(error)
|
173
|
+
|
174
|
+
def time(
|
175
|
+
self,
|
176
|
+
*,
|
177
|
+
model: type[T] | Any | None = None,
|
178
|
+
schema: dict | None = None,
|
179
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
180
|
+
"""Get server time.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
model: Optional Pydantic model to validate response
|
184
|
+
schema: Optional schema for DataFrame conversion
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
Result containing server time or error
|
188
|
+
"""
|
189
|
+
# Get raw data from API
|
190
|
+
raw_result = self.http.request("GET", "/time", weight=1)
|
191
|
+
# Convert to desired format
|
192
|
+
return self._convert_raw_result(raw_result, "time", model, schema)
|
193
|
+
|
194
|
+
def markets(
|
195
|
+
self,
|
196
|
+
options: AnyDict | None = None,
|
197
|
+
*,
|
198
|
+
model: type[T] | Any | None = None,
|
199
|
+
schema: dict | None = None,
|
200
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
201
|
+
"""Get market information.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
options: Optional query parameters
|
205
|
+
model: Optional Pydantic model to validate response
|
206
|
+
schema: Optional schema for DataFrame conversion
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Result containing market information or error
|
210
|
+
"""
|
211
|
+
# Get raw data from API
|
212
|
+
postfix = create_postfix(options)
|
213
|
+
raw_result = self.http.request("GET", f"/markets{postfix}", weight=1)
|
214
|
+
# Convert to desired format
|
215
|
+
return self._convert_raw_result(raw_result, "markets", model, schema)
|
216
|
+
|
217
|
+
def assets(
|
218
|
+
self,
|
219
|
+
options: AnyDict | None = None,
|
220
|
+
*,
|
221
|
+
model: type[T] | Any | None = None,
|
222
|
+
schema: dict | None = None,
|
223
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
224
|
+
"""Get asset information.
|
225
|
+
|
226
|
+
Returns information about the specified assets including deposit/withdrawal
|
227
|
+
fees, confirmations required, status, and supported networks.
|
228
|
+
|
229
|
+
Endpoint: GET /v2/assets
|
230
|
+
Rate limit weight: 1
|
231
|
+
|
232
|
+
Args:
|
233
|
+
options: Optional query parameters:
|
234
|
+
- symbol (str): The asset symbol (e.g., 'BTC'). If not specified,
|
235
|
+
all supported assets are returned.
|
236
|
+
model: Optional Pydantic model to validate response
|
237
|
+
schema: Optional schema for DataFrame conversion
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Result containing asset information array:
|
241
|
+
[
|
242
|
+
{
|
243
|
+
"symbol": "BTC",
|
244
|
+
"name": "Bitcoin",
|
245
|
+
"decimals": 8,
|
246
|
+
"depositFee": "0",
|
247
|
+
"depositConfirmations": 10,
|
248
|
+
"depositStatus": "OK",
|
249
|
+
"withdrawalFee": "0.2",
|
250
|
+
"withdrawalMinAmount": "0.2",
|
251
|
+
"withdrawalStatus": "OK",
|
252
|
+
"networks": ["Mainnet"],
|
253
|
+
"message": ""
|
254
|
+
}
|
255
|
+
]
|
256
|
+
|
257
|
+
Note:
|
258
|
+
This is a public endpoint but authenticating gives higher rate limits.
|
259
|
+
Status values can be: "OK", "MAINTENANCE", "DELISTED".
|
260
|
+
"""
|
261
|
+
postfix = create_postfix(options)
|
262
|
+
raw_result = self.http.request("GET", f"/assets{postfix}", weight=1)
|
263
|
+
return self._convert_raw_result(raw_result, "assets", model, schema)
|
264
|
+
|
265
|
+
def book(
|
266
|
+
self,
|
267
|
+
market: str,
|
268
|
+
options: AnyDict | None = None,
|
269
|
+
*,
|
270
|
+
model: type[T] | Any | None = None,
|
271
|
+
schema: dict | None = None,
|
272
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
273
|
+
"""Get order book for a market.
|
274
|
+
|
275
|
+
Returns the list of up to 1000 bids and asks per request for the specified
|
276
|
+
market, sorted by price.
|
277
|
+
|
278
|
+
Endpoint: GET /v2/{market}/book
|
279
|
+
Rate limit weight: 1
|
280
|
+
|
281
|
+
Args:
|
282
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
283
|
+
options: Optional query parameters:
|
284
|
+
- depth (int): Number of bids and asks to return (default: 1000, max: 1000)
|
285
|
+
model: Optional Pydantic model to validate response
|
286
|
+
schema: Optional schema for DataFrame conversion
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
Result containing order book data with structure:
|
290
|
+
{
|
291
|
+
"market": "BTC-EUR",
|
292
|
+
"nonce": 438524,
|
293
|
+
"bids": [["4999.9","0.015"], ...],
|
294
|
+
"asks": [["5001.1","0.015"], ...],
|
295
|
+
"timestamp": 1542967486256
|
296
|
+
}
|
297
|
+
|
298
|
+
Note:
|
299
|
+
This is a public endpoint but authenticating gives higher rate limits.
|
300
|
+
"""
|
301
|
+
# Validate depth parameter if provided
|
302
|
+
if options and "depth" in options:
|
303
|
+
depth = options["depth"]
|
304
|
+
if not isinstance(depth, int) or not (1 <= depth <= MAX_BOOK_DEPTH):
|
305
|
+
msg = f"depth must be an integer between 1 and {MAX_BOOK_DEPTH} (inclusive)"
|
306
|
+
raise ValueError(msg)
|
307
|
+
|
308
|
+
postfix = create_postfix(options)
|
309
|
+
raw_result = self.http.request("GET", f"/{market}/book{postfix}", weight=1)
|
310
|
+
return self._convert_raw_result(raw_result, "book", model, schema)
|
311
|
+
|
312
|
+
def trades(
|
313
|
+
self,
|
314
|
+
market: str,
|
315
|
+
options: AnyDict | None = None,
|
316
|
+
*,
|
317
|
+
model: type[T] | Any | None = None,
|
318
|
+
schema: dict | None = None,
|
319
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
320
|
+
"""Get public trades for a market.
|
321
|
+
|
322
|
+
Returns the list of trades from the specified market and time period made by all Bitvavo users.
|
323
|
+
The returned trades are sorted by their timestamp in descending order (latest to earliest).
|
324
|
+
|
325
|
+
Endpoint: GET /v2/{market}/trades
|
326
|
+
Rate limit weight: 5
|
327
|
+
|
328
|
+
Args:
|
329
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
330
|
+
options: Optional query parameters:
|
331
|
+
- limit: int (1-1000, default 500) - Maximum number of trades to return
|
332
|
+
- start: int - Unix timestamp in milliseconds to start from
|
333
|
+
- end: int - Unix timestamp in milliseconds to end at (max 24h after start)
|
334
|
+
- tradeIdFrom: str - Trade ID to start from
|
335
|
+
- tradeIdTo: str - Trade ID to end at
|
336
|
+
model: Optional Pydantic model to validate response
|
337
|
+
schema: Optional schema for DataFrame conversion
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
Result containing public trades data or error.
|
341
|
+
Each trade contains: id, timestamp, amount, price, side
|
342
|
+
|
343
|
+
Example:
|
344
|
+
>>> client.public.trades("BTC-EUR")
|
345
|
+
>>> client.public.trades("BTC-EUR", {"limit": 100})
|
346
|
+
>>> client.public.trades("BTC-EUR", {"start": 1577836800000, "end": 1577836900000})
|
347
|
+
"""
|
348
|
+
# Validate options if provided
|
349
|
+
if options:
|
350
|
+
self._validate_trades_options(options)
|
351
|
+
|
352
|
+
postfix = create_postfix(options)
|
353
|
+
raw_result = self.http.request("GET", f"/{market}/trades{postfix}", weight=5)
|
354
|
+
return self._convert_raw_result(raw_result, "trades", model, schema)
|
355
|
+
|
356
|
+
def candles(
|
357
|
+
self,
|
358
|
+
market: str,
|
359
|
+
interval: CandleInterval,
|
360
|
+
options: AnyDict | None = None,
|
361
|
+
*,
|
362
|
+
model: type[T] | Any | None = None,
|
363
|
+
schema: dict | None = None,
|
364
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
365
|
+
"""Get candlestick data for a market.
|
366
|
+
|
367
|
+
Args:
|
368
|
+
market: Market symbol
|
369
|
+
interval: Time interval - must be one of: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 1W, 1M
|
370
|
+
options: Optional query parameters (limit, start, end)
|
371
|
+
model: Optional Pydantic model to validate response
|
372
|
+
schema: Optional schema for DataFrame conversion
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
Result containing candlestick data or error
|
376
|
+
|
377
|
+
Raises:
|
378
|
+
ValueError: If interval is invalid or limit is not in range 1-1440 or timestamps are invalid
|
379
|
+
"""
|
380
|
+
# Validate interval parameter at runtime
|
381
|
+
valid_intervals = {"1m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "1W", "1M"}
|
382
|
+
if interval not in valid_intervals:
|
383
|
+
msg = f"interval must be one of: {', '.join(sorted(valid_intervals))}"
|
384
|
+
raise ValueError(msg)
|
385
|
+
|
386
|
+
if options is None:
|
387
|
+
options = {}
|
388
|
+
|
389
|
+
# Validate optional parameters according to Bitvavo API documentation
|
390
|
+
if "limit" in options:
|
391
|
+
limit = options["limit"]
|
392
|
+
if not isinstance(limit, int) or not (1 <= limit <= MAX_CANDLE_LIMIT):
|
393
|
+
msg = f"limit must be an integer between 1 and {MAX_CANDLE_LIMIT} (inclusive)"
|
394
|
+
raise ValueError(msg)
|
395
|
+
|
396
|
+
if "start" in options:
|
397
|
+
start = options["start"]
|
398
|
+
if not isinstance(start, int) or start < 0:
|
399
|
+
msg = "start must be a non-negative unix timestamp in milliseconds"
|
400
|
+
raise ValueError(msg)
|
401
|
+
|
402
|
+
if "end" in options:
|
403
|
+
end = options["end"]
|
404
|
+
if not isinstance(end, int) or end < 0 or end > MAX_TIMESTAMP_VALUE:
|
405
|
+
msg = f"end must be a unix timestamp in milliseconds <= {MAX_TIMESTAMP_VALUE}"
|
406
|
+
raise ValueError(msg)
|
407
|
+
|
408
|
+
options["interval"] = interval
|
409
|
+
postfix = create_postfix(options)
|
410
|
+
raw_result = self.http.request("GET", f"/{market}/candles{postfix}", weight=1)
|
411
|
+
return self._convert_raw_result(raw_result, "candles", model, schema)
|
412
|
+
|
413
|
+
def ticker_price(
|
414
|
+
self,
|
415
|
+
options: AnyDict | None = None,
|
416
|
+
*,
|
417
|
+
model: type[T] | Any | None = None,
|
418
|
+
schema: dict | None = None,
|
419
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
420
|
+
"""Get ticker prices for markets.
|
421
|
+
|
422
|
+
Returns prices of the latest trades on Bitvavo for all markets or a single market.
|
423
|
+
A tick in a market is any change in the price of a digital asset.
|
424
|
+
|
425
|
+
Endpoint: GET /v2/ticker/price
|
426
|
+
Rate limit weight: 1
|
427
|
+
|
428
|
+
Args:
|
429
|
+
options: Optional query parameters:
|
430
|
+
- market (str): The market for which to return the latest information.
|
431
|
+
For the list of all markets, use the markets() method.
|
432
|
+
Example: 'BTC-EUR'
|
433
|
+
model: Optional Pydantic model to validate response
|
434
|
+
schema: Optional schema for DataFrame conversion
|
435
|
+
|
436
|
+
Returns:
|
437
|
+
Result containing ticker price data array:
|
438
|
+
[
|
439
|
+
{
|
440
|
+
"market": "BTC-EUR",
|
441
|
+
"price": "34243"
|
442
|
+
}
|
443
|
+
]
|
444
|
+
|
445
|
+
Note:
|
446
|
+
This is a public endpoint but authenticating gives higher rate limits.
|
447
|
+
"""
|
448
|
+
# Validate market parameter if provided
|
449
|
+
if options and "market" in options:
|
450
|
+
market = options["market"]
|
451
|
+
if not isinstance(market, str) or not market.strip():
|
452
|
+
msg = "market must be a non-empty string"
|
453
|
+
raise ValueError(msg)
|
454
|
+
|
455
|
+
postfix = create_postfix(options)
|
456
|
+
raw_result = self.http.request("GET", f"/ticker/price{postfix}", weight=1)
|
457
|
+
return self._convert_raw_result(raw_result, "ticker_price", model, schema)
|
458
|
+
|
459
|
+
def ticker_book(
|
460
|
+
self,
|
461
|
+
options: AnyDict | None = None,
|
462
|
+
*,
|
463
|
+
model: type[T] | Any | None = None,
|
464
|
+
schema: dict | None = None,
|
465
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
466
|
+
"""Get ticker book.
|
467
|
+
|
468
|
+
Args:
|
469
|
+
options: Optional query parameters
|
470
|
+
model: Optional Pydantic model to validate response
|
471
|
+
schema: Optional schema for DataFrame conversion
|
472
|
+
|
473
|
+
Returns:
|
474
|
+
Result containing ticker book data or error
|
475
|
+
"""
|
476
|
+
postfix = create_postfix(options)
|
477
|
+
raw_result = self.http.request("GET", f"/ticker/book{postfix}", weight=1)
|
478
|
+
return self._convert_raw_result(raw_result, "ticker_book", model, schema)
|
479
|
+
|
480
|
+
def ticker_24h(
|
481
|
+
self,
|
482
|
+
options: AnyDict | None = None,
|
483
|
+
*,
|
484
|
+
model: type[T] | Any | None = None,
|
485
|
+
schema: dict | None = None,
|
486
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
487
|
+
"""Get 24h ticker statistics.
|
488
|
+
|
489
|
+
Rate limit weight points:
|
490
|
+
- All markets: 25
|
491
|
+
|
492
|
+
Args:
|
493
|
+
options: Optional query parameters
|
494
|
+
model: Optional Pydantic model to validate response
|
495
|
+
schema: Optional schema for DataFrame conversion
|
496
|
+
|
497
|
+
Returns:
|
498
|
+
Result containing 24h ticker statistics or error
|
499
|
+
"""
|
500
|
+
if options and "market" in options:
|
501
|
+
msg = "Market parameter is not allowed for 24h ticker statistics; yes, the API supports it, but I don't"
|
502
|
+
raise ValueError(msg)
|
503
|
+
|
504
|
+
postfix = create_postfix(options)
|
505
|
+
raw_result = self.http.request("GET", f"/ticker/24h{postfix}", weight=25)
|
506
|
+
return self._convert_raw_result(raw_result, "ticker_24h", model, schema)
|
507
|
+
|
508
|
+
def report_book(
|
509
|
+
self,
|
510
|
+
market: str,
|
511
|
+
options: AnyDict | None = None,
|
512
|
+
*,
|
513
|
+
model: type[T] | Any | None = None,
|
514
|
+
schema: dict | None = None,
|
515
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
516
|
+
"""Get MiCA-compliant order book report for a market.
|
517
|
+
|
518
|
+
Returns the list of all bids and asks for the specified market, sorted by price.
|
519
|
+
Includes data compliant with the European Markets in Crypto-Assets (MiCA) regulation.
|
520
|
+
|
521
|
+
Endpoint: GET /v2/report/{market}/book
|
522
|
+
Rate limit weight: 1
|
523
|
+
|
524
|
+
Args:
|
525
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
526
|
+
options: Optional query parameters:
|
527
|
+
- depth (int): Number of bids and asks to return (default: 1000, max: 1000)
|
528
|
+
model: Optional Pydantic model to validate response
|
529
|
+
schema: Optional schema for DataFrame conversion
|
530
|
+
|
531
|
+
Returns:
|
532
|
+
Result containing MiCA-compliant order book report with enhanced structure:
|
533
|
+
{
|
534
|
+
"submissionTimestamp": "2025-05-02T14:23:11.123456Z",
|
535
|
+
"assetCode": "4K6P57CMJ",
|
536
|
+
"assetName": "Bitcoin",
|
537
|
+
"bids": [
|
538
|
+
{
|
539
|
+
"side": "BUYI",
|
540
|
+
"price": "28500.12",
|
541
|
+
"quantity": "0.5",
|
542
|
+
"numOrders": 12
|
543
|
+
}
|
544
|
+
],
|
545
|
+
"asks": [
|
546
|
+
{
|
547
|
+
"side": "SELL",
|
548
|
+
"price": "28510.00",
|
549
|
+
"quantity": "0.4",
|
550
|
+
"numOrders": 9
|
551
|
+
}
|
552
|
+
],
|
553
|
+
"priceCurrency": "4K6P57CMJ",
|
554
|
+
"priceNotation": "MONE",
|
555
|
+
"quantityCurrency": "EUR",
|
556
|
+
"quantityNotation": "CRYP",
|
557
|
+
"venue": "VAVO",
|
558
|
+
"tradingSystem": "VAVO",
|
559
|
+
"publicationTimestamp": "2025-05-02T14:23:11.123456Z"
|
560
|
+
}
|
561
|
+
|
562
|
+
Note:
|
563
|
+
This is a public endpoint but authenticating gives higher rate limits.
|
564
|
+
The response structure is different from the regular order book endpoint
|
565
|
+
and includes additional MiCA compliance fields.
|
566
|
+
"""
|
567
|
+
# Validate depth parameter if provided
|
568
|
+
if options and "depth" in options:
|
569
|
+
depth = options["depth"]
|
570
|
+
if not isinstance(depth, int) or not (1 <= depth <= MAX_BOOK_REPORT_DEPTH):
|
571
|
+
msg = f"depth must be an integer between 1 and {MAX_BOOK_REPORT_DEPTH} (inclusive)"
|
572
|
+
raise ValueError(msg)
|
573
|
+
|
574
|
+
postfix = create_postfix(options)
|
575
|
+
raw_result = self.http.request("GET", f"/report/{market}/book{postfix}", weight=1)
|
576
|
+
return self._convert_raw_result(raw_result, "report_book", model, schema)
|
577
|
+
|
578
|
+
def report_trades(
|
579
|
+
self,
|
580
|
+
market: str,
|
581
|
+
options: AnyDict | None = None,
|
582
|
+
*,
|
583
|
+
model: type[T] | Any | None = None,
|
584
|
+
schema: dict | None = None,
|
585
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
586
|
+
"""Get MiCA-compliant trades report for a market.
|
587
|
+
|
588
|
+
Returns trades from the specified market and time period made by all Bitvavo users.
|
589
|
+
The returned trades are sorted by timestamp in descending order (latest to earliest).
|
590
|
+
Includes data compliant with the European Markets in Crypto-Assets (MiCA) regulation.
|
591
|
+
|
592
|
+
Endpoint: GET /v2/report/{market}/trades
|
593
|
+
Rate limit weight: 5
|
594
|
+
|
595
|
+
Args:
|
596
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
597
|
+
options: Optional query parameters:
|
598
|
+
- limit: int (1-1000, default 500) - Maximum number of trades to return
|
599
|
+
- start: int - Unix timestamp in milliseconds to start from
|
600
|
+
- end: int - Unix timestamp in milliseconds to end at (max 24h after start)
|
601
|
+
- tradeIdFrom: str - Trade ID to start from
|
602
|
+
- tradeIdTo: str - Trade ID to end at
|
603
|
+
model: Optional Pydantic model to validate response
|
604
|
+
schema: Optional schema for DataFrame conversion
|
605
|
+
|
606
|
+
Returns:
|
607
|
+
Result containing MiCA-compliant trades report with enhanced structure:
|
608
|
+
- tradeId: Unique identifier of the trade
|
609
|
+
- transactTimestamp: ISO 8601 timestamp when trade was added to database
|
610
|
+
- assetCode: DTI code or symbol of the asset
|
611
|
+
- assetName: Full name of the asset
|
612
|
+
- price: Price of 1 unit of base currency in quote currency
|
613
|
+
- missingPrice: Indicates if price is pending (PNDG) or not applicable (NOAP)
|
614
|
+
- priceNotation: Price expression type (MONE)
|
615
|
+
- priceCurrency: Currency in which price is expressed
|
616
|
+
- quantity: Quantity of the asset
|
617
|
+
- quantityCurrency: Currency in which quantity is expressed
|
618
|
+
- quantityNotation: Quantity expression type (CRYP)
|
619
|
+
- venue: Market Identifier Code of Bitvavo trading platform (VAVO)
|
620
|
+
- publicationTimestamp: ISO 8601 timestamp when trade was published
|
621
|
+
- publicationVenue: Market Identifier Code of publishing platform (VAVO)
|
622
|
+
|
623
|
+
Example:
|
624
|
+
>>> client.public.report_trades("BTC-EUR", {"limit": 100})
|
625
|
+
Success([...])
|
626
|
+
"""
|
627
|
+
postfix = create_postfix(options)
|
628
|
+
raw_result = self.http.request("GET", f"/report/{market}/trades{postfix}", weight=5)
|
629
|
+
return self._convert_raw_result(raw_result, "report_trades", model, schema)
|
630
|
+
|
631
|
+
def _validate_trades_options(self, options: AnyDict) -> None:
|
632
|
+
"""Validate options for the trades endpoint according to Bitvavo API documentation.
|
633
|
+
|
634
|
+
Args:
|
635
|
+
options: Dictionary of query parameters to validate
|
636
|
+
|
637
|
+
Raises:
|
638
|
+
ValueError: If any parameter violates Bitvavo's constraints
|
639
|
+
"""
|
640
|
+
if "limit" in options:
|
641
|
+
limit = options["limit"]
|
642
|
+
if not isinstance(limit, int) or limit < 1 or limit > MAX_TRADES_LIMIT:
|
643
|
+
msg = f"limit must be an integer between 1 and {MAX_TRADES_LIMIT}"
|
644
|
+
raise ValueError(msg)
|
645
|
+
|
646
|
+
if "start" in options and "end" in options:
|
647
|
+
start = options["start"]
|
648
|
+
end = options["end"]
|
649
|
+
# Check 24-hour constraint combined with type check
|
650
|
+
if isinstance(start, int) and isinstance(end, int) and end - start > MAX_24_HOUR_MS:
|
651
|
+
msg = "end timestamp cannot be more than 24 hours after start timestamp"
|
652
|
+
raise ValueError(msg)
|
653
|
+
|
654
|
+
if "end" in options:
|
655
|
+
end = options["end"]
|
656
|
+
if isinstance(end, int) and end > MAX_END_TIMESTAMP:
|
657
|
+
msg = f"end timestamp cannot exceed {MAX_END_TIMESTAMP}"
|
658
|
+
raise ValueError(msg)
|