bitvavo-api-upgraded 3.0.0__py3-none-any.whl → 4.1.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.
@@ -2,16 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import datetime as dt
5
- import hashlib
6
- import hmac
7
5
  import json
6
+ import statistics
8
7
  import time
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
  from pathlib import Path
10
10
  from threading import Thread
11
- from typing import Any, Callable
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  import websocket as ws_lib
14
- from requests import delete, get, post, put
14
+ from httpx import delete, get, post, put
15
15
  from structlog.stdlib import get_logger
16
16
  from websocket import WebSocketApp # missing stubs for WebSocketApp
17
17
 
@@ -19,88 +19,24 @@ from bitvavo_api_upgraded.dataframe_utils import convert_candles_to_dataframe, c
19
19
  from bitvavo_api_upgraded.helper_funcs import configure_loggers, time_ms, time_to_wait
20
20
  from bitvavo_api_upgraded.settings import bitvavo_settings, bitvavo_upgraded_settings
21
21
  from bitvavo_api_upgraded.type_aliases import OutputFormat, anydict, errordict, intdict, ms, s_f, strdict, strintdict
22
+ from bitvavo_client.auth.signing import create_signature
23
+ from bitvavo_client.endpoints.common import (
24
+ asks_compare,
25
+ bids_compare,
26
+ create_postfix,
27
+ default,
28
+ epoch_millis,
29
+ sort_and_insert,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from collections.abc import Callable
22
34
 
23
35
  configure_loggers()
24
36
 
25
37
  logger = get_logger(__name__)
26
38
 
27
39
 
28
- def create_signature(timestamp: ms, method: str, url: str, body: anydict | None, api_secret: str) -> str:
29
- string = f"{timestamp}{method}/v2{url}"
30
- if body is not None and len(body.keys()) > 0:
31
- string += json.dumps(body, separators=(",", ":"))
32
- signature = hmac.new(api_secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha256).hexdigest()
33
- return signature
34
-
35
-
36
- def create_postfix(options: anydict | None) -> str:
37
- """Generate a URL postfix, based on the `options` dict.
38
-
39
- ---
40
- Args:
41
- options (anydict): [description]
42
-
43
- ---
44
- Returns:
45
- str: [description]
46
- """
47
- options = _default(options, {})
48
- params = [f"{key}={options[key]}" for key in options]
49
- postfix = "&".join(params) # intersperse
50
- return f"?{postfix}" if len(options) > 0 else postfix
51
-
52
-
53
- def _default(value: anydict | None, fallback: anydict) -> anydict:
54
- """
55
- Note that is close, but not actually equal to:
56
-
57
- `return value or fallback`
58
-
59
- I checked this with a temporary hypothesis test.
60
-
61
- This note is all you will get out of me.
62
- """
63
- return value if value is not None else fallback
64
-
65
-
66
- def _epoch_millis(dt: dt.datetime) -> int:
67
- return int(dt.timestamp() * 1000)
68
-
69
-
70
- def asks_compare(a: float, b: float) -> bool:
71
- return a < b
72
-
73
-
74
- def bids_compare(a: float, b: float) -> bool:
75
- return a > b
76
-
77
-
78
- def sort_and_insert(
79
- asks_or_bids: list[list[str]],
80
- update: list[list[str]],
81
- compareFunc: Callable[[float, float], bool],
82
- ) -> list[list[str]] | errordict:
83
- for updateEntry in update:
84
- entrySet: bool = False
85
- for j in range(len(asks_or_bids)):
86
- bookItem = asks_or_bids[j]
87
- if compareFunc(float(updateEntry[0]), float(bookItem[0])):
88
- asks_or_bids.insert(j, updateEntry)
89
- entrySet = True
90
- break
91
- if float(updateEntry[0]) == float(bookItem[0]):
92
- if float(updateEntry[1]) > 0.0:
93
- asks_or_bids[j] = updateEntry
94
- entrySet = True
95
- break
96
- asks_or_bids.pop(j)
97
- entrySet = True
98
- break
99
- if not entrySet:
100
- asks_or_bids.append(updateEntry)
101
- return asks_or_bids
102
-
103
-
104
40
  def process_local_book(ws: Bitvavo.WebSocketAppFacade, message: anydict) -> None:
105
41
  market: str = ""
106
42
  if "action" in message:
@@ -412,7 +348,9 @@ class Bitvavo:
412
348
  key_name = f"API_KEY_{key_index}" if key_index >= 0 else "KEYLESS"
413
349
 
414
350
  logger.warning(
415
- "rate-limit-reached", key_name=key_name, rateLimitRemaining=self.rate_limits[key_index]["remaining"]
351
+ "rate-limit-reached",
352
+ key_name=key_name,
353
+ rateLimitRemaining=self.rate_limits[key_index]["remaining"],
416
354
  )
417
355
  logger.info(
418
356
  "napping-until-reset",
@@ -423,27 +361,108 @@ class Bitvavo:
423
361
  )
424
362
  time.sleep(napTime + 1) # +1 to add a tiny bit of buffer time
425
363
 
426
- def calc_lag(self) -> ms:
364
+ def calc_lag(self, samples: int = 5, timeout_seconds: float = 5.0) -> ms: # noqa: C901
427
365
  """
428
- Calculate the time difference between the client and server; use this value with BITVAVO_API_UPGRADED_LAG,
429
- when you make an api call, to precent 304 errors.
366
+ Calculate the time difference between the client and server using statistical analysis.
367
+
368
+ Uses multiple samples with outlier detection to get a more accurate lag measurement.
369
+
370
+ Args:
371
+ samples: Number of time samples to collect (default: 5)
372
+ timeout_seconds: Maximum time to spend collecting samples (default: 5.0)
373
+
374
+ Returns:
375
+ Average lag in milliseconds
430
376
 
431
- Raises KeyError if time() returns an error dict.
377
+ Raises:
378
+ ValueError: If unable to collect sufficient valid samples
379
+ RuntimeError: If all API calls fail
432
380
  """
433
- lag_list = [
434
- self.time()["time"] - time_ms(),
435
- self.time()["time"] - time_ms(),
436
- self.time()["time"] - time_ms(),
437
- self.time()["time"] - time_ms(),
438
- self.time()["time"] - time_ms(),
439
- self.time()["time"] - time_ms(),
440
- self.time()["time"] - time_ms(),
441
- self.time()["time"] - time_ms(),
442
- self.time()["time"] - time_ms(),
443
- self.time()["time"] - time_ms(),
444
- ]
381
+ ARBITRARY = 3
382
+ if samples < ARBITRARY:
383
+ msg = f"Need at least {ARBITRARY} samples for statistical analysis"
384
+ raise ValueError(msg)
385
+
386
+ def measure_single_lag() -> ms | None:
387
+ """Measure lag for a single request with error handling."""
388
+ try:
389
+ client_time_before = time_ms()
390
+ server_response = self.time()
391
+ client_time_after = time_ms()
392
+
393
+ if isinstance(server_response, dict) and "time" in server_response:
394
+ # Use midpoint of request duration for better accuracy
395
+ client_time_avg = (client_time_before + client_time_after) // 2
396
+ server_time = server_response["time"]
397
+ if isinstance(server_time, int):
398
+ return ms(server_time - client_time_avg)
399
+ return None
400
+ except (ValueError, TypeError, KeyError):
401
+ return None
402
+ else:
403
+ # If error or unexpected response
404
+ return None
405
+
406
+ lag_measurements: list[ms] = []
407
+
408
+ # Collect samples concurrently for better performance
409
+ with ThreadPoolExecutor(max_workers=min(samples, 5)) as executor:
410
+ try:
411
+ # Submit all measurement tasks
412
+ future_to_sample = {executor.submit(measure_single_lag): i for i in range(samples)}
413
+
414
+ # Collect results with timeout
415
+ for future in as_completed(future_to_sample, timeout=timeout_seconds):
416
+ lag = future.result()
417
+ if lag is not None:
418
+ lag_measurements.append(lag)
419
+
420
+ except TimeoutError:
421
+ if self.debugging:
422
+ logger.warning(
423
+ "lag-calculation-timeout",
424
+ collected_samples=len(lag_measurements),
425
+ requested_samples=samples,
426
+ )
427
+
428
+ if len(lag_measurements) < max(2, samples // 2):
429
+ msg = f"Insufficient valid samples: got {len(lag_measurements)}, need at least {max(2, samples // 2)}"
430
+ raise RuntimeError(msg)
431
+
432
+ # Remove outliers using interquartile range method
433
+ QUARTILES = 4
434
+ if len(lag_measurements) >= QUARTILES:
435
+ try:
436
+ q1 = statistics.quantiles(lag_measurements, n=QUARTILES)[0]
437
+ q3 = statistics.quantiles(lag_measurements, n=QUARTILES)[2]
438
+ iqr = q3 - q1
439
+ lower_bound = q1 - 1.5 * iqr
440
+ upper_bound = q3 + 1.5 * iqr
441
+
442
+ filtered_measurements = [lag for lag in lag_measurements if lower_bound <= lag <= upper_bound]
443
+
444
+ # Use filtered data if we still have enough samples
445
+ if len(filtered_measurements) >= 2: # noqa: PLR2004
446
+ lag_measurements = filtered_measurements
447
+
448
+ except statistics.StatisticsError:
449
+ # Fall back to original measurements if filtering fails
450
+ pass
451
+
452
+ # Calculate final lag using median for robustness
453
+ final_lag = ms(statistics.median(lag_measurements))
454
+
455
+ if self.debugging:
456
+ logger.debug(
457
+ "lag-calculated",
458
+ samples_collected=len(lag_measurements),
459
+ lag_ms=final_lag,
460
+ min_lag=min(lag_measurements),
461
+ max_lag=max(lag_measurements),
462
+ std_dev=statistics.stdev(lag_measurements) if len(lag_measurements) > 1 else 0,
463
+ )
445
464
 
446
- return ms(sum(lag_list) / len(lag_list))
465
+ return final_lag
447
466
 
448
467
  def get_remaining_limit(self) -> int:
449
468
  """Get the remaining rate limit
@@ -576,7 +595,6 @@ class Bitvavo:
576
595
 
577
596
  ---
578
597
  Args:
579
- # TODO(NostraDavid) fill these in
580
598
  ```python
581
599
  endpoint: str = "/order"
582
600
  postfix: str = "" # ?key=value&key2=another_value&...
@@ -1045,14 +1063,14 @@ class Bitvavo:
1045
1063
  # timestamp is converted to datetime, numeric columns to float
1046
1064
  ```
1047
1065
  """
1048
- options = _default(options, {})
1066
+ options = default(options, {})
1049
1067
  options["interval"] = interval
1050
1068
  if limit is not None:
1051
1069
  options["limit"] = limit
1052
1070
  if start is not None:
1053
- options["start"] = _epoch_millis(start)
1071
+ options["start"] = epoch_millis(start)
1054
1072
  if end is not None:
1055
- options["end"] = _epoch_millis(end)
1073
+ options["end"] = epoch_millis(end)
1056
1074
  postfix = create_postfix(options)
1057
1075
  result = self.public_request(f"{self.base}/{market}/candles{postfix}") # type: ignore[return-value]
1058
1076
  return convert_candles_to_dataframe(result, output_format)
@@ -1279,7 +1297,7 @@ class Bitvavo:
1279
1297
  ]
1280
1298
  ```
1281
1299
  """
1282
- options = _default(options, {})
1300
+ options = default(options, {})
1283
1301
  rateLimitingWeight = 25
1284
1302
  if "market" in options:
1285
1303
  rateLimitingWeight = 1
@@ -1796,7 +1814,7 @@ class Bitvavo:
1796
1814
  ]
1797
1815
  ```
1798
1816
  """ # noqa: E501
1799
- options = _default(options, {})
1817
+ options = default(options, {})
1800
1818
  options["market"] = market
1801
1819
  postfix = create_postfix(options)
1802
1820
  return self.private_request("/orders", postfix, {}, "GET", 5) # type: ignore[return-value]
@@ -1894,7 +1912,7 @@ class Bitvavo:
1894
1912
  ]
1895
1913
  ```
1896
1914
  """
1897
- options = _default(options, {})
1915
+ options = default(options, {})
1898
1916
  rateLimitingWeight = 25
1899
1917
  if "market" in options:
1900
1918
  rateLimitingWeight = 1
@@ -1960,7 +1978,7 @@ class Bitvavo:
1960
1978
  ]
