bitvavo-api-upgraded 2.0.0__py3-none-any.whl → 2.2.0__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.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import datetime as dt
4
5
  import hashlib
5
6
  import hmac
@@ -14,9 +15,10 @@ from requests import delete, get, post, put
14
15
  from structlog.stdlib import get_logger
15
16
  from websocket import WebSocketApp # missing stubs for WebSocketApp
16
17
 
18
+ from bitvavo_api_upgraded.dataframe_utils import convert_candles_to_dataframe, convert_to_dataframe
17
19
  from bitvavo_api_upgraded.helper_funcs import configure_loggers, time_ms, time_to_wait
18
- from bitvavo_api_upgraded.settings import bitvavo_upgraded_settings
19
- from bitvavo_api_upgraded.type_aliases import anydict, errordict, intdict, ms, s_f, strdict, strintdict
20
+ from bitvavo_api_upgraded.settings import bitvavo_settings, bitvavo_upgraded_settings
21
+ from bitvavo_api_upgraded.type_aliases import OutputFormat, anydict, errordict, intdict, ms, s_f, strdict, strintdict
20
22
 
21
23
  configure_loggers()
22
24
 
@@ -216,6 +218,7 @@ class Bitvavo:
216
218
  Example code to get your started:
217
219
 
218
220
  ```python
221
+ # Single API key (backward compatible)
219
222
  bitvavo = Bitvavo(
220
223
  {
221
224
  "APIKEY": "$YOUR_API_KEY",
@@ -227,22 +230,198 @@ class Bitvavo:
227
230
  },
228
231
  )
229
232
  time_dict = bitvavo.time()
233
+
234
+ # Multiple API keys with keyless preference
235
+ bitvavo = Bitvavo(
236
+ {
237
+ "APIKEYS": [
238
+ {"key": "$YOUR_API_KEY_1", "secret": "$YOUR_API_SECRET_1"},
239
+ {"key": "$YOUR_API_KEY_2", "secret": "$YOUR_API_SECRET_2"},
240
+ {"key": "$YOUR_API_KEY_3", "secret": "$YOUR_API_SECRET_3"},
241
+ ],
242
+ "PREFER_KEYLESS": True, # Use keyless requests first, then API keys
243
+ "RESTURL": "https://api.bitvavo.com/v2",
244
+ "WSURL": "wss://ws.bitvavo.com/v2/",
245
+ "ACCESSWINDOW": 10000,
246
+ "DEBUGGING": True,
247
+ },
248
+ )
249
+ time_dict = bitvavo.time()
250
+
251
+ # Keyless only (no API keys)
252
+ bitvavo = Bitvavo(
253
+ {
254
+ "PREFER_KEYLESS": True,
255
+ "RESTURL": "https://api.bitvavo.com/v2",
256
+ "WSURL": "wss://ws.bitvavo.com/v2/",
257
+ "ACCESSWINDOW": 10000,
258
+ "DEBUGGING": True,
259
+ },
260
+ )
261
+ markets = bitvavo.markets() # Only public endpoints will work
230
262
  ```
