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.
- bitvavo_api_upgraded/__init__.py +2 -0
- bitvavo_api_upgraded/bitvavo.py +931 -92
- 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.2.0.dist-info/METADATA +806 -0
- bitvavo_api_upgraded-2.2.0.dist-info/RECORD +10 -0
- bitvavo_api_upgraded-2.2.0.dist-info/WHEEL +4 -0
- bitvavo_api_upgraded-1.17.2.dist-info/METADATA +0 -320
- bitvavo_api_upgraded-1.17.2.dist-info/RECORD +0 -10
- bitvavo_api_upgraded-1.17.2.dist-info/WHEEL +0 -4
- bitvavo_api_upgraded-1.17.2.dist-info/licenses/LICENSE.txt +0 -15
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,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
|
-
|
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(
|
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
|
-
|
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,117 @@ 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)
|
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
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
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
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
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
|
-
|
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(
|
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("/
|
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(
|
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
|
-
|
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
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
"
|
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:
|