1961
1979
  ```
1962
1980
  """ # noqa: E501
1963
- options = _default(options, {})
1981
+ options = default(options, {})
1964
1982
  options["market"] = market
1965
1983
  postfix = create_postfix(options)
1966
1984
  result = self.private_request("/trades", postfix, {}, "GET", 5) # type: ignore[return-value]
@@ -2489,7 +2507,6 @@ class Bitvavo:
2489
2507
  time.sleep(0.1)
2490
2508
 
2491
2509
  def do_send(self, ws: WebSocketApp, message: str, private: bool = False) -> None: # noqa: FBT001, FBT002
2492
- # TODO(NostraDavid): add nap-time to the websocket, or do it here; I don't know yet.
2493
2510
  if private and self.APIKEY == "":
2494
2511
  logger.error(
2495
2512
  "no-apikey",
@@ -3961,8 +3978,6 @@ class Bitvavo:
3961
3978
  self.do_send(self.ws, json.dumps(options), True)
3962
3979
 
3963
3980
  def subscription_ticker(self, market: str, callback: Callable[[Any], None]) -> None:
3964
- # TODO(NostraDavid): one possible improvement here is to turn `market` into a list of markets, so we can sub
3965
- # to all of them at once. Same goes for other `subscription*()`
3966
3981
  """
3967
3982
  Subscribe to the ticker channel, which means `callback` gets passed the new best bid or ask whenever they
3968
3983
  change (server-side).
@@ -165,7 +165,9 @@ def convert_candles_to_dataframe(data: Any, output_format: str | OutputFormat) -
165
165
  # Convert list of lists to list of dicts first
166
166
  columns = ["timestamp", "open", "high", "low", "close", "volume"]
167
167
  dict_data = [
168
- dict(zip(columns, candle)) for candle in data if isinstance(candle, list) and len(candle) >= len(columns)
168
+ dict(zip(columns, candle, strict=True))
169
+ for candle in data
170
+ if isinstance(candle, list) and len(candle) >= len(columns)
169
171
  ]
170
172
 
171
173
  if not dict_data:
@@ -105,7 +105,7 @@ class BitvavoSettings(BaseSettings):
105
105
  PREFER_KEYLESS: bool = Field(default=True)
106
106
 
107
107
  # Configuration for Pydantic Settings
108
- model_config: SettingsConfigDict = SettingsConfigDict(
108
+ model_config = SettingsConfigDict(
109
109
  env_file=Path.cwd() / ".env",
110
110
  env_file_encoding="utf-8",
111
111
  env_prefix="BITVAVO_",
@@ -4,7 +4,7 @@ to clearify the intention or semantics/meaning/unit of a variable
4
4
  """
5
5
 
6
6
  import sys
7
- from typing import Any, Union
7
+ from typing import Any
8
8
 
9
9
  if sys.version_info >= (3, 11):
10
10
  from enum import StrEnum
@@ -26,7 +26,7 @@ anydict = dict[str, Any]
26
26
  strdict = dict[str, str]
27
27
  intdict = dict[str, int]
28
28
  # can't use | here, with __future__. Not sure why.
29
- strintdict = dict[str, Union[str, int]]
29
+ strintdict = dict[str, str | int]
30
30
  errordict = dict[str, Any] # same type as anydict, but the semantics/meaning is different
31
31
 
32
32
  # note: You can also use these for type conversion, so instead of int(some_float / 1000), you can just do ms(some_float
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 3.0.0
3
+ Version: 4.1.0
4
4
  Summary: A unit-tested fork of the Bitvavo API
5
5
  Author: Bitvavo BV (original code), NostraDavid
6
6
  Author-email: NostraDavid <55331731+NostraDavid@users.noreply.github.com>
@@ -21,10 +21,12 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python
23
23
  Classifier: Typing :: Typed
24
- Requires-Dist: pydantic-settings==2.*,>=2.6
25
- Requires-Dist: requests==2.*,>=2.26
26
- Requires-Dist: structlog>=21.5,==25.*
27
- Requires-Dist: websocket-client==1.*,>=1.2
24
+ Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: pydantic-settings>=2.6
26
+ Requires-Dist: requests>=2.26
27
+ Requires-Dist: returns>=0.23.0
28
+ Requires-Dist: structlog>=21.5
29
+ Requires-Dist: websocket-client>=1.2
28
30
  Requires-Dist: cudf-cu12>=24.0.0 ; extra == 'cudf'
29
31
  Requires-Dist: narwhals>=2.0.0 ; extra == 'cudf'
30
32
  Requires-Dist: narwhals[dask]>=2.0.0 ; extra == 'dask'
@@ -33,13 +35,16 @@ Requires-Dist: narwhals[ibis]>=2.0.0 ; extra == 'ibis'
33
35
  Requires-Dist: narwhals[modin]>=2.0.0 ; extra == 'modin'
34
36
  Requires-Dist: narwhals[pandas]>=2.0.0 ; extra == 'pandas'
35
37
  Requires-Dist: narwhals[polars]>=2.0.0 ; extra == 'polars'
38
+ Requires-Dist: cudf-polars-cu12>=24.0.0 ; extra == 'polars-gpu'
39
+ Requires-Dist: polars ; extra == 'polars-gpu'
40
+ Requires-Dist: narwhals>=2.0.0 ; extra == 'polars-gpu'
36
41
  Requires-Dist: narwhals[pyarrow]>=2.0.0 ; extra == 'pyarrow'
37
42
  Requires-Dist: narwhals[pyspark]>=2.0.0 ; extra == 'pyspark'
38
43
  Requires-Dist: narwhals[pyspark-connect]>=2.0.0 ; extra == 'pyspark-connect'
39
44
  Requires-Dist: narwhals[sqlframe]>=2.0.0 ; extra == 'sqlframe'
40
45
  Maintainer: NostraDavid
41
46
  Maintainer-email: NostraDavid <55331731+NostraDavid@users.noreply.github.com>
42
- Requires-Python: >=3.9
47
+ Requires-Python: >=3.10
43
48
  Project-URL: changelog, https://github.com/Thaumatorium/bitvavo-api-upgraded/blob/master/CHANGELOG.md
44
49
  Project-URL: homepage, https://github.com/Thaumatorium/bitvavo-api-upgraded
45
50
  Project-URL: repository, https://github.com/Thaumatorium/bitvavo-api-upgraded
@@ -50,6 +55,7 @@ Provides-Extra: ibis
50
55
  Provides-Extra: modin
51
56
  Provides-Extra: pandas
52
57
  Provides-Extra: polars
58
+ Provides-Extra: polars-gpu
53
59
  Provides-Extra: pyarrow
54
60
  Provides-Extra: pyspark
55
61
  Provides-Extra: pyspark-connect
@@ -58,7 +64,7 @@ Description-Content-Type: text/markdown
58
64
 
59
65
  # Bitvavo API (upgraded)
60
66
 
61
- A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency exchange API. This is an "upgraded" fork of the official Bitvavo SDK with comprehensive type hints, unit tests, and improved developer experience.
67
+ A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency exchange API. This is an "upgraded" fork of the official Bitvavo SDK with comprehensive type hints, unit tests, modern architecture, and improved developer experience.
62
68
 
63
69
  ## Quick Start
64
70
 
@@ -66,6 +72,23 @@ A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency
66
72
  pip install bitvavo_api_upgraded
67
73
  ```
68
74
 
75
+ ### Basic Usage
76
+
77
+ ```python
78
+ # Option 1: Original Bitvavo interface (legacy)
79
+ from bitvavo_api_upgraded import Bitvavo
80
+
81
+ bitvavo = Bitvavo({'APIKEY': 'your-key', 'APISECRET': 'your-secret'})
82
+ balance = bitvavo.balance({})
83
+
84
+ # Option 2: New modular BitvavoClient interface (recommended)
85
+ from bitvavo_client import BitvavoClient, BitvavoSettings
86
+
87
+ client = BitvavoClient()
88
+ result = client.public.time() # No authentication needed
89
+ result = client.private.balance() # Authentication required
90
+ ```
91
+
69
92
  ### Optional Dataframe Support
70
93
 
71
94
  This package supports multiple dataframe libraries via [Narwhals](https://narwhals-dev.github.io/narwhals/), providing a unified interface across:
@@ -112,66 +135,127 @@ Scroll down for detailed usage examples and configuration instructions.
112
135
 
113
136
  This wrapper improves upon the official Bitvavo SDK with:
114
137
 
115
- - 🎯 **Complete type annotations** for all functions and classes
116
- - 🧪 **Comprehensive test suite** (found and fixed multiple bugs in the original)
117
- - 📋 **Detailed changelog** tracking all changes and improvements
118
- - 🔄 **Up-to-date API compliance** including MiCA regulatory requirements
119
- - 📚 **Enhanced documentation** with examples and clear usage patterns
120
- - 🐍 **Modern Python support** (3.9+, dropped EOL versions)
121
- - 🔑 **Multi-key & keyless support** for enhanced rate limiting and public access
122
- - **Better error handling** and rate limiting
123
- - 🔧 **Developer-friendly tooling** (ruff, mypy, pre-commit hooks)
124
- - 📊 **Unified dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, SQLFrame)
138
+ ### Modern Architecture
139
+
140
+ - **Modular design**: Clean separation between public/private APIs, transport, and authentication
141
+ - **Two interfaces**: Legacy `Bitvavo` class for backward compatibility + new `BitvavoClient` for modern development
142
+ - **Dependency injection**: Testable, maintainable, and extensible codebase
143
+ - **Type safety**: Comprehensive type annotations with generics and precise return types
144
+
145
+ ### Quality & Reliability
146
+
147
+ - **Comprehensive test suite** (found and fixed multiple bugs in the original)
148
+ - **100% type coverage** with mypy strict mode
149
+ - **Enhanced error handling** with detailed validation messages
150
+ - **Rate limiting** with automatic throttling and multi-key support
151
+
152
+ ### Data Format Flexibility
153
+
154
+ - **Unified dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, SQLFrame)
155
+ - **Pydantic models** for validated, structured data
156
+ - **Raw dictionary access** for backward compatibility
157
+ - **Result types** for functional error handling
158
+
159
+ ### Enhanced Performance
160
+
161
+ - **Multi-key support** for better rate limiting and load distribution
162
+ - **Keyless access** for public endpoints (doesn't count against your API limits)
163
+ - **Connection pooling** and retry logic
164
+ - **Async-ready architecture** (async support coming in future release)
165
+
166
+ ### Developer Experience
167
+
168
+ - **Modern Python support** (3.9+, dropped EOL versions)
169
+ - **Configuration via environment variables** or Pydantic settings
170
+ - **Detailed changelog** tracking all changes and improvements
171
+ - **Enhanced documentation** with examples and clear usage patterns
172
+ - **Developer-friendly tooling** (ruff, mypy, pre-commit hooks)
125
173
 
126
174
  ## Features
127
175
 
128
176
  ### Full API Coverage
129
177
 
130
- - All REST endpoints (public and private)
131
- - ✅ **Multiple API key support** with automatic load balancing
132
- - ✅ **Keyless access** for public endpoints without authentication
133
- - ✅ **Comprehensive dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, and more)
134
- - WebSocket support with reconnection logic
135
- - Rate limiting with automatic throttling
136
- - MiCA compliance reporting endpoints
178
+ - All REST endpoints (public and private)
179
+ - Multiple API key support with automatic load balancing
180
+ - Keyless access for public endpoints without authentication
181
+ - Comprehensive dataframe support via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, and more)
182
+ - WebSocket support with reconnection logic
183
+ - Rate limiting with automatic throttling
184
+ - MiCA compliance reporting endpoints
137
185
 