231
263
  """
232
264
 
233
- def __init__(self, options: dict[str, str | int] | None = None) -> None:
265
+ def __init__(self, options: dict[str, str | int | list[dict[str, str]]] | None = None) -> None:
234
266
  if options is None:
235
267
  options = {}
236
268
  _options = {k.upper(): v for k, v in options.items()}
237
- self.base: str = str(_options.get("RESTURL", "https://api.bitvavo.com/v2"))
238
- self.wsUrl: str = str(_options.get("WSURL", "wss://ws.bitvavo.com/v2/"))
239
- self.ACCESSWINDOW = ms(_options.get("ACCESSWINDOW", 10000))
240
- self.APIKEY = str(_options.get("APIKEY", ""))
241
- self.APISECRET = str(_options.get("APISECRET", ""))
242
- self.rateLimitRemaining: int = 1000
269
+
270
+ # Options take precedence over settings
271
+ self.base: str = str(_options.get("RESTURL", bitvavo_settings.RESTURL))
272
+ self.wsUrl: str = str(_options.get("WSURL", bitvavo_settings.WSURL))
273
+ self.ACCESSWINDOW: int = int(_options.get("ACCESSWINDOW", bitvavo_settings.ACCESSWINDOW))
274
+
275
+ # Support for multiple API keys - options take absolute precedence
276
+ if "APIKEY" in _options and "APISECRET" in _options:
277
+ # Single API key explicitly provided in options - takes precedence
278
+ single_key = str(_options["APIKEY"])
279
+ single_secret = str(_options["APISECRET"])
280
+ self.api_keys: list[dict[str, str]] = [{"key": single_key, "secret": single_secret}]
281
+ elif "APIKEYS" in _options:
282
+ # Multiple API keys provided in options - takes precedence
283
+ api_keys = _options["APIKEYS"]
284
+ if isinstance(api_keys, list) and api_keys:
285
+ self.api_keys = api_keys
286
+ else:
287
+ self.api_keys = []
288
+ else:
289
+ # Fall back to settings only if no API key options provided
290
+ api_keys = bitvavo_settings.APIKEYS
291
+ if isinstance(api_keys, list) and api_keys:
292
+ self.api_keys = api_keys
293
+ else:
294
+ # Single API key from settings (backward compatibility)
295
+ single_key = str(bitvavo_settings.APIKEY)
296
+ single_secret = str(bitvavo_settings.APISECRET)
297
+ if single_key and single_secret:
298
+ self.api_keys = [{"key": single_key, "secret": single_secret}]
299
+ else:
300
+ self.api_keys = []
301
+
302
+ # Current API key index and keyless preference - options take precedence
303
+ self.current_api_key_index: int = 0
304
+ self.prefer_keyless: bool = bool(_options.get("PREFER_KEYLESS", bitvavo_upgraded_settings.PREFER_KEYLESS))
305
+
306
+ # Rate limiting per API key (keyless has index -1)
307
+ self.rate_limits: dict[int, dict[str, int | ms]] = {}
308
+ # Get default rate limit from options or settings
309
+ default_rate_limit_option = _options.get("DEFAULT_RATE_LIMIT", bitvavo_upgraded_settings.DEFAULT_RATE_LIMIT)
310
+ default_rate_limit = (
311
+ int(default_rate_limit_option)
312
+ if isinstance(default_rate_limit_option, (int, str))
313
+ else bitvavo_upgraded_settings.DEFAULT_RATE_LIMIT
314
+ )
315
+
316
+ self.rate_limits[-1] = {"remaining": default_rate_limit, "resetAt": ms(0)} # keyless
317
+ for i in range(len(self.api_keys)):
318
+ self.rate_limits[i] = {"remaining": default_rate_limit, "resetAt": ms(0)}
319
+
320
+ # Legacy properties for backward compatibility
321
+ self.APIKEY: str = self.api_keys[0]["key"] if self.api_keys else ""
322
+ self.APISECRET: str = self.api_keys[0]["secret"] if self.api_keys else ""
323
+ self._current_api_key: str = self.APIKEY
324
+ self._current_api_secret: str = self.APISECRET
325
+ self.rateLimitRemaining: int = default_rate_limit
243
326
  self.rateLimitResetAt: ms = 0
244
- # TODO(NostraDavid): for v2: remove this functionality - logger.debug is a level that can be set
245
- self.debugging = bool(_options.get("DEBUGGING", False))
327
+
328
+ # Options take precedence over settings for debugging
329
+ self.debugging: bool = bool(_options.get("DEBUGGING", bitvavo_settings.DEBUGGING))
330
+
331
+ def get_best_api_key_config(self, rateLimitingWeight: int = 1) -> tuple[str, str, int]:
332
+ """
333
+ Get the best API key configuration to use for a request.
334
+
335
+ Returns:
336
+ tuple: (api_key, api_secret, key_index) where key_index is -1 for keyless
337
+ """
338
+ # If prefer keyless and keyless has enough rate limit, use keyless
339
+ if self.prefer_keyless and self._has_rate_limit_available(-1, rateLimitingWeight):
340
+ return "", "", -1
341
+
342
+ # Try to find an API key with enough rate limit
343
+ for i in range(len(self.api_keys)):
344
+ if self._has_rate_limit_available(i, rateLimitingWeight):
345
+ return self.api_keys[i]["key"], self.api_keys[i]["secret"], i
346
+
347
+ # If keyless is available, use it as fallback
348
+ if self._has_rate_limit_available(-1, rateLimitingWeight):
349
+ return "", "", -1
350
+
351
+ # No keys available, use current key and let rate limiting handle the wait
352
+ if self.api_keys:
353
+ return (
354
+ self.api_keys[self.current_api_key_index]["key"],
355
+ self.api_keys[self.current_api_key_index]["secret"],
356
+ self.current_api_key_index,
357
+ )
358
+ return "", "", -1
359
+
360
+ def _has_rate_limit_available(self, key_index: int, weight: int) -> bool:
361
+ """Check if a specific API key (or keyless) has enough rate limit."""
362
+ if key_index not in self.rate_limits:
363
+ return False
364
+ remaining = self.rate_limits[key_index]["remaining"]
365
+ return (remaining - weight) > bitvavo_upgraded_settings.RATE_LIMITING_BUFFER
366
+
367
+ def _update_rate_limit_for_key(self, key_index: int, response: anydict | errordict) -> None:
368
+ """Update rate limit for a specific API key index."""
369
+ if key_index not in self.rate_limits:
370
+ self.rate_limits[key_index] = {"remaining": 1000, "resetAt": ms(0)}
371
+
372
+ if "errorCode" in response and response["errorCode"] == 105: # noqa: PLR2004
373
+ self.rate_limits[key_index]["remaining"] = 0
374
+ # rateLimitResetAt is a value that's stripped from a string.
375
+ reset_time_str = str(response.get("error", "")).split(" at ")
376
+ if len(reset_time_str) > 1:
377
+ try:
378
+ reset_time = ms(int(reset_time_str[1].split(".")[0]))
379
+ self.rate_limits[key_index]["resetAt"] = reset_time
380
+ except (ValueError, IndexError):
381
+ # Fallback to current time + 60 seconds if parsing fails
382
+ self.rate_limits[key_index]["resetAt"] = ms(time_ms() + 60000)
383
+ else:
384
+ self.rate_limits[key_index]["resetAt"] = ms(time_ms() + 60000)
385
+
386
+ timeToWait = time_to_wait(ms(self.rate_limits[key_index]["resetAt"]))
387
+ key_name = f"API_KEY_{key_index}" if key_index >= 0 else "KEYLESS"
388
+ logger.warning(
389
+ "api-key-banned",
390
+ info={
391
+ "key_name": key_name,
392
+ "wait_time_seconds": timeToWait + 1,
393
+ "until": (dt.datetime.now(tz=dt.timezone.utc) + dt.timedelta(seconds=timeToWait + 1)).isoformat(),
394
+ },
395
+ )
396
+
397
+ if "bitvavo-ratelimit-remaining" in response:
398
+ with contextlib.suppress(ValueError, TypeError):
399
+ self.rate_limits[key_index]["remaining"] = int(response["bitvavo-ratelimit-remaining"])
400
+
401
+ if "bitvavo-ratelimit-resetat" in response:
402
+ with contextlib.suppress(ValueError, TypeError):
403
+ self.rate_limits[key_index]["resetAt"] = ms(int(response["bitvavo-ratelimit-resetat"]))
404
+
405
+ def _sleep_for_key(self, key_index: int) -> None:
406
+ """Sleep until the specified API key's rate limit resets."""
407
+ if key_index not in self.rate_limits:
408
+ return
409
+
410
+ reset_at = ms(self.rate_limits[key_index]["resetAt"])
411
+ napTime = time_to_wait(reset_at)
412
+ key_name = f"API_KEY_{key_index}" if key_index >= 0 else "KEYLESS"
413
+
414
+ logger.warning(
415
+ "rate-limit-reached", key_name=key_name, rateLimitRemaining=self.rate_limits[key_index]["remaining"]
416
+ )
417
+ logger.info(
418
+ "napping-until-reset",
419
+ key_name=key_name,
420
+ napTime=napTime,
421
+ currentTime=dt.datetime.now(tz=dt.timezone.utc).isoformat(),
422
+ targetDatetime=dt.datetime.fromtimestamp(reset_at / 1000.0, tz=dt.timezone.utc).isoformat(),
423
+ )
424
+ time.sleep(napTime + 1) # +1 to add a tiny bit of buffer time
246
425
 
247
426
  def calcLag(self) -> ms:
248
427
  """
@@ -284,14 +463,20 @@ class Bitvavo:
284
463
  If you're banned, use the errordict to sleep until you're not banned
285
464
 
286
465
  If you're not banned, then use the received headers to update the variables.
466
+
467
+ This method maintains backward compatibility by updating the legacy properties.
287
468
  """
469
+ # Update rate limit for the current API key being used
470
+ current_key = self.current_api_key_index if self.APIKEY else -1
471
+ self._update_rate_limit_for_key(current_key, response)
472
+
473
+ # Update legacy properties for backward compatibility
474
+ if current_key in self.rate_limits:
475
+ self.rateLimitRemaining = int(self.rate_limits[current_key]["remaining"])
476
+ self.rateLimitResetAt = ms(self.rate_limits[current_key]["resetAt"])
477
+
478
+ # Handle ban with sleep (legacy behavior)
288
479
  if "errorCode" in response and response["errorCode"] == 105: # noqa: PLR2004
289
- self.rateLimitRemaining = 0
290
- # rateLimitResetAt is a value that's stripped from a string.
291
- # Kind of a terrible way to pass that information, but eh, whatever, I guess...
292
- # Anyway, here is the string that's being pulled apart:
293
- # "Your IP or API key has been banned for not respecting the rate limit. The ban expires at ${expiryInMs}""
294
- self.rateLimitResetAt = ms(response["error"].split(" at ")[1].split(".")[0])
295
480
  timeToWait = time_to_wait(self.rateLimitResetAt)
296
481
  logger.warning(
297
482
  "banned",
@@ -302,10 +487,6 @@ class Bitvavo:
302
487
  )
303
488
  logger.info("napping-until-ban-lifted")
304
489
  time.sleep(timeToWait + 1) # plus one second to ENSURE we're able to run again.
305
- if "bitvavo-ratelimit-remaining" in response:
306
- self.rateLimitRemaining = int(response["bitvavo-ratelimit-remaining"])
307
- if "bitvavo-ratelimit-resetat" in response:
308
- self.rateLimitResetAt = int(response["bitvavo-ratelimit-resetat"])
309
490
 
310
491
  def publicRequest(
311
492
  self,
@@ -330,22 +511,39 @@ class Bitvavo:
330
511
  list[list[str]]
331
512
  ```
332
513
  """
333
- if (self.rateLimitRemaining - rateLimitingWeight) <= bitvavo_upgraded_settings.RATE_LIMITING_BUFFER:
334
- self.sleep_until_can_continue()
514
+ # Get the best API key configuration (keyless preferred, then available keys)
515
+ api_key, api_secret, key_index = self.get_best_api_key_config(rateLimitingWeight)
516
+
517
+ # Check if we need to wait for rate limit
518
+ if not self._has_rate_limit_available(key_index, rateLimitingWeight):
519
+ self._sleep_for_key(key_index)
520
+
521
+ # Update current API key for legacy compatibility
522
+ if api_key:
523
+ self._current_api_key = api_key
524
+ self._current_api_secret = api_secret
525
+ self.current_api_key_index = key_index
526
+ else:
527
+ # Using keyless
528
+ self._current_api_key = ""
529
+ self._current_api_secret = ""
530
+
335
531
  if self.debugging:
336
532
  logger.debug(
337
533
  "api-request",
338
534
  info={
339
535
  "url": url,
340
- "with_api_key": bool(self.APIKEY != ""),
536
+ "with_api_key": bool(api_key != ""),
341
537
  "public_or_private": "public",
538
+ "key_index": key_index,
342
539
  },
343
540
  )
344
- if self.APIKEY != "":
541
+
542
+ if api_key:
345
543
  now = time_ms() + bitvavo_upgraded_settings.LAG
346
- sig = createSignature(now, "GET", url.replace(self.base, ""), None, self.APISECRET)
544
+ sig = createSignature(now, "GET", url.replace(self.base, ""), None, api_secret)
347
545
  headers = {
348
- "bitvavo-access-key": self.APIKEY,
546
+ "bitvavo-access-key": api_key,
349
547
  "bitvavo-access-signature": sig,
350
548
  "bitvavo-access-timestamp": str(now),
351
549
  "bitvavo-access-window": str(self.ACCESSWINDOW),
@@ -353,10 +551,16 @@ class Bitvavo:
353
551
  r = get(url, headers=headers, timeout=(self.ACCESSWINDOW / 1000))
354
552
  else:
355
553
  r = get(url, timeout=(self.ACCESSWINDOW / 1000))
554
+
555
+ # Update rate limit for the specific key used
356
556
  if "error" in r.json():
357
- self.updateRateLimit(r.json())
557
+ self._update_rate_limit_for_key(key_index, r.json())
358
558
  else:
359
- self.updateRateLimit(dict(r.headers))
559
+ self._update_rate_limit_for_key(key_index, dict(r.headers))
560
+
561
+ # Also update legacy rate limit tracking
562
+ self.updateRateLimit(r.json() if "error" in r.json() else dict(r.headers))
563
+
360
564
  return r.json() # type:ignore[no-any-return]
361
565
 
362
566
  def privateRequest(
@@ -366,7 +570,7 @@ class Bitvavo:
366
570
  body: anydict | None = None,
367
571
  method: str = "GET",
368
572
  rateLimitingWeight: int = 1,
369
- ) -> list[anydict] | list[list[str]] | intdict | strdict | anydict | Any | errordict:
573
+ ) -> list[anydict] | list[list[str]] | intdict | strdict | anydict | errordict:
370
574
  """Execute a request to the private part of the API. API key and SECRET are required.
371
575
  Will return the reponse as one of three types.
372
576
 
@@ -389,14 +593,33 @@ class Bitvavo:
389
593
  list[list[str]]
390
594
  ```
391
595
  """
392
- if (self.rateLimitRemaining - rateLimitingWeight) <= bitvavo_upgraded_settings.RATE_LIMITING_BUFFER:
393
- self.sleep_until_can_continue()
394
- # if this method breaks: add `= {}` after `body: dict`
596
+ # Private requests require an API key, so get the best available one
597
+ api_key, api_secret, key_index = self.get_best_api_key_config(rateLimitingWeight)
598
+
599
+ # If no API keys available, use the configured one (may fail)
600
+ if not api_key and self.api_keys:
601
+ api_key = self.api_keys[self.current_api_key_index]["key"]
602
+ api_secret = self.api_keys[self.current_api_key_index]["secret"]
603
+ key_index = self.current_api_key_index
604
+ elif not api_key:
605
+ # No API keys configured at all
606
+ api_key = self.APIKEY
607
+ api_secret = self.APISECRET
608
+ key_index = 0 if api_key else -1
609
+
610
+ # Check if we need to wait for rate limit
611
+ if not self._has_rate_limit_available(key_index, rateLimitingWeight):
612
+ self._sleep_for_key(key_index)
613
+
614
+ # Update current API key for legacy compatibility
615
+ self._current_api_key = api_key
616
+ self._current_api_secret = api_secret
617
+
395
618
  now = time_ms() + bitvavo_upgraded_settings.LAG
396
- sig = createSignature(now, method, (endpoint + postfix), body, self.APISECRET)
619
+ sig = createSignature(now, method, (endpoint + postfix), body, api_secret)
397
620
  url = self.base + endpoint + postfix
398
621
  headers = {
399
- "bitvavo-access-key": self.APIKEY,
622
+ "bitvavo-access-key": api_key,
400
623
  "bitvavo-access-signature": sig,
401
624
  "bitvavo-access-timestamp": str(now),
402
625
  "bitvavo-access-window": str(self.ACCESSWINDOW),
@@ -406,9 +629,10 @@ class Bitvavo:
406
629
  "api-request",
407
630
  info={
408
631
  "url": url,
409
- "with_api_key": bool(self.APIKEY != ""),
632
+ "with_api_key": bool(api_key != ""),
410
633
  "public_or_private": "private",
411
634
  "method": method,
635
+ "key_index": key_index,
412
636
  },
413
637
  )
414
638
  if method == "DELETE":
@@ -419,10 +643,16 @@ class Bitvavo:
419
643
  r = put(url, headers=headers, json=body, timeout=(self.ACCESSWINDOW / 1000))
420
644
  else: # method == "GET"
421
645
  r = get(url, headers=headers, timeout=(self.ACCESSWINDOW / 1000))
646
+
647
+ # Update rate limit for the specific key used
422
648
  if "error" in r.json():
423
- self.updateRateLimit(r.json())
649
+ self._update_rate_limit_for_key(key_index, r.json())
424
650
  else:
425
- self.updateRateLimit(dict(r.headers))
651
+ self._update_rate_limit_for_key(key_index, dict(r.headers))
652
+
653
+ # Also update legacy rate limit tracking
654
+ self.updateRateLimit(r.json() if "error" in r.json() else dict(r.headers))
655
+
426
656
  return r.json()
427
657
 
428
658
  def sleep_until_can_continue(self) -> None:
@@ -457,7 +687,11 @@ class Bitvavo:
457
687
  """
458
688
  return self.publicRequest(f"{self.base}/time") # type: ignore[return-value]
459
689
 
460
- def markets(self, options: strdict | None = None) -> list[anydict] | anydict | errordict:
690
+ def markets(
691
+ self,
692
+ options: strdict | None = None,
693
+ output_format: OutputFormat = OutputFormat.DICT,
694
+ ) -> list[anydict] | anydict | errordict | Any:
461
695
  """Get all available markets with some meta-information, unless options is given a `market` key.
462
696
  Then you will get a single market, instead of a list of markets.
463
697
 
@@ -474,6 +708,23 @@ class Bitvavo:
474
708
  options={} # returns all markets
475
709
  options={"market": "BTC-EUR"} # returns only the BTC-EUR market
476
710
  # If you want multiple markets, but not all, make multiple calls
711
+
712
+ # Output format selection:
713
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
714
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
715
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
716
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
717
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
718
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
719
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
720
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
721
+ output_format=OutputFormat.IBIS # Returns Ibis expression
722
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
723
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
724
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
725
+
726
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
727
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
477
728
  ```
478
729
 
479
730
  ---
@@ -485,6 +736,7 @@ class Bitvavo:
485
736
  ---
486
737
  Returns:
487
738
  ```python
739
+ # When output_format=OutputFormat.DICT (default):
488
740
  [
489
741
  {
490
742
  "market": "BTC-EUR",
@@ -504,12 +756,22 @@ class Bitvavo:
504
756
  ]
505
757
  }
506
758
  ]
759
+
760
+ # When output_format is any DataFrame format (pandas, polars, cudf, etc.):
761
+ # Returns a DataFrame with columns: market, status, base, quote, pricePrecision,
762
+ # minOrderInQuoteAsset, minOrderInBaseAsset, orderTypes
763
+ # The specific DataFrame type depends on the selected format.
507
764
  ```
508
765
  """
509
766
  postfix = createPostfix(options)
510
- return self.publicRequest(f"{self.base}/markets{postfix}") # type: ignore[return-value]
767
+ result = self.publicRequest(f"{self.base}/markets{postfix}") # type: ignore[return-value]
768
+ return convert_to_dataframe(result, output_format)
511
769
 
512
- def assets(self, options: strdict | None = None) -> list[anydict] | anydict:
770
+ def assets(
771
+ self,
772
+ options: strdict | None = None,
773
+ output_format: OutputFormat = OutputFormat.DICT,
774
+ ) -> list[anydict] | anydict | Any:
513
775
  """Get all available assets, unless `options` is given a `symbol` key.
514
776
  Then you will get a single asset, instead of a list of assets.
515
777
 
@@ -527,6 +789,23 @@ class Bitvavo:
527
789
  # pick one
528
790
  options={} # returns all assets
529
791
  options={"symbol": "BTC"} # returns a single asset (the one of Bitcoin)
792
+
793
+ # Output format selection:
794
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
795
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
796
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
797
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
798
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
799
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
800
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
801
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
802
+ output_format=OutputFormat.IBIS # Returns Ibis expression
803
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
804
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
805
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
806
+
807
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
808
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
530
809
  ```
531
810
 
532
811
  ---
@@ -538,6 +817,7 @@ class Bitvavo:
538
817
  ---
539
818
  Returns:
540
819
  ```python
820
+ # When output_format=OutputFormat.DICT (default):
541
821
  [
542
822
  {
543
823
  "symbol": "BTC",
@@ -553,10 +833,17 @@ class Bitvavo:
553
833
  "message": ""
554
834
  }
555
835
  ]
836
+
837
+ # When output_format is any DataFrame format (pandas, polars, cudf, etc.):
838
+ # Returns a DataFrame with columns: symbol, name, decimals, depositFee,
839
+ # depositConfirmations, depositStatus, withdrawalFee, withdrawalMinAmount,
840
+ # withdrawalStatus, networks, message
841
+ # The specific DataFrame type depends on the selected format.
556
842
  ```
557
843
  """
558
844
  postfix = createPostfix(options)
559
- return self.publicRequest(f"{self.base}/assets{postfix}") # type: ignore[return-value]
845
+ result = self.publicRequest(f"{self.base}/assets{postfix}") # type: ignore[return-value]
846
+ return convert_to_dataframe(result, output_format)
560
847
 
561
848
  def book(self, market: str, options: intdict | None = None) -> dict[str, str | int | list[str]] | errordict:
562
849
  """Get a book (with two lists: asks and bids, as they're called)
@@ -603,7 +890,12 @@ class Bitvavo:
603
890
  postfix = createPostfix(options)
604
891
  return self.publicRequest(f"{self.base}/{market}/book{postfix}") # type: ignore[return-value]
605
892
 
606
- def publicTrades(self, market: str, options: strintdict | None = None) -> list[anydict] | errordict:
893
+ def publicTrades(
894
+ self,
895
+ market: str,
896
+ options: strintdict | None = None,
897
+ output_format: OutputFormat = OutputFormat.DICT,
898
+ ) -> list[anydict] | errordict | Any:
607
899
  """Publically available trades
608
900
 
609
901
  ---
@@ -628,6 +920,23 @@ class Bitvavo:
628
920
  "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
629
921
  "tradeIdTo": "" # if you get a list and want everything BEFORE a certain id, put that id here
630
922
  }
