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.
@@ -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)