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.
- bitvavo_api_upgraded/__init__.py +2 -0
- bitvavo_api_upgraded/bitvavo.py +704 -94
- bitvavo_api_upgraded/dataframe_utils.py +175 -0
- bitvavo_api_upgraded/settings.py +81 -16
- bitvavo_api_upgraded/type_aliases.py +34 -0
- {bitvavo_api_upgraded-2.0.0.dist-info → bitvavo_api_upgraded-2.2.0.dist-info}/METADATA +362 -101
- bitvavo_api_upgraded-2.2.0.dist-info/RECORD +10 -0
- {bitvavo_api_upgraded-2.0.0.dist-info → bitvavo_api_upgraded-2.2.0.dist-info}/WHEEL +1 -1
- bitvavo_api_upgraded-2.0.0.dist-info/RECORD +0 -9
bitvavo_api_upgraded/bitvavo.py
CHANGED
@@ -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
|
-
|
238
|
-
|
239
|
-
self.
|
240
|
-
self.
|
241
|
-
self.
|
242
|
-
|
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
|
-
|
245
|
-
|
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
|
-
|
334
|
-
|
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(
|
536
|
+
"with_api_key": bool(api_key != ""),
|
341
537
|
"public_or_private": "public",
|
538
|
+
"key_index": key_index,
|
342
539
|
},
|
343
540
|
)
|
344
|
-
|
541
|
+
|
542
|
+
if api_key:
|
345
543
|
now = time_ms() + bitvavo_upgraded_settings.LAG
|
346
|
-
sig = createSignature(now, "GET", url.replace(self.base, ""), None,
|
544
|
+
sig = createSignature(now, "GET", url.replace(self.base, ""), None, api_secret)
|
347
545
|
headers = {
|
348
|
-
"bitvavo-access-key":
|
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.
|
557
|
+
self._update_rate_limit_for_key(key_index, r.json())
|
358
558
|
else:
|
359
|
-
self.
|
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 |
|
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
|
-
|
393
|
-
|
394
|
-
|
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,
|
619
|
+
sig = createSignature(now, method, (endpoint + postfix), body, api_secret)
|
397
620
|
url = self.base + endpoint + postfix
|
398
621
|
headers = {
|
399
|
-
"bitvavo-access-key":
|
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(
|
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.
|
649
|
+
self._update_rate_limit_for_key(key_index, r.json())
|
424
650
|
else:
|
425
|
-
self.
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
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
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
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
|
-
|
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(
|
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
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
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
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
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:
|