923
+
924
+ # Output format selection:
925
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
926
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
927
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
928
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
929
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
930
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
931
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
932
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
933
+ output_format=OutputFormat.IBIS # Returns Ibis expression
934
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
935
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
936
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
937
+
938
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
939
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
631
940
  ```
632
941
 
633
942
  ---
@@ -639,6 +948,7 @@ class Bitvavo:
639
948
  ---
640
949
  Returns:
641
950
  ```python
951
+ # When output_format='dict' (default):
642
952
  [
643
953
  {
644
954
  "timestamp": 1542967486256,
@@ -648,10 +958,14 @@ class Bitvavo:
648
958
  "side": "sell"
649
959
  }
650
960
  ]
961
+
962
+ # When output_format is any DataFrame format:
963
+ # Returns the above data as a DataFrame in the requested format (pandas, polars, etc.)
651
964
  ```
652
965
  """
653
966
  postfix = createPostfix(options)
654
- return self.publicRequest(f"{self.base}/{market}/trades{postfix}", 5) # type: ignore[return-value]
967
+ result = self.publicRequest(f"{self.base}/{market}/trades{postfix}", 5) # type: ignore[return-value]
968
+ return convert_to_dataframe(result, output_format)
655
969
 
656
970
  def candles(
657
971
  self,
@@ -661,7 +975,8 @@ class Bitvavo:
661
975
  limit: int | None = None,
662
976
  start: dt.datetime | None = None,
663
977
  end: dt.datetime | None = None,
664
- ) -> list[list[str]] | errordict:
978
+ output_format: OutputFormat = OutputFormat.DICT,
979
+ ) -> list[list[str]] | errordict | Any:
665
980
  """Get up to 1440 candles for a market, with a specific interval (candle size)
666
981
 
667
982
  Extra reading material: https://en.wikipedia.org/wiki/Candlestick_chart
@@ -684,6 +999,23 @@ class Bitvavo:
684
999
  "start": int timestamp in ms >= 0
685
1000
  "end": int timestamp in ms <= 8640000000000000
686
1001
  }
1002
+
1003
+ # Output format selection:
1004
+ output_format=OutputFormat.DICT # Default: returns standard Python list/dict
1005
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1006
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1007
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1008
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1009
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1010
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1011
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1012
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1013
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1014
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1015
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1016
+
1017
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
1018
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
687
1019
  ```
688
1020
 
689
1021
  ---
@@ -695,6 +1027,7 @@ class Bitvavo:
695
1027
  ---
696
1028
  Returns:
697
1029
  ```python
1030
+ # When output_format='dict' (default):
698
1031
  [
699
1032
  # For whatever reason, you're getting a list of lists; no keys,
700
1033
  # so here is the explanation of what's what.
@@ -705,6 +1038,11 @@ class Bitvavo:
705
1038
  [1640804400000, "41937", "41955", "41449", "41540", "23.64498292"],
706
1039
  [1640800800000, "41955", "42163", "41807", "41939", "10.40093845"],
707
1040
  ]
1041
+
1042
+ # When output_format is any DataFrame format:
1043
+ # Returns the above data as a DataFrame in the requested format (pandas, polars, etc.)
1044
+ # with columns: timestamp, open, high, low, close, volume
1045
+ # timestamp is converted to datetime, numeric columns to float
708
1046
  ```
709
1047
  """
710
1048
  options = _default(options, {})
@@ -716,9 +1054,14 @@ class Bitvavo:
716
1054
  if end is not None:
717
1055
  options["end"] = _epoch_millis(end)
718
1056
  postfix = createPostfix(options)
719
- return self.publicRequest(f"{self.base}/{market}/candles{postfix}") # type: ignore[return-value]
1057
+ result = self.publicRequest(f"{self.base}/{market}/candles{postfix}") # type: ignore[return-value]
1058
+ return convert_candles_to_dataframe(result, output_format)
720
1059
 
721
- def tickerPrice(self, options: strdict | None = None) -> list[strdict] | strdict:
1060
+ def tickerPrice(
1061
+ self,
1062
+ options: strdict | None = None,
1063
+ output_format: OutputFormat = OutputFormat.DICT,
1064
+ ) -> list[strdict] | strdict | Any:
722
1065
  """Get the current price for each market
723
1066
 
724
1067
  ---
@@ -735,6 +1078,23 @@ class Bitvavo:
735
1078
  ```python
736
1079
  options={}
737
1080
  options={"market": "BTC-EUR"}
1081
+
1082
+ # Output format selection:
1083
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
1084
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1085
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1086
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1087
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1088
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1089
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1090
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1091
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1092
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1093
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1094
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1095
+
1096
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
1097
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
738
1098
  ```
739
1099
 
740
1100
  ---
@@ -746,6 +1106,7 @@ class Bitvavo:
746
1106
  ---
747
1107
  Returns:
748
1108
  ```python
1109
+ # When output_format=OutputFormat.DICT (default):
749
1110
  # Note that `price` is unconverted
750
1111
  [
751
1112
  {"market": "1INCH-EUR", "price": "2.1594"},
@@ -761,12 +1122,20 @@ class Bitvavo:
761
1122
  {"market": "ALGO-EUR", "price": "1.3942"},
762
1123
  # and another 210 markets below this point
763
1124
  ]
1125
+
1126
+ # When output_format is any DataFrame format (pandas, polars, cudf, etc.):
1127
+ # Returns a DataFrame with columns: market, price
764
1128
  ```
765
1129
  """
766
1130
  postfix = createPostfix(options)
767
- return self.publicRequest(f"{self.base}/ticker/price{postfix}") # type: ignore[return-value]
1131
+ result = self.publicRequest(f"{self.base}/ticker/price{postfix}") # type: ignore[return-value]
1132
+ return convert_to_dataframe(result, output_format)
768
1133
 
769
- def tickerBook(self, options: strdict | None = None) -> list[strdict] | strdict:
1134
+ def tickerBook(
1135
+ self,
1136
+ options: strdict | None = None,
1137
+ output_format: OutputFormat = OutputFormat.DICT,
1138
+ ) -> list[strdict] | strdict | Any:
770
1139
  """Get current bid/ask, bidsize/asksize per market
771
1140
 
772
1141
  ---
@@ -783,6 +1152,23 @@ class Bitvavo:
783
1152
  ```python
784
1153
  options={}
785
1154
  options={"market": "BTC-EUR"}
1155
+
1156
+ # Output format selection:
1157
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
1158
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1159
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1160
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1161
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1162
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1163
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1164
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1165
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1166
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1167
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1168
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1169
+
1170
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
1171
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
786
1172
  ```
787
1173
 
788
1174
  ---
@@ -794,6 +1180,7 @@ class Bitvavo:
794
1180
  ---
795
1181
  Returns:
796
1182
  ```python
1183
+ # When output_format=OutputFormat.DICT (default):
797
1184
  [
798
1185
  {"market": "1INCH-EUR", "bid": "2.1534", "ask": "2.1587", "bidSize": "194.8", "askSize": "194.8"},
799
1186
  {"market": "AAVE-EUR", "bid": "213.7", "ask": "214.05", "bidSize": "212.532", "askSize": "4.77676965"},
@@ -802,12 +1189,20 @@ class Bitvavo:
802
1189
  {"market": "AION-EUR", "bid": "0.12531", "ask": "0.12578", "bidSize": "3345", "askSize": "10958.49228653"},
803
1190
  # and another 215 markets below this point
804
1191
  ]
1192
+
1193
+ # When output_format is any DataFrame format (pandas, polars, cudf, etc.):
1194
+ # Returns a DataFrame with columns: market, bid, ask, bidSize, askSize
805
1195
  ```
806
1196
  """
807
1197
  postfix = createPostfix(options)
808
- return self.publicRequest(f"{self.base}/ticker/book{postfix}") # type: ignore[return-value]
1198
+ result = self.publicRequest(f"{self.base}/ticker/book{postfix}") # type: ignore[return-value]
1199
+ return convert_to_dataframe(result, output_format)
809
1200
 
810
- def ticker24h(self, options: strdict | None = None) -> list[anydict] | anydict | errordict:
1201
+ def ticker24h(
1202
+ self,
1203
+ options: strdict | None = None,
1204
+ output_format: OutputFormat = OutputFormat.DICT,
1205
+ ) -> list[anydict] | anydict | errordict | Any:
811
1206
  """Get current bid/ask, bidsize/asksize per market
812
1207
 
813
1208
  ---
@@ -824,15 +1219,30 @@ class Bitvavo:
824
1219
  ```python
825
1220
  options={}
826
1221
  options={"market": "BTC-EUR"}
827
- ```
828
1222
 
1223
+ # Output format selection:
1224
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
1225
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1226
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1227
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1228
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1229
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1230
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1231
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1232
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1233
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1234
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1235
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1236
+
1237
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
1238
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
1239
+ ```
829
1240
  ---
830
1241
  Rate Limit Weight:
831
1242
  ```python
832
1243
  25 # if no market option is used
833
1244
  1 # if a market option is used
834
1245
  ```
835
-
836
1246
  ---
837
1247
  Returns:
838
1248
  ```python
@@ -874,9 +1284,15 @@ class Bitvavo:
874
1284
  if "market" in options:
875
1285
  rateLimitingWeight = 1
876
1286
  postfix = createPostfix(options)
877
- return self.publicRequest(f"{self.base}/ticker/24h{postfix}", rateLimitingWeight) # type: ignore[return-value]
1287
+ result = self.publicRequest(f"{self.base}/ticker/24h{postfix}", rateLimitingWeight) # type: ignore[return-value]
1288
+ return convert_to_dataframe(result, output_format)
878
1289
 
879
- def reportTrades(self, market: str, options: strintdict | None = None) -> list[anydict] | errordict:
1290
+ def reportTrades(
1291
+ self,
1292
+ market: str,
1293
+ options: strintdict | None = None,
1294
+ output_format: OutputFormat = OutputFormat.DICT,
1295
+ ) -> list[anydict] | errordict | Any:
880
1296
  """Get MiCA-compliant trades report for a specific market
881
1297
 
882
1298
  Returns trades from the specified market and time period made by all Bitvavo users.
@@ -893,14 +1309,27 @@ class Bitvavo:
893
1309
  ```python
894
1310
  market="BTC-EUR"
895
1311
  options={
896
- "limit": [ 1 .. 1000 ], default 500
897
- "start": int timestamp in ms >= 0
898
- "end": int timestamp in ms <= 8_640_000_000_000_000 # Cannot be more than 24 hours after start
899
- "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
900
- "tradeIdTo": "" # if you get a list and want everything BEFORE a certain id, put that id here
1312
+ "limit": [ 1 .. 1000 ], default 500
1313
+ "start": int timestamp in ms >= 0
1314
+ "end": int timestamp in ms <= 8_640_000_000_000_000 # Cannot be more than 24 hours after start
1315
+ "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
1316
+ "tradeIdTo": "" # if you get a list and want everything BEFORE a certain id, put that id here
901
1317
  }
902
- ```
903
1318
 
1319
+ # Output format selection:
1320
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
1321
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1322
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1323
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1324
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1325
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1326
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1327
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1328
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1329
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1330
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1331
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1332
+ ```
904
1333
  ---
905
1334
  Rate Limit Weight:
906
1335
  ```python
@@ -912,17 +1341,18 @@ class Bitvavo:
912
1341
  ```python
913
1342
  [
914
1343
  {
915
- "timestamp": 1542967486256,
916
- "id": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf",
917
- "amount": "0.1",
918
- "price": "5012",
919
- "side": "sell"
1344
+ "timestamp": 1542967486256,
1345
+ "id": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf",
1346
+ "amount": "0.1",
1347
+ "price": "5012",
1348
+ "side": "sell"
920
1349
  }
921
1350
  ]
922
1351
  ```
923
1352
  """
924
1353
  postfix = createPostfix(options)
925
- return self.publicRequest(f"{self.base}/report/{market}/trades{postfix}", 5) # type: ignore[return-value]
1354
+ result = self.publicRequest(f"{self.base}/report/{market}/trades{postfix}", 5) # type: ignore[return-value]
1355
+ return convert_to_dataframe(result, output_format)
926
1356
 
927
1357
  def reportBook(self, market: str, options: intdict | None = None) -> dict[str, str | int | list[str]] | errordict:
928
1358
  """Get MiCA-compliant order book report for a specific market
@@ -1471,7 +1901,12 @@ class Bitvavo:
1471
1901
  postfix = createPostfix(options)
1472
1902
  return self.privateRequest("/ordersOpen", postfix, {}, "GET", rateLimitingWeight) # type: ignore[return-value]
1473
1903
 
1474
- def trades(self, market: str, options: anydict | None = None) -> list[anydict] | errordict:
1904
+ def trades(
1905
+ self,
1906
+ market: str,
1907
+ options: anydict | None = None,
1908
+ output_format: OutputFormat = OutputFormat.DICT,
1909
+ ) -> list[anydict] | errordict | Any:
1475
1910
  """Get all historic trades from this account
1476
1911
 
1477
1912
  ---
@@ -1479,36 +1914,48 @@ class Bitvavo:
1479
1914
  ```python
1480
1915
  market="BTC-EUR"
1481
1916
  options={
1482
- "limit": [ 1 .. 1000 ], default 500
1483
- "start": int timestamp in ms >= 0
1484
- "end": int timestamp in ms <= 8_640_000_000_000_000 # (that's somewhere in the year 2243, or near the number 2^52)
1485
- "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
1486
- "tradeIdTo": "" # if you get a list and want everything BEFORE a certain id, put that id here
1917
+ "limit": [ 1 .. 1000 ], default 500
1918
+ "start": int timestamp in ms >= 0
1919
+ "end": int timestamp in ms <= 8_640_000_000_000_000 # (that's somewhere in the year 2243, or near the number 2^52)
1920
+ "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
1921
+ "tradeIdTo": "" # if you get a list and want everything BEFORE a certain id, put that id here
1487
1922
  }
1488
- ```
1489
1923
 
1924
+ # Output format selection:
1925
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
1926
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
1927
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
1928
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
1929
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
1930
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
1931
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
1932
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
1933
+ output_format=OutputFormat.IBIS # Returns Ibis expression
1934
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
1935
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
1936
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
1937
+ ```
1490
1938
  ---
1491
1939
  Rate Limit Weight:
1492
1940
  ```python
1493
1941
  5
1494
1942
  ```
1495
-
1496
1943
  ---
1497
1944
  Returns:
1498
1945
  ```python
1499
1946
  [
1500
1947
  {
1501
- "id": "108c3633-0276-4480-a902-17a01829deae",
1502
- "orderId": "1d671998-3d44-4df4-965f-0d48bd129a1b",
1503
- "timestamp": 1542967486256,
1504
- "market": "BTC-EUR",
1505
- "side": "buy",
1506
- "amount": "0.005",
1507
- "price": "5000.1",
1508
- "taker": true,
1509
- "fee": "0.03",
1510
- "feeCurrency": "EUR",
1511
- "settled": true
1948
+ "id": "108c3633-0276-4480-a902-17a01829deae",
1949
+ "orderId": "1d671998-3d44-4df4-965f-0d48bd129a1b",
1950
+ "timestamp": 1542967486256,
1951
+ "market": "BTC-EUR",
1952
+ "side": "buy",
1953
+ "amount": "0.005",
1954
+ "price": "5000.1",
1955
+ "taker": true,
1956
+ "fee": "0.03",
1957
+ "feeCurrency": "EUR",
1958
+ "settled": true
1512
1959
  }
1513
1960
  ]
1514
1961
  ```
@@ -1516,7 +1963,8 @@ class Bitvavo:
1516
1963
  options = _default(options, {})
1517
1964
  options["market"] = market
1518
1965
  postfix = createPostfix(options)
1519
- return self.privateRequest("/trades", postfix, {}, "GET", 5) # type: ignore[return-value]
1966
+ result = self.privateRequest("/trades", postfix, {}, "GET", 5) # type: ignore[return-value]
1967
+ return convert_to_dataframe(result, output_format)
1520
1968
 
1521
1969
  def account(self) -> dict[str, strdict]:
1522
1970
  """Get all fees for this account
@@ -1579,7 +2027,11 @@ class Bitvavo:
1579
2027
  postfix = createPostfix(options)
1580
2028
  return self.privateRequest("/account/fees", postfix, {}, "GET") # type: ignore[return-value]
1581
2029
 
1582
- def balance(self, options: strdict | None = None) -> list[strdict] | errordict:
2030
+ def balance(
2031
+ self,
2032
+ options: strdict | None = None,
2033
+ output_format: OutputFormat = OutputFormat.DICT,
2034
+ ) -> list[strdict] | errordict | Any:
1583
2035
  """Get the balance for this account
1584
2036
 
1585
2037
  ---
@@ -1587,6 +2039,23 @@ class Bitvavo:
1587
2039
  ```python
1588
2040
  options={} # return all balances
1589
2041
  options={symbol="BTC"} # return a single balance
2042
+
2043
+ # Output format selection:
2044
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
2045
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
2046
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
2047
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
2048
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
2049
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
2050
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
2051
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
2052
+ output_format=OutputFormat.IBIS # Returns Ibis expression
2053
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
2054
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
2055
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
2056
+
2057
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
2058
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
1590
2059
  ```
1591
2060
 
1592
2061
  ---
@@ -1598,6 +2067,7 @@ class Bitvavo:
1598
2067
  ---
1599
2068
  Returns:
1600
2069
  ```python
2070
+ # When output_format='dict' (default):
1601
2071
  [
1602
2072
  {
1603
2073
  "symbol": "BTC",
@@ -1605,10 +2075,15 @@ class Bitvavo:
1605
2075
  "inOrder": "0.74832374"
1606
2076
  }
1607
2077
  ]
2078
+
2079
+ # When output_format is any DataFrame format:
2080
+ # Returns the above data as a DataFrame in the requested format (pandas, polars, etc.)
2081
+ # with columns: symbol, available, inOrder
1608
2082
  ```
1609
2083
  """
1610
2084
  postfix = createPostfix(options)
1611
- return self.privateRequest("/balance", postfix, {}, "GET", 5) # type: ignore[return-value]
2085
+ result = self.privateRequest("/balance", postfix, {}, "GET", 5) # type: ignore[return-value]
2086
+ return convert_to_dataframe(result, output_format)
1612
2087
 
1613
2088
  def accountHistory(self, options: strintdict | None = None) -> anydict | errordict:
1614
2089
  """Get all past transactions for your account
@@ -1780,7 +2255,11 @@ class Bitvavo:
1780
2255
  body["address"] = address
1781
2256
  return self.privateRequest("/withdrawal", "", body, "POST") # type: ignore[return-value]
1782
2257
 
1783
- def withdrawalHistory(self, options: anydict | None = None) -> list[anydict] | errordict:
2258
+ def withdrawalHistory(
2259
+ self,
2260
+ options: anydict | None = None,
2261
+ output_format: OutputFormat = OutputFormat.DICT,
2262
+ ) -> list[anydict] | errordict | Any:
1784
2263
  """Get the withdrawal history
1785
2264
 
1786
2265
  ---
@@ -1792,8 +2271,24 @@ class Bitvavo:
1792
2271
  "start": int timestamp in ms >= 0
1793
2272
  "end": int timestamp in ms <= 8_640_000_000_000_000 # (that's somewhere in the year 2243, or near the number 2^52)
1794
2273
  }
1795
- ```
1796
2274
 
2275
+ # Output format selection:
2276
+ output_format=OutputFormat.DICT # Default: returns standard Python dict/list
2277
+ output_format=OutputFormat.PANDAS # Returns pandas DataFrame
2278
+ output_format=OutputFormat.POLARS # Returns polars DataFrame
2279
+ output_format=OutputFormat.CUDF # Returns NVIDIA cuDF (GPU-accelerated)
2280
+ output_format=OutputFormat.MODIN # Returns modin (distributed pandas)
2281
+ output_format=OutputFormat.PYARROW # Returns Apache Arrow Table
2282
+ output_format=OutputFormat.DASK # Returns Dask DataFrame (distributed)
2283
+ output_format=OutputFormat.DUCKDB # Returns DuckDB relation
2284
+ output_format=OutputFormat.IBIS # Returns Ibis expression
2285
+ output_format=OutputFormat.PYSPARK # Returns PySpark DataFrame
2286
+ output_format=OutputFormat.PYSPARK_CONNECT # Returns PySpark Connect DataFrame
2287
+ output_format=OutputFormat.SQLFRAME # Returns SQLFrame DataFrame
2288
+
2289
+ # Note: DataFrame formats require narwhals and the respective library to be installed.
2290
+ # Install with: pip install 'bitvavo-api-upgraded[pandas]' or similar for other formats.
2291
+ ```
1797
2292
  ---
1798
2293
  Rate Limit Weight:
1799
2294
  ```python
@@ -1814,11 +2309,124 @@ class Bitvavo:
1814
2309
  "fee": "0.00006",
1815
2310
  "status": "awaiting_processing"
1816
2311
  }
1817
- }
2312
+ ]
1818
2313
  ```
1819
2314
  """ # noqa: E501
1820
2315
  postfix = createPostfix(options)
1821
- return self.privateRequest("/withdrawalHistory", postfix, {}, "GET", 5) # type: ignore[return-value]
2316
+ result = self.privateRequest("/withdrawalHistory", postfix, {}, "GET", 5) # type: ignore[return-value]
2317
+ return convert_to_dataframe(result, output_format)
2318
+
2319
+ # API Key Management Helper Methods
2320
+
2321
+ def add_api_key(self, api_key: str, api_secret: str) -> None:
2322
+ """Add a new API key to the available keys.
2323
+
2324
+ Args:
2325
+ api_key: The API key to add
2326
+ api_secret: The corresponding API secret
2327
+ """
2328
+ new_key = {"key": api_key, "secret": api_secret}
2329
+ self.api_keys.append(new_key)
2330
+
2331
+ # Initialize rate limit tracking for this key using settings default
2332
+ key_index = len(self.api_keys) - 1
2333
+ default_rate_limit = bitvavo_upgraded_settings.DEFAULT_RATE_LIMIT
2334
+ self.rate_limits[key_index] = {"remaining": default_rate_limit, "resetAt": ms(0)}
2335
+
2336
+ logger.info("api-key-added", key_index=key_index)
2337
+
2338
+ def remove_api_key(self, api_key: str) -> bool:
2339
+ """Remove an API key from the available keys.
2340
+
2341
+ Args:
2342
+ api_key: The API key to remove
2343
+
2344
+ Returns:
2345
+ bool: True if the key was found and removed, False otherwise
2346
+ """
2347
+ for i, key_data in enumerate(self.api_keys):
2348
+ if key_data["key"] == api_key:
2349
+ _ = self.api_keys.pop(i)
2350
+ # Remove rate limit tracking for this key
2351
+ if i in self.rate_limits:
2352
+ del self.rate_limits[i]
2353
+ # Update rate limit tracking indices (shift them down)
2354
+ new_rate_limits = {}
2355
+ for key_idx, limits in self.rate_limits.items():
2356
+ if key_idx == -1 or key_idx < i: # keyless
2357
+ new_rate_limits[key_idx] = limits
2358
+ elif key_idx > i:
2359
+ new_rate_limits[key_idx - 1] = limits
2360
+ self.rate_limits = new_rate_limits
2361
+
2362
+ # Update current index if needed
2363
+ if self.current_api_key_index >= i:
2364
+ self.current_api_key_index = max(0, self.current_api_key_index - 1)
2365
+
2366
+ logger.info("api-key-removed", key_index=i)
2367
+ return True
2368
+ return False
2369
+
2370
+ def get_api_key_status(self) -> dict[str, dict[str, int | str | bool]]:
2371
+ """Get the current status of all API keys including rate limits.
2372
+
2373
+ Returns:
2374
+ dict: Status information for keyless and all API keys
2375
+ """
2376
+ status = {}
2377
+
2378
+ # Keyless status
2379
+ keyless_limits = self.rate_limits.get(-1, {"remaining": 0, "resetAt": ms(0)})
2380
+ status["keyless"] = {
2381
+ "remaining": int(keyless_limits["remaining"]),
2382
+ "resetAt": int(keyless_limits["resetAt"]),
2383
+ "available": self._has_rate_limit_available(-1, 1),
2384
+ }
2385
+
2386
+ # API key status
2387
+ for i, key_data in enumerate(self.api_keys):
2388
+ key_limits = self.rate_limits.get(i, {"remaining": 0, "resetAt": ms(0)})
2389
+ KEY_LENGTH = 12
2390
+ key_masked = (
2391
+ key_data["key"][:8] + "..." + key_data["key"][-4:]
2392
+ if len(key_data["key"]) > KEY_LENGTH
2393
+ else key_data["key"]
2394
+ )
2395
+ status[f"api_key_{i}"] = {
2396
+ "key": key_masked,
2397
+ "remaining": int(key_limits["remaining"]),
2398
+ "resetAt": int(key_limits["resetAt"]),
2399
+ "available": self._has_rate_limit_available(i, 1),
2400
+ }
2401
+
2402
+ return status
2403
+
2404
+ def set_keyless_preference(self, prefer_keyless: bool) -> None: # noqa: FBT001 (Boolean-typed positional argument in function definition)
2405
+ """Set whether to prefer keyless requests.
2406
+
2407
+ Args:
2408
+ prefer_keyless: If True, use keyless requests first when available
2409
+ """
2410
+ self.prefer_keyless = prefer_keyless
2411
+ logger.info("keyless-preference-changed", prefer_keyless=prefer_keyless)
2412
+
2413
+ def get_current_config(self) -> dict[str, str | bool | int]:
2414
+ """Get the current configuration.
2415
+
2416
+ Returns:
2417
+ dict: Current configuration including key count and preferences
2418
+ """
2419
+ KEY_LENGTH = 12
2420
+ return {
2421
+ "api_key_count": len(self.api_keys),
2422
+ "prefer_keyless": self.prefer_keyless,
2423
+ "current_api_key_index": self.current_api_key_index,
2424
+ "current_api_key": self._current_api_key[:8] + "..." + self._current_api_key[-4:]
2425
+ if len(self._current_api_key) > KEY_LENGTH
2426
+ else self._current_api_key,
2427
+ "rate_limit_remaining": self.rateLimitRemaining,
2428
+ "rate_limit_reset_at": int(self.rateLimitResetAt),
2429
+ }
1822
2430
 
1823
2431
  def newWebsocket(self) -> Bitvavo.WebSocketAppFacade:
1824
2432
  return Bitvavo.WebSocketAppFacade(self.APIKEY, self.APISECRET, self.ACCESSWINDOW, self.wsUrl, self)
@@ -1875,7 +2483,7 @@ class Bitvavo:
1875
2483
  self.receiveThread.join()
1876
2484
 
1877
2485
  def waitForSocket(self, ws: WebSocketApp, message: str, private: bool) -> None: # noqa: ARG002, FBT001
1878
- while True:
2486
+ while self.keepAlive:
1879
2487
  if (not private and self.open) or (private and self.authenticated and self.open):
1880
2488
  return
1881
2489
  time.sleep(0.1)
@@ -1989,6 +2597,8 @@ class Bitvavo:
1989
2597
  callbacks["subscriptionTrades"][market](msg_dict)
1990
2598
 
1991
2599
  def on_error(self, ws: Any, error: Any) -> None: # noqa: ARG002
2600
+ # Stop the receive thread on error to prevent hanging
2601
+ self.receiveThread.stop()
1992
2602
  if "error" in self.callbacks:
1993
2603
  self.callbacks["error"](error)
1994
2604
  else: