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