bitvavo-api-upgraded 1.17.2__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,12 +958,16 @@ 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
- def candles( # noqa: PLR0913
970
+ def candles(
657
971
  self,
658
972
  market: str,
659
973
  interval: str,
@@ -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,117 @@ 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)
1289
+
1290
+ def reportTrades(
1291
+ self,
1292
+ market: str,
1293
+ options: strintdict | None = None,
1294
+ output_format: OutputFormat = OutputFormat.DICT,
1295
+ ) -> list[anydict] | errordict | Any:
1296
+ """Get MiCA-compliant trades report for a specific market
1297
+
1298
+ Returns trades from the specified market and time period made by all Bitvavo users.
1299
+ The returned trades are sorted by timestamp in descending order (latest to earliest).
1300
+ Includes data compliant with the European Markets in Crypto-Assets (MiCA) regulation.
1301
+
1302
+ ---
1303
+ Examples:
1304
+ * https://api.bitvavo.com/v2/report/BTC-EUR/trades
1305
+ * https://api.bitvavo.com/v2/report/BTC-EUR/trades?limit=100&start=1640995200000
1306
+
1307
+ ---
1308
+ Args:
1309
+ ```python
1310
+ market="BTC-EUR"
1311
+ options={
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
1317
+ }
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
+ ```
1333
+ ---
1334
+ Rate Limit Weight:
1335
+ ```python
1336
+ 5
1337
+ ```
1338
+
1339
+ ---
1340
+ Returns:
1341
+ ```python
1342
+ [
1343
+ {
1344
+ "timestamp": 1542967486256,
1345
+ "id": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf",
1346
+ "amount": "0.1",
1347
+ "price": "5012",
1348
+ "side": "sell"
1349
+ }
1350
+ ]
1351
+ ```
1352
+ """
1353
+ postfix = createPostfix(options)
1354
+ result = self.publicRequest(f"{self.base}/report/{market}/trades{postfix}", 5) # type: ignore[return-value]
1355
+ return convert_to_dataframe(result, output_format)
878
1356
 
879
- def placeOrder(self, market: str, side: str, orderType: str, body: anydict) -> anydict:
1357
+ def reportBook(self, market: str, options: intdict | None = None) -> dict[str, str | int | list[str]] | errordict:
1358
+ """Get MiCA-compliant order book report for a specific market
1359
+
1360
+ Returns the list of all bids and asks for the specified market, sorted by price.
1361
+ Includes data compliant with the European Markets in Crypto-Assets (MiCA) regulation.
1362
+
1363
+ ---
1364
+ Examples:
1365
+ * https://api.bitvavo.com/v2/report/BTC-EUR/book
1366
+ * https://api.bitvavo.com/v2/report/BTC-EUR/book?depth=100
1367
+
1368
+ ---
1369
+ Args:
1370
+ ```python
1371
+ market="BTC-EUR"
1372
+ options={"depth": 100} # returns the best 100 asks and 100 bids, default 1000
1373
+ options={} # returns up to 1000 bids and asks for that book
1374
+ ```
1375
+
1376
+ ---
1377
+ Rate Limit Weight:
1378
+ ```python
1379
+ 1
1380
+ ```
1381
+
1382
+ ---
1383
+ Returns:
1384
+ ```python
1385
+ {
1386
+ "market": "BTC-EUR",
1387
+ "nonce": 10378032,
1388
+ "bids": [["41648", "0.12"], ["41647", "0.25"], ["41646", "0.33"]],
1389
+ "asks": [["41649", "0.15"], ["41650", "0.28"], ["41651", "0.22"]],
1390
+ "timestamp": 1700000000000,
1391
+ }
1392
+ ```
1393
+ """
1394
+ postfix = createPostfix(options)
1395
+ return self.publicRequest(f"{self.base}/report/{market}/book{postfix}") # type: ignore[return-value]
1396
+
1397
+ def placeOrder(self, market: str, side: str, orderType: str, operatorId: int, body: anydict) -> anydict:
880
1398
  """Place a new order on the exchange
881
1399
 
882
1400
  ---
@@ -886,9 +1404,11 @@ class Bitvavo:
886
1404
  side="buy" # Choose: buy, sell
887
1405
  # For market orders either `amount` or `amountQuote` is required
888
1406
  orderType="market" # Choose: market, limit, stopLoss, stopLossLimit, takeProfit, takeProfitLimit
1407
+ operatorId=123 # Your identifier for the trader or bot that made the request
889
1408
  body={
890
1409
  "amount": "1.567",
891
1410
  "amountQuote": "5000",
1411
+ "clientOrderId": "2be7d0df-d8dc-7b93-a550-8876f3b393e9", # Optional: your identifier for the order
892
1412
  # GTC orders will remain on the order book until they are filled or canceled.
893
1413
  # IOC orders will fill against existing orders, but will cancel any remaining amount after that.
894
1414
  # FOK orders will fill against existing orders in its entirety, or will be canceled (if the entire order cannot be filled).
@@ -904,6 +1424,7 @@ class Bitvavo:
904
1424
 
905
1425
  # For limit orders `amount` and `price` are required.
906
1426
  orderType="limit" # Choose: market, limit, stopLoss, stopLossLimit, takeProfit, takeProfitLimit
1427
+ operatorId=123
907
1428
  body={
908
1429
  "amount": "1.567",
909
1430
  "price": "6000",
@@ -916,6 +1437,7 @@ class Bitvavo:
916
1437
  orderType="stopLoss"
917
1438
  # or
918
1439
  orderType="takeProfit"
1440
+ operatorId=123
919
1441
  body={
920
1442
  "amount": "1.567",
921
1443
  "amountQuote": "5000",
@@ -931,6 +1453,7 @@ class Bitvavo:
931
1453
  orderType="stopLossLimit"
932
1454
  # or
933
1455
  orderType="takeProfitLimit"
1456
+ operatorId=123
934
1457
  body={
935
1458
  "amount": "1.567",
936
1459
  "price": "6000",
@@ -999,9 +1522,10 @@ class Bitvavo:
999
1522
  body["market"] = market
1000
1523
  body["side"] = side
1001
1524
  body["orderType"] = orderType
1525
+ body["operatorId"] = operatorId
1002
1526
  return self.privateRequest("/order", "", body, "POST") # type: ignore[return-value]
1003
1527
 
1004
- def updateOrder(self, market: str, orderId: str, body: anydict) -> anydict:
1528
+ def updateOrder(self, market: str, orderId: str, operatorId: int, body: anydict) -> anydict:
1005
1529
  """Update an existing order for a specific market. Make sure that at least one of the optional parameters is set, otherwise nothing will be updated.
1006
1530
 
1007
1531
  ---
@@ -1009,11 +1533,13 @@ class Bitvavo:
1009
1533
  ```python
1010
1534
  market="BTC-EUR"
1011
1535
  orderId="95d92d6c-ecf0-4960-a608-9953ef71652e"
1536
+ operatorId=123 # Your identifier for the trader or bot that made the request
1012
1537
  body={
1013
1538
  "amount": "1.567",
1014
1539
  "amountRemaining": "1.567",
1015
1540
  "price": "6000",
1016
1541
  "triggerAmount": "4000", # only for stop orders
1542
+ "clientOrderId": "2be7d0df-d8dc-7b93-a550-8876f3b393e9", # Optional: your identifier for the order
1017
1543
  # GTC orders will remain on the order book until they are filled or canceled.
1018
1544
  # IOC orders will fill against existing orders, but will cancel any remaining amount after that.
1019
1545
  # FOK orders will fill against existing orders in its entirety, or will be canceled (if the entire order cannot be filled).
@@ -1082,22 +1608,32 @@ class Bitvavo:
1082
1608
  """ # noqa: E501
1083
1609
  body["market"] = market
1084
1610
  body["orderId"] = orderId
1611
+ body["operatorId"] = operatorId
1085
1612
  return self.privateRequest("/order", "", body, "PUT") # type: ignore[return-value]
1086
1613
 
1087
- def cancelOrder(self, market: str, orderId: str) -> strdict:
1614
+ def cancelOrder(
1615
+ self,
1616
+ market: str,
1617
+ operatorId: int,
1618
+ orderId: str | None = None,
1619
+ clientOrderId: str | None = None,
1620
+ ) -> strdict:
1088
1621
  """Cancel an existing order for a specific market
1089
1622
 
1090
1623
  ---
1091
1624
  Args:
1092
1625
  ```python
1093
1626
  market="BTC-EUR"
1094
- orderId="a4a5d310-687c-486e-a3eb-1df832405ccd"
1627
+ operatorId=123 # Your identifier for the trader or bot that made the request
1628
+ orderId="a4a5d310-687c-486e-a3eb-1df832405ccd" # Either orderId or clientOrderId required
1629
+ clientOrderId="2be7d0df-d8dc-7b93-a550-8876f3b393e9" # Either orderId or clientOrderId required
1630
+ # If both orderId and clientOrderId are provided, clientOrderId takes precedence
1095
1631
  ```
1096
1632
 
1097
1633
  ---
1098
1634
  Rate Limit Weight:
1099
1635
  ```python
1100
- 1
1636
+ N/A
1101
1637
  ```
1102
1638
 
1103
1639
  ---
@@ -1106,7 +1642,22 @@ class Bitvavo:
1106
1642
  {"orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61"}
1107
1643
  ```
1108
1644
  """
1109
- postfix = createPostfix({"market": market, "orderId": orderId})
1645
+ if orderId is None and clientOrderId is None:
1646
+ msg = "Either orderId or clientOrderId must be provided"
1647
+ raise ValueError(msg)
1648
+
1649
+ params = {
1650
+ "market": market,
1651
+ "operatorId": operatorId,
1652
+ }
1653
+
1654
+ # clientOrderId takes precedence if both are provided
1655
+ if clientOrderId is not None:
1656
+ params["clientOrderId"] = clientOrderId
1657
+ elif orderId is not None:
1658
+ params["orderId"] = orderId
1659
+
1660
+ postfix = createPostfix(params)
1110
1661
  return self.privateRequest("/order", postfix, {}, "DELETE") # type: ignore[return-value]
1111
1662
 
1112
1663
  def getOrder(self, market: str, orderId: str) -> list[anydict] | errordict:
@@ -1350,7 +1901,12 @@ class Bitvavo:
1350
1901
  postfix = createPostfix(options)
1351
1902
  return self.privateRequest("/ordersOpen", postfix, {}, "GET", rateLimitingWeight) # type: ignore[return-value]
1352
1903
 
1353
- 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:
1354
1910
  """Get all historic trades from this account
1355
1911
 
1356
1912
  ---
@@ -1358,36 +1914,48 @@ class Bitvavo:
1358
1914
  ```python
1359
1915
  market="BTC-EUR"
1360
1916
  options={
1361
- "limit": [ 1 .. 1000 ], default 500
1362
- "start": int timestamp in ms >= 0
1363
- "end": int timestamp in ms <= 8_640_000_000_000_000 # (that's somewhere in the year 2243, or near the number 2^52)
1364
- "tradeIdFrom": "" # if you get a list and want everything AFTER a certain id, put that id here
1365
- "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
1366
1922
  }
1367
- ```
1368
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
+ ```
1369
1938
  ---
1370
1939
  Rate Limit Weight:
1371
1940
  ```python
1372
1941
  5
1373
1942
  ```
1374
-
1375
1943
  ---
1376
1944
  Returns:
1377
1945
  ```python
1378
1946
  [
1379
1947
  {
1380
- "id": "108c3633-0276-4480-a902-17a01829deae",
1381
- "orderId": "1d671998-3d44-4df4-965f-0d48bd129a1b",
1382
- "timestamp": 1542967486256,
1383
- "market": "BTC-EUR",
1384
- "side": "buy",
1385
- "amount": "0.005",
1386
- "price": "5000.1",
1387
- "taker": true,
1388
- "fee": "0.03",
1389
- "feeCurrency": "EUR",
1390
- "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
1391
1959
  }
1392
1960
  ]
1393
1961
  ```
@@ -1395,7 +1963,8 @@ class Bitvavo:
1395
1963
  options = _default(options, {})
1396
1964
  options["market"] = market
1397
1965
  postfix = createPostfix(options)
1398
- 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)
1399
1968
 
1400
1969
  def account(self) -> dict[str, strdict]:
1401
1970
  """Get all fees for this account
@@ -1421,6 +1990,35 @@ class Bitvavo:
1421
1990
  return self.privateRequest("/account", "", {}, "GET") # type: ignore[return-value]
1422
1991
 
1423
1992
  def fees(self, market: str | None = None, quote: str | None = None) -> list[strdict] | errordict:
1993
+ """Get market fees for a specific market or quote currency
1994
+
1995
+ ---
1996
+ Args:
1997
+ ```python
1998
+ market="BTC-EUR" # Optional: get fees for specific market
1999
+ quote="EUR" # Optional: get fees for all markets with EUR as quote currency
2000
+ # If both are provided, market takes precedence
2001
+ # If neither are provided, returns fees for all markets
2002
+ ```
2003
+
2004
+ ---
2005
+ Rate Limit Weight:
2006
+ ```python
2007
+ 1
2008
+ ```
2009
+
2010
+ ---
2011
+ Returns:
2012
+ ```python
2013
+ [
2014
+ {
2015
+ "market": "BTC-EUR",
2016
+ "maker": "0.0015",
2017
+ "taker": "0.0025"
2018
+ }
2019
+ ]
2020
+ ```
2021
+ """
1424
2022
  options = {}
1425
2023
  if market is not None:
1426
2024
  options["market"] = market
@@ -1429,7 +2027,11 @@ class Bitvavo:
1429
2027
  postfix = createPostfix(options)
1430
2028
  return self.privateRequest("/account/fees", postfix, {}, "GET") # type: ignore[return-value]
1431
2029
 
1432
- 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:
1433
2035
  """Get the balance for this account
1434
2036
 
1435
2037
  ---
@@ -1437,6 +2039,23 @@ class Bitvavo:
1437
2039
  ```python
1438
2040
  options={} # return all balances
1439
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.
1440
2059
  ```
1441
2060
 
1442
2061
  ---
@@ -1448,6 +2067,7 @@ class Bitvavo:
1448
2067
  ---
1449
2068
  Returns:
1450
2069
  ```python
2070
+ # When output_format='dict' (default):
1451
2071
  [
1452
2072
  {
1453
2073
  "symbol": "BTC",
@@ -1455,10 +2075,62 @@ class Bitvavo:
1455
2075
  "inOrder": "0.74832374"
1456
2076
  }
1457
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
2082
+ ```
2083
+ """
2084
+ postfix = createPostfix(options)
2085
+ result = self.privateRequest("/balance", postfix, {}, "GET", 5) # type: ignore[return-value]
2086
+ return convert_to_dataframe(result, output_format)
2087
+
2088
+ def accountHistory(self, options: strintdict | None = None) -> anydict | errordict:
2089
+ """Get all past transactions for your account
2090
+
2091
+ ---
2092
+ Args:
2093
+ ```python
2094
+ options={
2095
+ "fromDate": int timestamp in ms >= 0, # Starting timestamp to return transactions from
2096
+ "toDate": int timestamp in ms <= 8_640_000_000_000_000, # Timestamp up to which to return transactions
2097
+ "maxItems": [ 1 .. 100 ], default 100, # Maximum number of transactions per page
2098
+ "page": 1, # Page number to return (1-indexed)
2099
+ }
2100
+ ```
2101
+
2102
+ ---
2103
+ Rate Limit Weight:
2104
+ ```python
2105
+ 1
2106
+ ```
2107
+
2108
+ ---
2109
+ Returns:
2110
+ ```python
2111
+ {
2112
+ "items": [
2113
+ {
2114
+ "transactionId": "5f5e7b3b-4f5b-4b2d-8b2f-4f2b5b3f5e5f",
2115
+ "timestamp": 1542967486256,
2116
+ "type": "deposit",
2117
+ "symbol": "BTC",
2118
+ "amount": "0.99994",
2119
+ "description": "Deposit via bank transfer",
2120
+ "status": "completed",
2121
+ "feesCurrency": "EUR",
2122
+ "feesAmount": "0.01",
2123
+ "address": "BitcoinAddress"
2124
+ }
2125
+ ],
2126
+ "currentPage": 1,
2127
+ "totalPages": 1,
2128
+ "maxItems": 100
2129
+ }
1458
2130
  ```
1459
2131
  """
1460
2132
  postfix = createPostfix(options)
1461
- return self.privateRequest("/balance", postfix, {}, "GET", 5) # type: ignore[return-value]
2133
+ return self.privateRequest("/account/history", postfix, {}, "GET") # type: ignore[return-value]
1462
2134
 
1463
2135
  def depositAssets(self, symbol: str) -> strdict:
1464
2136
  """Get the deposit address (with paymentId for some assets) or bank account information to increase your balance
@@ -1583,7 +2255,11 @@ class Bitvavo:
1583
2255
  body["address"] = address
1584
2256
  return self.privateRequest("/withdrawal", "", body, "POST") # type: ignore[return-value]
1585
2257
 
1586
- 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:
1587
2263
  """Get the withdrawal history
1588
2264
 
1589
2265
  ---
@@ -1595,8 +2271,24 @@ class Bitvavo:
1595
2271
  "start": int timestamp in ms >= 0
1596
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)
1597
2273
  }
1598
- ```
1599
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
+ ```
1600
2292
  ---
1601
2293
  Rate Limit Weight:
1602
2294
  ```python
@@ -1617,11 +2309,124 @@ class Bitvavo:
1617
2309
  "fee": "0.00006",
1618
2310
  "status": "awaiting_processing"
1619
2311
  }
1620
- }
2312
+ ]
1621
2313
  ```
1622
2314
  """ # noqa: E501
1623
2315
  postfix = createPostfix(options)
1624
- 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
+ }
1625
2430
 
1626
2431
  def newWebsocket(self) -> Bitvavo.WebSocketAppFacade:
1627
2432
  return Bitvavo.WebSocketAppFacade(self.APIKEY, self.APISECRET, self.ACCESSWINDOW, self.wsUrl, self)
@@ -1678,7 +2483,7 @@ class Bitvavo:
1678
2483
  self.receiveThread.join()
1679
2484
 
1680
2485
  def waitForSocket(self, ws: WebSocketApp, message: str, private: bool) -> None: # noqa: ARG002, FBT001
1681
- while True:
2486
+ while self.keepAlive:
1682
2487
  if (not private and self.open) or (private and self.authenticated and self.open):
1683
2488
  return
1684
2489
  time.sleep(0.1)
@@ -1792,6 +2597,8 @@ class Bitvavo:
1792
2597
  callbacks["subscriptionTrades"][market](msg_dict)
1793
2598
 
1794
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()
1795
2602
  if "error" in self.callbacks:
1796
2603
  self.callbacks["error"](error)
1797
2604
  else:
@@ -2311,6 +3118,7 @@ class Bitvavo:
2311
3118
  market: str,
2312
3119
  side: str,
2313
3120
  orderType: str,
3121
+ operatorId: int,
2314
3122
  body: anydict,
2315
3123
  callback: Callable[[Any], None],
2316
3124
  ) -> None:
@@ -2323,9 +3131,11 @@ class Bitvavo:
2323
3131
  side="buy" # Choose: buy, sell
2324
3132
  # For market orders either `amount` or `amountQuote` is required
2325
3133
  orderType="market" # Choose: market, limit, stopLoss, stopLossLimit, takeProfit, takeProfitLimit
3134
+ operatorId=123 # Your identifier for the trader or bot that made the request
2326
3135
  body={
2327
3136
  "amount": "1.567",
2328
3137
  "amountQuote": "5000",
3138
+ "clientOrderId": "2be7d0df-d8dc-7b93-a550-8876f3b393e9", # Optional: your identifier for the order
2329
3139
  # GTC orders will remain on the order book until they are filled or canceled.
2330
3140
  # IOC orders will fill against existing orders, but will cancel any remaining amount after that.
2331
3141
  # FOK orders will fill against existing orders in its entirety, or will be canceled (if the entire order cannot be filled).
@@ -2341,6 +3151,7 @@ class Bitvavo:
2341
3151
 
2342
3152
  # For limit orders `amount` and `price` are required.
2343
3153
  orderType="limit" # Choose: market, limit, stopLoss, stopLossLimit, takeProfit, takeProfitLimit
3154
+ operatorId=123
2344
3155
  body={
2345
3156
  "amount": "1.567",
2346
3157
  "price": "6000",
@@ -2353,6 +3164,7 @@ class Bitvavo:
2353
3164
  orderType="stopLoss"
2354
3165
  # or
2355
3166
  orderType="takeProfit"
3167
+ operatorId=123
2356
3168
  body={
2357
3169
  "amount": "1.567",
2358
3170
  "amountQuote": "5000",
@@ -2368,6 +3180,7 @@ class Bitvavo:
2368
3180
  orderType="stopLossLimit"
2369
3181
  # or
2370
3182
  orderType="takeProfitLimit"
3183
+ operatorId=123
2371
3184
  body={
2372
3185
  "amount": "1.567",
2373
3186
  "price": "6000",
@@ -2438,6 +3251,7 @@ class Bitvavo:
2438
3251
  body["market"] = market
2439
3252
  body["side"] = side
2440
3253
  body["orderType"] = orderType
3254
+ body["operatorId"] = operatorId
2441
3255
  body["action"] = "privateCreateOrder"
2442
3256
  self.doSend(self.ws, json.dumps(body), True)
2443
3257
 
@@ -2445,6 +3259,7 @@ class Bitvavo:
2445
3259
  self,
2446
3260
  market: str,
2447
3261
  orderId: str,
3262
+ operatorId: int,
2448
3263
  body: anydict,
2449
3264
  callback: Callable[[Any], None],
2450
3265
  ) -> None:
@@ -2457,11 +3272,13 @@ class Bitvavo:
2457
3272
  ```python