138
186
  ### Developer Experience
139
187
 
140
- - Type hints for better IDE support
141
- - Comprehensive error handling
142
- - Detailed logging with `structlog`
143
- - Configuration via `.env` files
144
- - Extensive test coverage
188
+ - Type hints for better IDE support
189
+ - Comprehensive error handling
190
+ - Detailed logging with `structlog`
191
+ - Configuration via `.env` files
192
+ - Extensive test coverage
145
193
 
146
194
  ### Production Ready
147
195
 
148
- - Automatic rate limit management
149
- - Multi-key failover support
150
- - Connection retry logic
151
- - Proper error responses
152
- - Memory efficient WebSocket handling
196
+ - Automatic rate limit management
197
+ - Multi-key failover support
198
+ - Connection retry logic
199
+ - Proper error responses
200
+ - Memory efficient WebSocket handling
153
201
 
154
202
  ## Configuration
155
203
 
204
+ ### Environment Variables
205
+
156
206
  Create a `.env` file in your project root:
157
207
 
158
208
  ```env
159
- # Single API key (traditional)
160
- BITVAVO_APIKEY=your-api-key-here
161
- BITVAVO_APISECRET=your-api-secret-here
209
+ # API authentication
210
+ BITVAVO_API_KEY=your-api-key-here
211
+ BITVAVO_API_SECRET=your-api-secret-here
212
+
213
+ # Multi-key support (JSON array as string)
214
+ # BITVAVO_API_KEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
215
+
216
+ # Client behavior
217
+ BITVAVO_PREFER_KEYLESS=true # Use keyless for public endpoints
218
+ BITVAVO_DEFAULT_RATE_LIMIT=1000 # Rate limit per key
219
+ BITVAVO_RATE_LIMIT_BUFFER=50 # Buffer to avoid hitting limits
220
+ BITVAVO_DEBUGGING=false # Enable debug logging
221
+
222
+ # API endpoints (usually not needed to change)
223
+ BITVAVO_REST_URL=https://api.bitvavo.com/v2
224
+ BITVAVO_WS_URL=wss://ws.bitvavo.com/v2/
225
+ ```
226
+
227
+ ### Usage Examples
228
+
229
+ #### New BitvavoClient (Recommended)
230
+
231
+ ```python
232
+ from bitvavo_client import BitvavoClient, BitvavoSettings
233
+
234
+ # Option 1: Auto-load from .env file
235
+ client = BitvavoClient()
236
+
237
+ # Option 2: Custom settings
238
+ settings = BitvavoSettings(
239
+ api_key="your-key",
240
+ api_secret="your-secret",
241
+ prefer_keyless=True,
242
+ debugging=True
243
+ )
244
+ client = BitvavoClient(settings)
162
245
 
163
- # Multiple API keys (for rate limiting)
164
- # BITVAVO_APIKEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
246
+ # Option 3: Manual settings override
247
+ client = BitvavoClient(BitvavoSettings(default_rate_limit=750))
165
248
 
166
- # Keyless access (public endpoints only)
167
- BITVAVO_PREFER_KEYLESS=true
249
+ # Access public endpoints (no auth needed)
250
+ time_result = client.public.time()
251
+ markets_result = client.public.markets()
168
252
 
169
- # Enhanced settings
170
- BITVAVO_API_UPGRADED_DEFAULT_RATE_LIMIT=750
171
- BITVAVO_API_UPGRADED_PREFER_KEYLESS=false
253
+ # Access private endpoints (auth required)
254
+ balance_result = client.private.balance()
255
+ account_result = client.private.account()
172
256
  ```
173
257
 
174
- Then use the settings:
258
+ #### Legacy Bitvavo Class (Backward Compatibility)
175
259
 
176
260
  ```python
177
261
  from bitvavo_api_upgraded import Bitvavo, BitvavoSettings
@@ -198,6 +282,68 @@ bitvavo = Bitvavo({
198
282
  bitvavo = Bitvavo({'PREFER_KEYLESS': True})
199
283
  ```
200
284
 
285
+ ## Data Format Flexibility
286
+
287
+ The new BitvavoClient supports multiple output formats to match your workflow:
288
+
289
+ ### Model Preferences
290
+
291
+ ```python
292
+ from bitvavo_client import BitvavoClient
293
+ from bitvavo_client.core.model_preferences import ModelPreference
294
+
295
+ # Option 1: Raw dictionaries (default, backward compatible)
296
+ client = BitvavoClient(preferred_model=ModelPreference.RAW)
297
+ result = client.public.time() # Returns: {"time": 1609459200000}
298
+
299
+ # Option 2: Validated Pydantic models
300
+ client = BitvavoClient(preferred_model=ModelPreference.PYDANTIC)
301
+ result = client.public.time() # Returns: ServerTime(time=1609459200000)
302
+
303
+ # Option 3: DataFrame format (pandas, polars, etc.)
304
+ client = BitvavoClient(preferred_model=ModelPreference.DATAFRAME)
305
+ result = client.public.markets() # Returns: polars.DataFrame with market data
306
+ ```
307
+
308
+ ### Per-Request Format Override
309
+
310
+ ```python
311
+ # Set a default preference but override per request
312
+ client = BitvavoClient(preferred_model=ModelPreference.RAW)
313
+
314
+ # Get raw dict (uses default)
315
+ raw_data = client.public.markets()
316
+
317
+ # Override to get DataFrame for this request
318
+ import polars as pl
319
+ df_data = client.public.markets(model=pl.DataFrame)
320
+
321
+ # Override to get Pydantic model
322
+ from bitvavo_client.core.public_models import Markets
323
+ validated_data = client.public.markets(model=Markets)
324
+ ```
325
+
326
+ ### Result Types for Error Handling
327
+
328
+ ```python
329
+ from returns.result import Success, Failure
330
+
331
+ # Use result types for functional error handling
332
+ result = client.public.time()
333
+
334
+ if isinstance(result, Success):
335
+ print(f"Server time: {result.unwrap()}")
336
+ elif isinstance(result, Failure):
337
+ print(f"Error: {result.failure()}")
338
+
339
+ # Or use match-case (Python 3.10+)
340
+ match result:
341
+ case Success(value):
342
+ print(f"Success: {value}")
343
+ case Failure(error):
344
+ print(f"Error: {error}")
345
+ ```
346
+
201
347
  ## WebSocket Usage
202
348
 
203
349
  ```python
@@ -287,9 +433,44 @@ balance = bitvavo.balance({})
287
433
 
288
434
  ## API Examples
289
435
 
290
- ### Public Endpoints (No Authentication)
436
+ ### Public Endpoints (No Authentication Required)
437
+
438
+ #### New BitvavoClient Interface
291
439
 
292
440
  ```python
441
+ from bitvavo_client import BitvavoClient
442
+
443
+ client = BitvavoClient()
444
+
445
+ # Get server time
446
+ time_result = client.public.time()
447
+
448
+ # Get all markets
449
+ markets_result = client.public.markets()
450
+
451
+ # Get specific market
452
+ btc_market = client.public.markets(market='BTC-EUR')
453
+
454
+ # Get order book
455
+ book_result = client.public.book('BTC-EUR')
456
+
457
+ # Get recent trades
458
+ trades_result = client.public.trades('BTC-EUR')
459
+
460
+ # Get 24h ticker
461
+ ticker_result = client.public.ticker_24h(market='BTC-EUR')
462
+
463
+ # Get candlestick data
464
+ candles_result = client.public.candles('BTC-EUR', '1h')
465
+ ```
466
+
467
+ #### Legacy Bitvavo Interface
468
+
469
+ ```python
470
+ from bitvavo_api_upgraded import Bitvavo
471
+
472
+ bitvavo = Bitvavo({'PREFER_KEYLESS': True}) # For public endpoints
473
+
293
474
  # Get server time
294
475
  time_resp = bitvavo.time()
295
476
 
@@ -311,7 +492,50 @@ ticker = bitvavo.ticker24h({'market': 'BTC-EUR'})
311
492
 
312
493
  ### Private Endpoints (Authentication Required)
313
494
 
495
+ #### New BitvavoClient Interface
496
+
497
+ ```python
498
+ from bitvavo_client import BitvavoClient, BitvavoSettings
499
+
500
+ # Configure with API credentials
501
+ settings = BitvavoSettings(api_key="your-key", api_secret="your-secret")
502
+ client = BitvavoClient(settings)
503
+
504
+ # Get account info
505
+ account_result = client.private.account()
506
+
507
+ # Get balance
508
+ balance_result = client.private.balance()
509
+
510
+ # Place order
511
+ order_result = client.private.place_order(
512
+ market="BTC-EUR",
513
+ side="buy",
514
+ order_type="limit",
515
+ amount="0.01",
516
+ price="45000"
517
+ )
518
+
519
+ # Get order history
520
+ orders_result = client.private.orders('BTC-EUR')
521
+
522
+ # Cancel order
523
+ cancel_result = client.private.cancel_order(
524
+ market="BTC-EUR",
525
+ order_id="order-id-here"
526
+ )
527
+
528
+ # Get trades
529
+ trades_result = client.private.trades('BTC-EUR')
530
+ ```
531
+
532
+ #### Legacy Bitvavo Interface
533
+
314
534
  ```python
535
+ from bitvavo_api_upgraded import Bitvavo
536
+
537
+ bitvavo = Bitvavo({'APIKEY': 'your-key', 'APISECRET': 'your-secret'})
538
+
315
539
  # Get account info
316
540
  account = bitvavo.account()
317
541
 
@@ -523,15 +747,49 @@ uv run ruff format
523
747
  ### Project Structure
524
748
 
525
749
  ```text
526
- src/bitvavo_api_upgraded/ # Source code
527
- ├── __init__.py # Main exports
528
- ├── bitvavo.py # Core API class
529
- ├── settings.py # Pydantic settings
530
- ├── helper_funcs.py # Utility functions
531
- └── type_aliases.py # Type definitions
532
-
533
- tests/ # Comprehensive test suite
534
- docs/ # Documentation
750
+ src/
751
+ ├── bitvavo_api_upgraded/ # Legacy interface (backward compatibility)
752
+ ├── __init__.py # Main exports
753
+ ├── bitvavo.py # Original monolithic API class
754
+ ├── settings.py # Pydantic settings
755
+ │ ├── helper_funcs.py # Utility functions
756
+ │ └── type_aliases.py # Type definitions
757
+ └── bitvavo_client/ # Modern modular interface
758
+ ├── __init__.py # New client exports
759
+ ├── facade.py # Main BitvavoClient class
760
+ ├── core/ # Core functionality
761
+ │ ├── settings.py # Settings management
762
+ │ ├── models.py # Pydantic data models
763
+ │ ├── validation_helpers.py # Enhanced error handling
764
+ │ └── types.py # Type definitions
765
+ ├── endpoints/ # API endpoint handlers
766
+ │ ├── public.py # Public API endpoints
767
+ │ ├── private.py # Private API endpoints
768
+ │ └── common.py # Shared endpoint utilities
769
+ ├── transport/ # HTTP transport layer
770
+ │ └── http.py # HTTP client with connection pooling
771
+ ├── auth/ # Authentication & authorization
772
+ │ ├── signing.py # Request signing
773
+ │ └── rate_limit.py # Rate limiting management
774
+ ├── adapters/ # External integrations
775
+ │ └── returns_adapter.py # Result type adapters
776
+ ├── schemas/ # DataFrame schemas
777
+ │ ├── public_schemas.py # Public endpoint schemas
778
+ │ └── private_schemas.py # Private endpoint schemas
779
+ └── df/ # DataFrame conversion
780
+ └── convert.py # Narwhals-based converters
781
+
782
+ tests/ # Comprehensive test suite
783
+ ├── bitvavo_api_upgraded/ # Legacy interface tests
784
+ └── bitvavo_client/ # Modern interface tests
785
+ ├── core/ # Core functionality tests
786
+ ├── endpoints/ # Endpoint tests
787
+ ├── transport/ # Transport layer tests
788
+ ├── auth/ # Authentication tests
789
+ ├── adapters/ # Adapter tests
790
+ └── df/ # DataFrame tests
791
+
792
+ docs/ # Documentation
535
793
  ```
536
794
 
537
795
  ### Semantic Versioning
@@ -546,47 +804,110 @@ This project follows [semantic versioning](https://semver.org/):
546
804
 
547
805
  This package includes a `py.typed` file to enable type checking. Reference: [Don't forget py.typed for your typed Python package](https://blog.whtsky.me/tech/2021/dont-forget-py.typed-for-your-typed-python-package/)
548
806
 
549
- ## Migration from Official SDK
807
+ ## Migration & Architecture Options
808
+
809
+ This package provides **two interfaces** to suit different use cases:
810
+
811
+ ### 1. Legacy Bitvavo Class (Backward Compatibility)
812
+
813
+ For existing users migrating from the official SDK:
550
814
 
551
- ### Key Differences
815
+ ```python
816
+ from bitvavo_api_upgraded import Bitvavo
552
817
 
553
- - Import: `from bitvavo_api_upgraded import Bitvavo` (instead of `from python_bitvavo_api.bitvavo import Bitvavo`)
554
- - **Breaking Change**: Trading operations require `operatorId` parameter
555
- - **New**: Multiple API key support for better rate limiting
818
+ # Drop-in replacement for python_bitvavo_api.bitvavo
819
+ bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
820
+ balance = bitvavo.balance({})
821
+ ```
822
+
823
+ ### 2. New BitvavoClient (Modern Architecture)
824
+
825
+ For new projects or those wanting better architecture:
826
+
827
+ ```python
828
+ from bitvavo_client import BitvavoClient, BitvavoSettings
829
+
830
+ # Modern, typed, modular interface
831
+ client = BitvavoClient()
832
+ result = client.public.time()
833
+ result = client.private.balance()
834
+ ```
835
+
836
+ ### Migration from Official SDK
837
+
838
+ #### Key Changes
839
+
840
+ - **Import**: `from bitvavo_api_upgraded import Bitvavo` (instead of `from python_bitvavo_api.bitvavo import Bitvavo`)
841
+ - **Breaking**: Trading operations require `operatorId` parameter
842
+ - **Enhanced**: Better error handling and type safety
843
+ - **New**: Modern `BitvavoClient` interface available
844
+ - **New**: Multiple API key support for rate limiting
556
845
  - **New**: Keyless access for public endpoints
557
846
  - **New**: Comprehensive dataframe support
558
- - Enhanced error handling and type safety
559
- - Better configuration management with `.env` support
847
+ - **New**: Configuration via `.env` files
560
848
 
561
- ### Migration Steps
849
+ #### Migration Steps
562
850
 
563
- 1. Update import statements
564
- 2. Add `operatorId` to trading method calls
565
- 3. Optional: Migrate to `.env` configuration
566
- 4. Optional: Configure multiple API keys for better rate limits
567
- 5. Optional: Enable keyless mode for public endpoint efficiency
568
- 6. Enjoy improved type hints and error handling!
851
+ 1. **Update import statements**
569
852
 
570
- ### Enhanced Features (Optional)
853
+ ```python
854
+ # Old
855
+ from python_bitvavo_api.bitvavo import Bitvavo
571
856
 
572
- ```python
573
- # Traditional single key (works as before)
574
- bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
857
+ # New (legacy interface)
858
+ from bitvavo_api_upgraded import Bitvavo
575
859
 
576
- # New: Multiple keys for rate limiting
577
- bitvavo = Bitvavo({
578
- 'APIKEYS': [
579
- {'key': 'key1', 'secret': 'secret1'},
580
- {'key': 'key2', 'secret': 'secret2'}
581
- ]
582
- })
860
+ # New (modern interface)
861
+ from bitvavo_client import BitvavoClient
862
+ ```
583
863
 
584
- # New: Keyless for public endpoints
585
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
864
+ 2. **Add operatorId to trading operations**
586
865
 
587
- # New: Dataframe support
588
- markets_df = bitvavo.markets({}, output_format='pandas')
589
- ```
866
+ ```python
867
+ # Add operatorId parameter to placeOrder, cancelOrder, etc.
868
+ order = bitvavo.placeOrder("BTC-EUR", "buy", "limit", {...}, operatorId=12345)
869
+ ```
870
+
871
+ 3. **Optional: Migrate to modern interface**
872
+
873
+ ```python
874
+ # Legacy style
875
+ bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
876
+
877
+ # Modern style
878
+ client = BitvavoClient(BitvavoSettings(api_key='key', api_secret='secret'))
879
+ ```
880
+
881
+ 4. **Optional: Use new features**
882
+
883
+ ```python
884
+ # Multi-key support
885
+ bitvavo = Bitvavo({'APIKEYS': [{'key': 'k1', 'secret': 's1'}, {'key': 'k2', 'secret': 's2'}]})
886
+
887
+ # Keyless for public endpoints
888
+ bitvavo = Bitvavo({'PREFER_KEYLESS': True})
889
+
890
+ # DataFrame support
891
+ markets_df = bitvavo.markets({}, output_format='pandas')
892
+ ```
893
+
894
+ ### Choosing an Interface
895
+
896
+ | Feature | Legacy `Bitvavo` | Modern `BitvavoClient` |
897
+ | -------------------------- | ---------------------- | -------------------------- |
898
+ | **Backward compatibility** | ✅ Drop-in replacement | ❌ New interface |
899
+ | **Type safety** | ✅ Typed responses | ✅ Full generics support |
900
+ | **Error handling** | ✅ Enhanced errors | ✅ Result types + enhanced |
901
+ | **Modular design** | ❌ Monolithic | ✅ Separated concerns |
902
+ | **Testing** | ✅ Testable | ✅ Highly testable |
903
+ | **DataFrame support** | ✅ Via output_format | ✅ Via model preferences |
904
+ | **Result types** | ❌ Exceptions only | ✅ Success/Failure pattern |
905
+ | **WebSocket support** | ✅ Full support | 🚧 Coming soon |
906
+
907
+ **Recommendation**:
908
+
909
+ - Use **Legacy `Bitvavo`** for quick migrations and WebSocket usage
910
+ - Use **Modern `BitvavoClient`** for new projects requiring clean architecture
590
911
 
591
912
  ---
592
913
 
@@ -0,0 +1,10 @@
1
+ bitvavo_api_upgraded/__init__.py,sha256=J_HdGBmZOfb1eOydaxsPmXfOIZ58hVa1qAfE6QErUHs,301
2
+ bitvavo_api_upgraded/bitvavo.py,sha256=_3FRVVPg7_1HrALyGPjcuokCsHF5oz6itN_GKx7yTMo,166155
3
+ bitvavo_api_upgraded/dataframe_utils.py,sha256=UvcDM0HeE-thUvsm9EjCmddmGBzZ9Puu40UVa0fR_p8,5821
4
+ bitvavo_api_upgraded/helper_funcs.py,sha256=4oBdQ1xB-C2XkQTmN-refzIzWfO-IUowDSWhOSFdCRU,3212
5
+ bitvavo_api_upgraded/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ bitvavo_api_upgraded/settings.py,sha256=I1fogU6_kb1hOe_0YDzOgDhzKfnnYFoIR2OXbwtyD4E,5291
7
+ bitvavo_api_upgraded/type_aliases.py,sha256=SbPBcuKWJZPZ8DSDK-Uycu5O-TUO6ejVaTt_7oyGyIU,1979
8
+ bitvavo_api_upgraded-4.1.0.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
9
+ bitvavo_api_upgraded-4.1.0.dist-info/METADATA,sha256=Go7SrHcYNBc3aZKxf09IBC8nudujCyN_Ze7tZST3mZI,35857
10
+ bitvavo_api_upgraded-4.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.13
2
+ Generator: uv 0.8.15
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- bitvavo_api_upgraded/__init__.py,sha256=J_HdGBmZOfb1eOydaxsPmXfOIZ58hVa1qAfE6QErUHs,301
2
- bitvavo_api_upgraded/bitvavo.py,sha256=PHN4lPlA2wWB5e6LAOewQLGqN--x_N_BLg1A25z5TxA,164948
3
- bitvavo_api_upgraded/dataframe_utils.py,sha256=Jf-2zkYuK5Zs9kiMFJlCmO-OykjZyFIvW2aaMJYPUpo,5792
4
- bitvavo_api_upgraded/helper_funcs.py,sha256=4oBdQ1xB-C2XkQTmN-refzIzWfO-IUowDSWhOSFdCRU,3212
5
- bitvavo_api_upgraded/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- bitvavo_api_upgraded/settings.py,sha256=br1oqN3K8OoimmKa1JlvzMgpYNHfr2kK2f14tC72dl8,5311
7
- bitvavo_api_upgraded/type_aliases.py,sha256=EOd3LhruyM1aZYn4xyKYhdoSql8mZH88acN0qGzMMck,1992
8
- bitvavo_api_upgraded-3.0.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
9
- bitvavo_api_upgraded-3.0.0.dist-info/METADATA,sha256=kFAZJlSH89h5kmUC81QGg-t0jpwIkiJhsnyYLdEcxso,25478
10
- bitvavo_api_upgraded-3.0.0.dist-info/RECORD,,