2458
3273
  market="BTC-EUR"
2459
3274
  orderId="95d92d6c-ecf0-4960-a608-9953ef71652e"
3275
+ operatorId=123 # Your identifier for the trader or bot that made the request
2460
3276
  body={
2461
3277
  "amount": "1.567",
2462
3278
  "amountRemaining": "1.567",
2463
3279
  "price": "6000",
2464
3280
  "triggerAmount": "4000", # only for stop orders
3281
+ "clientOrderId": "2be7d0df-d8dc-7b93-a550-8876f3b393e9", # Optional: your identifier for the order
2465
3282
  # GTC orders will remain on the order book until they are filled or canceled.
2466
3283
  # IOC orders will fill against existing orders, but will cancel any remaining amount after that.
2467
3284
  # FOK orders will fill against existing orders in its entirety, or will be canceled (if the entire order cannot be filled).
@@ -2532,24 +3349,35 @@ class Bitvavo:
2532
3349
  self.callbacks["updateOrder"] = callback
2533
3350
  body["market"] = market
2534
3351
  body["orderId"] = orderId
3352
+ body["operatorId"] = operatorId
2535
3353
  body["action"] = "privateUpdateOrder"
2536
3354
  self.doSend(self.ws, json.dumps(body), True)
2537
3355
 
2538
- def cancelOrder(self, market: str, orderId: str, callback: Callable[[Any], None]) -> None:
3356
+ def cancelOrder(
3357
+ self,
3358
+ market: str,
3359
+ operatorId: int,
3360
+ callback: Callable[[Any], None],
3361
+ orderId: str | None = None,
3362
+ clientOrderId: str | None = None,
3363
+ ) -> None:
2539
3364
  """Cancel an existing order for a specific market
2540
3365
 
2541
3366
  ---
2542
3367
  Args:
2543
3368
  ```python
2544
3369
  market="BTC-EUR"
2545
- orderId="a4a5d310-687c-486e-a3eb-1df832405ccd"
3370
+ operatorId=123 # Your identifier for the trader or bot that made the request
2546
3371
  callback=callback_example
3372
+ orderId="a4a5d310-687c-486e-a3eb-1df832405ccd" # Either orderId or clientOrderId required
3373
+ clientOrderId="2be7d0df-d8dc-7b93-a550-8876f3b393e9" # Either orderId or clientOrderId required
3374
+ # If both orderId and clientOrderId are provided, clientOrderId takes precedence
2547
3375
  ```
2548
3376
 
2549
3377
  ---
2550
3378
  Rate Limit Weight:
2551
3379
  ```python
2552
- 1
3380
+ N/A
2553
3381
  ```
2554
3382
 
2555
3383
  ---
@@ -2558,12 +3386,23 @@ class Bitvavo:
2558
3386
  {"orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61"}
2559
3387
  ```
2560
3388
  """
3389
+ if orderId is None and clientOrderId is None:
3390
+ msg = "Either orderId or clientOrderId must be provided"
3391
+ raise ValueError(msg)
3392
+
2561
3393
  self.callbacks["cancelOrder"] = callback
2562
3394
  options = {
2563
3395
  "action": "privateCancelOrder",
2564
3396
  "market": market,
2565
- "orderId": orderId,
3397
+ "operatorId": operatorId,
2566
3398
  }
3399
+
3400
+ # clientOrderId takes precedence if both are provided
3401
+ if clientOrderId is not None:
3402
+ options["clientOrderId"] = clientOrderId
3403
+ elif orderId is not None:
3404
+ options["orderId"] = orderId
3405
+
2567
3406
  self.doSend(self.ws, json.dumps(options), True)
2568
3407
 
2569
3408
  def getOrder(self, market: str, orderId: str, callback: Callable[[Any], None]) -> None: