bitvavo-api-upgraded 4.1.0__py3-none-any.whl → 4.1.1__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-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/METADATA +1 -1
- bitvavo_api_upgraded-4.1.1.dist-info/RECORD +38 -0
- bitvavo_client/__init__.py +9 -0
- bitvavo_client/adapters/__init__.py +1 -0
- bitvavo_client/adapters/returns_adapter.py +363 -0
- bitvavo_client/auth/__init__.py +1 -0
- bitvavo_client/auth/rate_limit.py +104 -0
- bitvavo_client/auth/signing.py +33 -0
- bitvavo_client/core/__init__.py +1 -0
- bitvavo_client/core/errors.py +17 -0
- bitvavo_client/core/model_preferences.py +33 -0
- bitvavo_client/core/private_models.py +886 -0
- bitvavo_client/core/public_models.py +1087 -0
- bitvavo_client/core/settings.py +52 -0
- bitvavo_client/core/types.py +11 -0
- bitvavo_client/core/validation_helpers.py +90 -0
- bitvavo_client/df/__init__.py +1 -0
- bitvavo_client/df/convert.py +86 -0
- bitvavo_client/endpoints/__init__.py +1 -0
- bitvavo_client/endpoints/common.py +88 -0
- bitvavo_client/endpoints/private.py +1090 -0
- bitvavo_client/endpoints/public.py +658 -0
- bitvavo_client/facade.py +66 -0
- bitvavo_client/py.typed +0 -0
- bitvavo_client/schemas/__init__.py +50 -0
- bitvavo_client/schemas/private_schemas.py +191 -0
- bitvavo_client/schemas/public_schemas.py +149 -0
- bitvavo_client/transport/__init__.py +1 -0
- bitvavo_client/transport/http.py +159 -0
- bitvavo_client/ws/__init__.py +1 -0
- bitvavo_api_upgraded-4.1.0.dist-info/RECORD +0 -10
- {bitvavo_api_upgraded-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1090 @@
|
|
1
|
+
"""Private API endpoints that require authentication."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import contextlib
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
import polars as pl
|
10
|
+
from returns.result import Failure, Result, Success
|
11
|
+
|
12
|
+
from bitvavo_client.adapters.returns_adapter import BitvavoError
|
13
|
+
from bitvavo_client.core import private_models
|
14
|
+
from bitvavo_client.core.model_preferences import ModelPreference
|
15
|
+
from bitvavo_client.endpoints.common import create_postfix, default
|
16
|
+
from bitvavo_client.schemas.private_schemas import DEFAULT_SCHEMAS
|
17
|
+
|
18
|
+
if TYPE_CHECKING: # pragma: no cover
|
19
|
+
from bitvavo_client.core.types import AnyDict
|
20
|
+
from bitvavo_client.transport.http import HTTPClient
|
21
|
+
|
22
|
+
T = TypeVar("T")
|
23
|
+
|
24
|
+
|
25
|
+
def _extract_dataframe_data(data: Any, *, items_key: str | None = None) -> list[dict] | dict:
|
26
|
+
"""Extract the meaningful data for DataFrame creation from API responses.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
data: Raw API response data
|
30
|
+
items_key: Key to extract from nested response (e.g., 'items' for paginated responses)
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
List of dicts or single dict suitable for DataFrame creation
|
34
|
+
|
35
|
+
Raises:
|
36
|
+
ValueError: If data format is unexpected
|
37
|
+
"""
|
38
|
+
if items_key and isinstance(data, dict) and items_key in data:
|
39
|
+
# Extract nested items (e.g., transaction_history['items'])
|
40
|
+
items = data[items_key]
|
41
|
+
if not isinstance(items, list):
|
42
|
+
msg = f"Expected {items_key} to be a list, got {type(items)}"
|
43
|
+
raise ValueError(msg)
|
44
|
+
return items
|
45
|
+
if isinstance(data, list):
|
46
|
+
# Direct list response (e.g., balance, trades)
|
47
|
+
return data
|
48
|
+
if isinstance(data, dict):
|
49
|
+
# Single dict response - wrap in list for DataFrame
|
50
|
+
return [data]
|
51
|
+
|
52
|
+
msg = f"Unexpected data type for DataFrame creation: {type(data)}"
|
53
|
+
raise ValueError(msg)
|
54
|
+
|
55
|
+
|
56
|
+
def _create_dataframe_from_data(
|
57
|
+
data: Any, *, items_key: str | None = None, empty_schema: dict[str, Any] | None = None
|
58
|
+
) -> Result[pl.DataFrame, BitvavoError]:
|
59
|
+
"""Create a DataFrame from API response data.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
data: Raw API response data
|
63
|
+
items_key: Key to extract from nested response (optional)
|
64
|
+
empty_schema: Schema to use for empty DataFrames and type casting
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Result containing DataFrame or error
|
68
|
+
"""
|
69
|
+
try:
|
70
|
+
df_data = _extract_dataframe_data(data, items_key=items_key)
|
71
|
+
|
72
|
+
if df_data:
|
73
|
+
# Create DataFrame with flexible schema first
|
74
|
+
df = pl.DataFrame(df_data, strict=False)
|
75
|
+
|
76
|
+
# Apply schema casting if provided
|
77
|
+
if empty_schema:
|
78
|
+
# Cast columns that exist in both DataFrame and schema
|
79
|
+
for col, expected_dtype in empty_schema.items():
|
80
|
+
if col in df.columns:
|
81
|
+
with contextlib.suppress(pl.exceptions.PolarsError, ValueError):
|
82
|
+
df = df.with_columns(pl.col(col).cast(expected_dtype))
|
83
|
+
|
84
|
+
return Success(df) # type: ignore[return-value]
|
85
|
+
|
86
|
+
# Create empty DataFrame with provided schema or minimal default
|
87
|
+
if empty_schema is None:
|
88
|
+
empty_schema = {"id": pl.String} # Minimal default schema
|
89
|
+
df = pl.DataFrame([], schema=empty_schema)
|
90
|
+
return Success(df) # type: ignore[return-value]
|
91
|
+
|
92
|
+
except (ValueError, TypeError, pl.exceptions.PolarsError) as exc:
|
93
|
+
error = BitvavoError(
|
94
|
+
http_status=500,
|
95
|
+
error_code=-1,
|
96
|
+
reason="DataFrame creation failed",
|
97
|
+
message=f"Failed to create DataFrame from API response: {exc}",
|
98
|
+
raw={"data_type": type(data).__name__, "data_sample": str(data)[:200]},
|
99
|
+
)
|
100
|
+
return Failure(error)
|
101
|
+
|
102
|
+
|
103
|
+
class PrivateAPI:
|
104
|
+
"""Handles all private Bitvavo API endpoints requiring authentication."""
|
105
|
+
|
106
|
+
def __init__(
|
107
|
+
self,
|
108
|
+
http_client: HTTPClient,
|
109
|
+
*,
|
110
|
+
preferred_model: ModelPreference | str | None = None,
|
111
|
+
default_schema: dict | None = None,
|
112
|
+
) -> None:
|
113
|
+
"""Initialize private API handler.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
http_client: HTTP client for making requests
|
117
|
+
preferred_model: Preferred model format for responses
|
118
|
+
default_schema: Default schema for DataFrame conversion
|
119
|
+
"""
|
120
|
+
self.http: HTTPClient = http_client
|
121
|
+
self.preferred_model = ModelPreference(preferred_model) if preferred_model else None
|
122
|
+
self.default_schema = default_schema
|
123
|
+
|
124
|
+
def _get_effective_model(
|
125
|
+
self,
|
126
|
+
endpoint_type: str,
|
127
|
+
model: type[T] | Any | None,
|
128
|
+
schema: dict | None,
|
129
|
+
) -> tuple[type[T] | Any | None, dict | None]:
|
130
|
+
"""Get the effective model and schema to use for a request.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
endpoint_type: Type of endpoint (e.g., 'account', 'balance', 'orders')
|
134
|
+
model: Model explicitly passed to method (overrides preference)
|
135
|
+
schema: Schema explicitly passed to method
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Tuple of (effective_model, effective_schema)
|
139
|
+
|
140
|
+
Raises:
|
141
|
+
ValueError: If endpoint_type is not recognized
|
142
|
+
"""
|
143
|
+
# If model is explicitly provided, use it
|
144
|
+
if model is not None:
|
145
|
+
return model, schema
|
146
|
+
|
147
|
+
# If no preferred model is set, return None (raw response)
|
148
|
+
if self.preferred_model is None:
|
149
|
+
return None, schema
|
150
|
+
|
151
|
+
# Apply preference based on enum value
|
152
|
+
if self.preferred_model == ModelPreference.RAW:
|
153
|
+
return Any, schema
|
154
|
+
|
155
|
+
if self.preferred_model == ModelPreference.DATAFRAME:
|
156
|
+
# Validate endpoint type
|
157
|
+
if endpoint_type not in list(DEFAULT_SCHEMAS.keys()):
|
158
|
+
msg = f"Invalid endpoint_type '{endpoint_type}'. Valid types: {sorted(DEFAULT_SCHEMAS)}"
|
159
|
+
raise ValueError(msg)
|
160
|
+
# Use the provided schema, fallback to instance default, then to endpoint-specific default
|
161
|
+
effective_schema = schema or self.default_schema or DEFAULT_SCHEMAS.get(endpoint_type)
|
162
|
+
# Convert to dict if it's a Mapping but not already a dict
|
163
|
+
if effective_schema is not None and not isinstance(effective_schema, dict):
|
164
|
+
effective_schema = dict(effective_schema)
|
165
|
+
return pl.DataFrame, effective_schema
|
166
|
+
|
167
|
+
if self.preferred_model == ModelPreference.PYDANTIC:
|
168
|
+
# Map endpoint types to appropriate Pydantic models
|
169
|
+
endpoint_model_map = {
|
170
|
+
"account": private_models.Account,
|
171
|
+
"balance": private_models.Balances,
|
172
|
+
"orders": private_models.Orders,
|
173
|
+
"order": private_models.Order,
|
174
|
+
"trade_history": private_models.Trades,
|
175
|
+
"transaction_history": private_models.TransactionHistory,
|
176
|
+
"fees": private_models.Fees,
|
177
|
+
"deposit_history": private_models.DepositHistories,
|
178
|
+
"deposit": private_models.Deposit,
|
179
|
+
"withdrawals": private_models.Withdrawals,
|
180
|
+
"withdraw": private_models.WithdrawResponse,
|
181
|
+
"cancel_order": private_models.CancelOrderResponse,
|
182
|
+
}
|
183
|
+
if endpoint_type not in endpoint_model_map:
|
184
|
+
msg = f"No Pydantic model defined for endpoint_type '{endpoint_type}'. Add it to endpoint_model_map."
|
185
|
+
raise ValueError(msg)
|
186
|
+
return endpoint_model_map[endpoint_type], schema
|
187
|
+
|
188
|
+
# Default case (AUTO or unknown)
|
189
|
+
return None, schema
|
190
|
+
|
191
|
+
def _convert_raw_result(
|
192
|
+
self,
|
193
|
+
raw_result: Result[Any, BitvavoError | httpx.HTTPError],
|
194
|
+
endpoint_type: str,
|
195
|
+
model: type[T] | Any | None,
|
196
|
+
schema: dict | None,
|
197
|
+
*,
|
198
|
+
items_key: str | None = None,
|
199
|
+
) -> Result[Any, BitvavoError | httpx.HTTPError]:
|
200
|
+
"""Convert raw API result to the desired model format.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
raw_result: Raw result from HTTP client
|
204
|
+
endpoint_type: Type of endpoint (e.g., 'account', 'balance', 'orders')
|
205
|
+
model: Model explicitly passed to method (overrides preference)
|
206
|
+
schema: Schema explicitly passed to method
|
207
|
+
items_key: Key to extract from nested response for DataFrames
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Result with converted data or original error
|
211
|
+
"""
|
212
|
+
# If the raw result is an error, return it as-is
|
213
|
+
if isinstance(raw_result, Failure):
|
214
|
+
return raw_result
|
215
|
+
|
216
|
+
# Get the effective model and schema to use
|
217
|
+
effective_model, effective_schema = self._get_effective_model(endpoint_type, model, schema)
|
218
|
+
|
219
|
+
# If no conversion needed (raw data requested), return as-is
|
220
|
+
if effective_model is Any or effective_model is None:
|
221
|
+
return raw_result
|
222
|
+
|
223
|
+
# Extract the raw data
|
224
|
+
raw_data = raw_result.unwrap()
|
225
|
+
|
226
|
+
# Handle DataFrames specially
|
227
|
+
if effective_model is pl.DataFrame:
|
228
|
+
return _create_dataframe_from_data(raw_data, items_key=items_key, empty_schema=effective_schema)
|
229
|
+
|
230
|
+
# Handle other model types using the same logic as PublicAPI
|
231
|
+
try:
|
232
|
+
# Handle different model types
|
233
|
+
if hasattr(effective_model, "model_validate"):
|
234
|
+
# Pydantic model
|
235
|
+
parsed = effective_model.model_validate(raw_data) # type: ignore[misc]
|
236
|
+
elif effective_schema is None:
|
237
|
+
# Simple constructor call - this handles dict and other simple types
|
238
|
+
parsed = effective_model(raw_data) # type: ignore[misc]
|
239
|
+
else:
|
240
|
+
# Other models with schema
|
241
|
+
parsed = effective_model(raw_data, schema=effective_schema) # type: ignore[misc]
|
242
|
+
|
243
|
+
return Success(parsed)
|
244
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
245
|
+
# If conversion fails, return a structured error
|
246
|
+
error = BitvavoError(
|
247
|
+
http_status=500,
|
248
|
+
error_code=-1,
|
249
|
+
reason="Model conversion failed",
|
250
|
+
message=str(exc),
|
251
|
+
raw=raw_data if isinstance(raw_data, dict) else {"raw": raw_data},
|
252
|
+
)
|
253
|
+
return Failure(error)
|
254
|
+
|
255
|
+
def account(
|
256
|
+
self,
|
257
|
+
*,
|
258
|
+
model: type[T] | Any | None = None,
|
259
|
+
schema: dict | None = None,
|
260
|
+
) -> Result[T, BitvavoError | httpx.HTTPError | TypeError]:
|
261
|
+
"""Get account information including fees and capabilities.
|
262
|
+
|
263
|
+
Endpoint: GET /v2/account
|
264
|
+
Rate limit weight: 1
|
265
|
+
|
266
|
+
Args:
|
267
|
+
model: Optional Pydantic model to validate response
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
Result containing account information including fees and capabilities:
|
271
|
+
{
|
272
|
+
"fees": {
|
273
|
+
"tier": 0,
|
274
|
+
"volume": "0.00",
|
275
|
+
"maker": "0.0015",
|
276
|
+
"taker": "0.0025"
|
277
|
+
},
|
278
|
+
"capabilities": [
|
279
|
+
"buy", "sell", "depositCrypto", "depositFiat",
|
280
|
+
"withdrawCrypto", "withdrawFiat"
|
281
|
+
]
|
282
|
+
}
|
283
|
+
"""
|
284
|
+
# Check if DataFrame is requested - not supported for this endpoint
|
285
|
+
effective_model, effective_schema = self._get_effective_model("account", model, schema)
|
286
|
+
if effective_model is pl.DataFrame:
|
287
|
+
msg = "DataFrame model is not supported due to the shape of data"
|
288
|
+
return Failure(TypeError(msg))
|
289
|
+
|
290
|
+
# Get raw data from API
|
291
|
+
raw_result = self.http.request("GET", "/account", weight=1)
|
292
|
+
# Convert to desired format
|
293
|
+
return self._convert_raw_result(raw_result, "account", model, schema)
|
294
|
+
|
295
|
+
def balance(
|
296
|
+
self,
|
297
|
+
options: AnyDict | None = None,
|
298
|
+
*,
|
299
|
+
model: type[T] | Any | None = None,
|
300
|
+
schema: dict | None = None,
|
301
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
302
|
+
"""Get account balance.
|
303
|
+
|
304
|
+
Args:
|
305
|
+
options: Optional query parameters
|
306
|
+
model: Optional Pydantic model to validate response
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
Result containing balance information or error
|
310
|
+
"""
|
311
|
+
postfix = create_postfix(options)
|
312
|
+
|
313
|
+
# Get raw data from API
|
314
|
+
raw_result = self.http.request("GET", f"/balance{postfix}", weight=5)
|
315
|
+
# Convert to desired format
|
316
|
+
return self._convert_raw_result(raw_result, "balance", model, schema)
|
317
|
+
|
318
|
+
def place_order(
|
319
|
+
self,
|
320
|
+
market: str,
|
321
|
+
side: str,
|
322
|
+
order_type: str,
|
323
|
+
operator_id: int,
|
324
|
+
body: AnyDict,
|
325
|
+
*,
|
326
|
+
response_required: bool = True,
|
327
|
+
model: type[T] | Any | None = None,
|
328
|
+
schema: dict | None = None,
|
329
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
330
|
+
"""Place a new order.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
market: Market symbol
|
334
|
+
side: Order side ('buy' or 'sell')
|
335
|
+
order_type: Order type ('market', 'limit', etc.)
|
336
|
+
operator_id: Your identifier for the trader or bot within your account
|
337
|
+
body: Order parameters (amount, price, etc.)
|
338
|
+
response_required: Whether to return full order details (True) or minimal response (False)
|
339
|
+
|
340
|
+
Returns:
|
341
|
+
Order placement result
|
342
|
+
"""
|
343
|
+
effective_model, effective_schema = self._get_effective_model("order", model, schema)
|
344
|
+
payload = {
|
345
|
+
"market": market,
|
346
|
+
"side": side,
|
347
|
+
"orderType": order_type,
|
348
|
+
"operatorId": operator_id,
|
349
|
+
"responseRequired": response_required,
|
350
|
+
**body,
|
351
|
+
}
|
352
|
+
return self.http.request("POST", "/order", body=payload, weight=1)
|
353
|
+
|
354
|
+
def get_order(
|
355
|
+
self,
|
356
|
+
market: str,
|
357
|
+
order_id: str,
|
358
|
+
*,
|
359
|
+
model: type[T] | Any | None = None,
|
360
|
+
schema: dict | None = None,
|
361
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
362
|
+
"""Get order by ID.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
market: Market symbol
|
366
|
+
order_id: Order ID
|
367
|
+
model: Optional Pydantic model to validate response
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
Result containing order information or error
|
371
|
+
"""
|
372
|
+
# Get raw data from API
|
373
|
+
raw_result = self.http.request(
|
374
|
+
"GET",
|
375
|
+
f"/{market}/order",
|
376
|
+
body={"orderId": order_id},
|
377
|
+
weight=1,
|
378
|
+
)
|
379
|
+
# Convert to desired format
|
380
|
+
return self._convert_raw_result(raw_result, "order", model, schema)
|
381
|
+
|
382
|
+
def update_order(
|
383
|
+
self,
|
384
|
+
market: str,
|
385
|
+
operator_id: int,
|
386
|
+
*,
|
387
|
+
order_id: str | None = None,
|
388
|
+
client_order_id: str | None = None,
|
389
|
+
amount: str | None = None,
|
390
|
+
amount_quote: str | None = None,
|
391
|
+
amount_remaining: str | None = None,
|
392
|
+
price: str | None = None,
|
393
|
+
trigger_amount: str | None = None,
|
394
|
+
time_in_force: str | None = None,
|
395
|
+
self_trade_prevention: str | None = None,
|
396
|
+
post_only: bool | None = None,
|
397
|
+
response_required: bool | None = None,
|
398
|
+
model: type[T] | Any | None = None,
|
399
|
+
schema: dict | None = None,
|
400
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
401
|
+
"""Update an existing limit or trigger order.
|
402
|
+
|
403
|
+
Args:
|
404
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
405
|
+
operator_id: Your identifier for the trader or bot within your account
|
406
|
+
order_id: Bitvavo identifier of the order to update (required if client_order_id not provided)
|
407
|
+
client_order_id: Your identifier of the order to update (required if order_id not provided)
|
408
|
+
amount: Amount of base currency to update
|
409
|
+
amount_quote: Amount of quote currency (market orders only)
|
410
|
+
amount_remaining: Remaining amount of base currency to update
|
411
|
+
price: Price for limit orders
|
412
|
+
trigger_amount: Trigger price for stop orders
|
413
|
+
time_in_force: Time in force ('GTC', 'IOC', 'FOK')
|
414
|
+
self_trade_prevention: Self-trade prevention
|
415
|
+
('decrementAndCancel', 'cancelOldest', 'cancelNewest', 'cancelBoth')
|
416
|
+
post_only: Whether order should only be maker
|
417
|
+
response_required: Whether to return full response or just status code
|
418
|
+
model: Optional Pydantic model to validate response
|
419
|
+
schema: Optional schema for DataFrame conversion
|
420
|
+
|
421
|
+
Returns:
|
422
|
+
Result containing updated order information or error
|
423
|
+
|
424
|
+
Note:
|
425
|
+
- You must set either order_id or client_order_id
|
426
|
+
- If both are set, client_order_id takes precedence
|
427
|
+
- Updates are faster than canceling and creating new orders
|
428
|
+
- Only works with limit or trigger orders
|
429
|
+
"""
|
430
|
+
if not order_id and not client_order_id:
|
431
|
+
error = BitvavoError(
|
432
|
+
http_status=400,
|
433
|
+
error_code=203,
|
434
|
+
reason="Missing or incompatible parameters in your request.",
|
435
|
+
message="Either order_id or client_order_id must be provided",
|
436
|
+
raw={"provided": {"order_id": order_id, "client_order_id": client_order_id}},
|
437
|
+
)
|
438
|
+
return Failure(error)
|
439
|
+
|
440
|
+
effective_model, effective_schema = self._get_effective_model("order", model, schema)
|
441
|
+
payload = self._build_update_order_payload(
|
442
|
+
market,
|
443
|
+
operator_id,
|
444
|
+
order_id,
|
445
|
+
client_order_id,
|
446
|
+
amount,
|
447
|
+
amount_quote,
|
448
|
+
amount_remaining,
|
449
|
+
price,
|
450
|
+
trigger_amount,
|
451
|
+
time_in_force,
|
452
|
+
self_trade_prevention,
|
453
|
+
post_only=post_only,
|
454
|
+
response_required=response_required,
|
455
|
+
)
|
456
|
+
|
457
|
+
return self.http.request("PUT", "/order", body=payload, weight=1)
|
458
|
+
|
459
|
+
def _build_update_order_payload(
|
460
|
+
self,
|
461
|
+
market: str,
|
462
|
+
operator_id: int,
|
463
|
+
order_id: str | None,
|
464
|
+
client_order_id: str | None,
|
465
|
+
amount: str | None,
|
466
|
+
amount_quote: str | None,
|
467
|
+
amount_remaining: str | None,
|
468
|
+
price: str | None,
|
469
|
+
trigger_amount: str | None,
|
470
|
+
time_in_force: str | None,
|
471
|
+
self_trade_prevention: str | None,
|
472
|
+
*,
|
473
|
+
post_only: bool | None,
|
474
|
+
response_required: bool | None,
|
475
|
+
) -> dict[str, Any]:
|
476
|
+
"""Build the payload for update order request."""
|
477
|
+
payload = {
|
478
|
+
"market": market,
|
479
|
+
"operatorId": operator_id,
|
480
|
+
}
|
481
|
+
|
482
|
+
# Add order identifier - clientOrderId takes precedence if both provided
|
483
|
+
if client_order_id:
|
484
|
+
payload["clientOrderId"] = client_order_id
|
485
|
+
elif order_id:
|
486
|
+
payload["orderId"] = order_id
|
487
|
+
|
488
|
+
# Add optional update parameters
|
489
|
+
payload.update(
|
490
|
+
{
|
491
|
+
key: value
|
492
|
+
for key, value in {
|
493
|
+
"amount": amount,
|
494
|
+
"amountQuote": amount_quote,
|
495
|
+
"amountRemaining": amount_remaining,
|
496
|
+
"price": price,
|
497
|
+
"triggerAmount": trigger_amount,
|
498
|
+
"timeInForce": time_in_force,
|
499
|
+
"selfTradePrevention": self_trade_prevention,
|
500
|
+
"postOnly": post_only,
|
501
|
+
"responseRequired": response_required,
|
502
|
+
}.items()
|
503
|
+
if value is not None
|
504
|
+
}
|
505
|
+
)
|
506
|
+
|
507
|
+
return payload
|
508
|
+
|
509
|
+
def cancel_order(
|
510
|
+
self,
|
511
|
+
market: str,
|
512
|
+
operator_id: int,
|
513
|
+
*,
|
514
|
+
order_id: str | None = None,
|
515
|
+
client_order_id: str | None = None,
|
516
|
+
model: type[T] | Any | None = None,
|
517
|
+
schema: dict | None = None,
|
518
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
519
|
+
"""Cancel an order.
|
520
|
+
|
521
|
+
Args:
|
522
|
+
market: Market symbol
|
523
|
+
operator_id: Your identifier for the trader or bot within your account
|
524
|
+
order_id: Bitvavo identifier of the order (required if client_order_id not provided)
|
525
|
+
client_order_id: Your identifier of the order (required if order_id not provided)
|
526
|
+
model: Optional Pydantic model to validate response
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
Result containing cancellation result or error
|
530
|
+
|
531
|
+
Note:
|
532
|
+
You must set either order_id or client_order_id. If you set both,
|
533
|
+
client_order_id takes precedence as per Bitvavo documentation.
|
534
|
+
"""
|
535
|
+
if not order_id and not client_order_id:
|
536
|
+
# Create a validation error using httpx.HTTPError as a fallback
|
537
|
+
error = httpx.RequestError("Either order_id or client_order_id must be provided")
|
538
|
+
return Failure(error)
|
539
|
+
|
540
|
+
# Build query parameters
|
541
|
+
params = {
|
542
|
+
"market": market,
|
543
|
+
"operatorId": operator_id,
|
544
|
+
}
|
545
|
+
|
546
|
+
if client_order_id:
|
547
|
+
params["clientOrderId"] = client_order_id
|
548
|
+
elif order_id:
|
549
|
+
params["orderId"] = order_id
|
550
|
+
|
551
|
+
# Create query string
|
552
|
+
postfix = create_postfix(params)
|
553
|
+
|
554
|
+
effective_model, effective_schema = self._get_effective_model("cancel_order", model, schema)
|
555
|
+
return self.http.request(
|
556
|
+
"DELETE",
|
557
|
+
f"/order{postfix}",
|
558
|
+
weight=1,
|
559
|
+
)
|
560
|
+
|
561
|
+
def get_orders(
|
562
|
+
self,
|
563
|
+
market: str,
|
564
|
+
options: AnyDict | None = None,
|
565
|
+
*,
|
566
|
+
model: type[T] | Any | None = None,
|
567
|
+
schema: dict | None = None,
|
568
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
569
|
+
"""Get orders for a market.
|
570
|
+
|
571
|
+
Args:
|
572
|
+
market: Market symbol (e.g., 'BTC-EUR')
|
573
|
+
options: Optional query parameters:
|
574
|
+
- limit (int): Max number of orders (1-1000, default: 500)
|
575
|
+
- start (int): Unix timestamp in ms to start from
|
576
|
+
- end (int): Unix timestamp in ms to end at (max: 8640000000000000)
|
577
|
+
- orderIdFrom (str): UUID to start from
|
578
|
+
- orderIdTo (str): UUID to end at
|
579
|
+
model: Optional Pydantic model to validate response
|
580
|
+
schema: Optional schema for DataFrame conversion
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
Result containing list of orders or error
|
584
|
+
|
585
|
+
Rate limit weight: 5
|
586
|
+
"""
|
587
|
+
# Constants for validation
|
588
|
+
MIN_LIMIT = 1
|
589
|
+
MAX_LIMIT = 1000
|
590
|
+
MAX_TIMESTAMP = 8640000000000000
|
591
|
+
|
592
|
+
# Validate options if provided
|
593
|
+
if options:
|
594
|
+
# Validate limit parameter
|
595
|
+
if "limit" in options:
|
596
|
+
limit = options["limit"]
|
597
|
+
if not isinstance(limit, int) or limit < MIN_LIMIT or limit > MAX_LIMIT:
|
598
|
+
msg = f"Invalid limit '{limit}'. Must be an integer between {MIN_LIMIT} and {MAX_LIMIT}"
|
599
|
+
raise ValueError(msg)
|
600
|
+
|
601
|
+
# Validate end timestamp
|
602
|
+
if "end" in options:
|
603
|
+
end = options["end"]
|
604
|
+
if not isinstance(end, int) or end > MAX_TIMESTAMP:
|
605
|
+
msg = f"Invalid end timestamp '{end}'. Must be <= {MAX_TIMESTAMP}"
|
606
|
+
raise ValueError(msg)
|
607
|
+
|
608
|
+
# Validate start/end relationship
|
609
|
+
if "start" in options and "end" in options and options["start"] > options["end"]:
|
610
|
+
msg = f"Start timestamp ({options['start']}) cannot be greater than end timestamp ({options['end']})"
|
611
|
+
raise ValueError(msg)
|
612
|
+
|
613
|
+
effective_model, effective_schema = self._get_effective_model("orders", model, schema)
|
614
|
+
options = default(options, {})
|
615
|
+
options["market"] = market
|
616
|
+
postfix = create_postfix(options)
|
617
|
+
|
618
|
+
# Get raw data first
|
619
|
+
raw_result = self.http.request("GET", f"/orders{postfix}", weight=5)
|
620
|
+
|
621
|
+
# Convert using the shared method
|
622
|
+
return self._convert_raw_result(raw_result, "orders", effective_model, effective_schema)
|
623
|
+
|
624
|
+
def cancel_orders(
|
625
|
+
self,
|
626
|
+
operator_id: int,
|
627
|
+
*,
|
628
|
+
market: str | None = None,
|
629
|
+
model: type[T] | Any | None = None,
|
630
|
+
schema: dict | None = None,
|
631
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
632
|
+
"""Cancel all orders for a market or the entire account.
|
633
|
+
|
634
|
+
Args:
|
635
|
+
operator_id: Your identifier for the trader or bot within your account
|
636
|
+
market: Optional market symbol. If not specified, all open orders are canceled
|
637
|
+
model: Optional Pydantic model to validate response
|
638
|
+
|
639
|
+
Returns:
|
640
|
+
Result containing array of cancellation results or error
|
641
|
+
|
642
|
+
Example Response:
|
643
|
+
[
|
644
|
+
{
|
645
|
+
"orderId": "1be6d0df-d5dc-4b53-a250-3376f3b393e6",
|
646
|
+
"operatorId": 543462
|
647
|
+
}
|
648
|
+
]
|
649
|
+
"""
|
650
|
+
effective_model, effective_schema = self._get_effective_model("orders", model, schema)
|
651
|
+
|
652
|
+
# Build query parameters
|
653
|
+
params: dict[str, Any] = {"operatorId": operator_id}
|
654
|
+
if market is not None:
|
655
|
+
params["market"] = market
|
656
|
+
|
657
|
+
postfix = create_postfix(params)
|
658
|
+
return self.http.request("DELETE", f"/orders{postfix}", weight=1)
|
659
|
+
|
660
|
+
def orders_open(
|
661
|
+
self,
|
662
|
+
options: AnyDict | None = None,
|
663
|
+
*,
|
664
|
+
model: type[T] | Any | None = None,
|
665
|
+
schema: dict | None = None,
|
666
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
667
|
+
"""Get all open orders.
|
668
|
+
|
669
|
+
Args:
|
670
|
+
options: Optional query parameters. Supports:
|
671
|
+
- market (str): Filter by specific market (e.g., 'BTC-EUR')
|
672
|
+
- base (str): Filter by base asset (e.g., 'BTC')
|
673
|
+
model: Optional Pydantic model to validate response
|
674
|
+
|
675
|
+
Returns:
|
676
|
+
Result containing open orders data or error
|
677
|
+
|
678
|
+
Rate limit: 25 points (without market), 1 point (with market)
|
679
|
+
"""
|
680
|
+
effective_model, effective_schema = self._get_effective_model("orders", model, schema)
|
681
|
+
postfix = create_postfix(options)
|
682
|
+
|
683
|
+
# Rate limit is 1 point with market parameter, 25 points without
|
684
|
+
weight = 1 if options and "market" in options else 25
|
685
|
+
|
686
|
+
# Get raw data first
|
687
|
+
raw_result = self.http.request("GET", f"/ordersOpen{postfix}", weight=weight)
|
688
|
+
|
689
|
+
# Convert using the shared method
|
690
|
+
return self._convert_raw_result(raw_result, "orders", effective_model, effective_schema)
|
691
|
+
|
692
|
+
def fees(
|
693
|
+
self,
|
694
|
+
options: AnyDict | None = None,
|
695
|
+
*,
|
696
|
+
model: type[T] | Any | None = None,
|
697
|
+
schema: dict | None = None,
|
698
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
699
|
+
"""Get market-specific trading fees.
|
700
|
+
|
701
|
+
Returns your current trading fees from the Category of the specified market
|
702
|
+
based on the trading volume of your account. This is different from the
|
703
|
+
account() method which returns general account information with fees wrapped
|
704
|
+
in a "fees" object plus capabilities.
|
705
|
+
|
706
|
+
Endpoint: GET /v2/account/fees
|
707
|
+
Rate limit weight: 1
|
708
|
+
|
709
|
+
Args:
|
710
|
+
options: Optional query parameters:
|
711
|
+
- market (str): Market symbol (e.g., 'BTC-EUR'). If not specified,
|
712
|
+
returns fees for your current tier in Category B.
|
713
|
+
- quote (str): Quote currency ('EUR' or 'USDC'). If not specified,
|
714
|
+
returns fees for your current tier in Category B.
|
715
|
+
model: Optional Pydantic model to validate response
|
716
|
+
|
717
|
+
Returns:
|
718
|
+
Result containing market fee information directly (no wrapper, includes tier):
|
719
|
+
{
|
720
|
+
"tier": "0",
|
721
|
+
"volume": "10000.00",
|
722
|
+
"taker": "0.0025",
|
723
|
+
"maker": "0.0015"
|
724
|
+
}
|
725
|
+
|
726
|
+
Note:
|
727
|
+
This differs from account() which returns:
|
728
|
+
{
|
729
|
+
"fees": {"tier": "0", "volume": "...", "maker": "...", "taker": "..."},
|
730
|
+
"capabilities": [...]
|
731
|
+
}
|
732
|
+
"""
|
733
|
+
# Validate quote parameter if provided
|
734
|
+
if options and "quote" in options:
|
735
|
+
quote = options["quote"]
|
736
|
+
valid_quotes = ["EUR", "USDC"]
|
737
|
+
if quote not in valid_quotes:
|
738
|
+
msg = f"Invalid quote currency '{quote}'. Must be one of: {valid_quotes}"
|
739
|
+
raise ValueError(msg)
|
740
|
+
|
741
|
+
effective_model, effective_schema = self._get_effective_model("fees", model, schema)
|
742
|
+
postfix = create_postfix(options)
|
743
|
+
|
744
|
+
# Get raw data first
|
745
|
+
raw_result = self.http.request("GET", f"/account/fees{postfix}", weight=1)
|
746
|
+
|
747
|
+
# Convert using the shared method
|
748
|
+
return self._convert_raw_result(raw_result, "fees", effective_model, effective_schema)
|
749
|
+
|
750
|
+
def deposit(
|
751
|
+
self,
|
752
|
+
symbol: str,
|
753
|
+
*,
|
754
|
+
model: type[T] | Any | None = None,
|
755
|
+
schema: dict | None = None,
|
756
|
+
) -> Result[T, BitvavoError | httpx.HTTPError | TypeError]:
|
757
|
+
"""Get deposit data for making deposits.
|
758
|
+
|
759
|
+
Returns wallet or bank account information required to deposit digital or fiat assets.
|
760
|
+
|
761
|
+
Endpoint: GET /v2/deposit
|
762
|
+
Rate limit weight: 1
|
763
|
+
|
764
|
+
Args:
|
765
|
+
symbol: The asset symbol you want to deposit (e.g., 'BTC', 'EUR')
|
766
|
+
model: Optional Pydantic model to validate response
|
767
|
+
schema: Optional schema for DataFrame conversion
|
768
|
+
|
769
|
+
Returns:
|
770
|
+
Result containing deposit information:
|
771
|
+
- For digital assets: {"address": "string", "paymentid": "string"}
|
772
|
+
- For fiat: {"iban": "string", "bic": "string", "description": "string"}
|
773
|
+
"""
|
774
|
+
|
775
|
+
effective_model, effective_schema = self._get_effective_model("deposit", model, schema)
|
776
|
+
|
777
|
+
if effective_model is pl.DataFrame:
|
778
|
+
msg = "DataFrame model is not supported due to the shape of data"
|
779
|
+
return Failure(TypeError(msg))
|
780
|
+
|
781
|
+
params = {"symbol": symbol}
|
782
|
+
postfix = create_postfix(params)
|
783
|
+
|
784
|
+
# Get raw data first
|
785
|
+
raw_result = self.http.request(
|
786
|
+
"GET",
|
787
|
+
f"/deposit{postfix}",
|
788
|
+
weight=1,
|
789
|
+
)
|
790
|
+
|
791
|
+
# Convert using the shared method
|
792
|
+
return self._convert_raw_result(raw_result, "deposit", effective_model, effective_schema)
|
793
|
+
|
794
|
+
def deposit_history(
|
795
|
+
self,
|
796
|
+
options: AnyDict | None = None,
|
797
|
+
*,
|
798
|
+
model: type[T] | Any | None = None,
|
799
|
+
schema: dict | None = None,
|
800
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
801
|
+
"""Get deposit history.
|
802
|
+
|
803
|
+
Ensures every deposit dict includes a non-empty "address" key (fallback to txId or "unknown").
|
804
|
+
|
805
|
+
Args:
|
806
|
+
options: Optional query parameters
|
807
|
+
model: Optional Pydantic model to validate response
|
808
|
+
|
809
|
+
Returns:
|
810
|
+
Result containing deposits data or error
|
811
|
+
"""
|
812
|
+
effective_model, effective_schema = self._get_effective_model("deposit_history", model, schema)
|
813
|
+
postfix = create_postfix(options)
|
814
|
+
|
815
|
+
# Get raw data first
|
816
|
+
raw_result = self.http.request("GET", f"/depositHistory{postfix}", weight=5)
|
817
|
+
|
818
|
+
# Convert using the shared method
|
819
|
+
return self._convert_raw_result(raw_result, "deposit_history", effective_model, effective_schema)
|
820
|
+
|
821
|
+
def withdrawals(
|
822
|
+
self,
|
823
|
+
options: AnyDict | None = None,
|
824
|
+
*,
|
825
|
+
model: type[T] | Any | None = None,
|
826
|
+
schema: dict | None = None,
|
827
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
828
|
+
"""Get withdrawal history.
|
829
|
+
|
830
|
+
Args:
|
831
|
+
options: Optional query parameters
|
832
|
+
model: Optional Pydantic model to validate response
|
833
|
+
|
834
|
+
Returns:
|
835
|
+
Result containing withdrawals data or error
|
836
|
+
"""
|
837
|
+
effective_model, effective_schema = self._get_effective_model("withdrawals", model, schema)
|
838
|
+
postfix = create_postfix(options)
|
839
|
+
|
840
|
+
# Get raw data first
|
841
|
+
raw_result = self.http.request("GET", f"/withdrawalHistory{postfix}", weight=1)
|
842
|
+
|
843
|
+
# Convert using the shared method
|
844
|
+
return self._convert_raw_result(raw_result, "withdrawals", effective_model, effective_schema)
|
845
|
+
|
846
|
+
def _convert_transaction_items(
|
847
|
+
self,
|
848
|
+
items_data: list[dict],
|
849
|
+
effective_model: type[T] | Any | None,
|
850
|
+
effective_schema: dict | None,
|
851
|
+
) -> Result[Any, BitvavoError]:
|
852
|
+
"""Convert transaction items to the desired model format."""
|
853
|
+
if effective_model is pl.DataFrame:
|
854
|
+
# Convert items to DataFrame
|
855
|
+
return _create_dataframe_from_data(items_data, items_key=None, empty_schema=effective_schema)
|
856
|
+
|
857
|
+
if effective_model is Any or effective_model is None:
|
858
|
+
# Raw data - return items list directly
|
859
|
+
return Success(items_data)
|
860
|
+
|
861
|
+
# Handle Pydantic or other model types
|
862
|
+
try:
|
863
|
+
if hasattr(effective_model, "model_validate"):
|
864
|
+
# Pydantic model - validate items list
|
865
|
+
parsed_items = effective_model.model_validate(items_data) # type: ignore[misc]
|
866
|
+
elif effective_schema is None:
|
867
|
+
# Simple constructor call
|
868
|
+
parsed_items = effective_model(items_data) # type: ignore[misc]
|
869
|
+
else:
|
870
|
+
# Other models with schema
|
871
|
+
parsed_items = effective_model(items_data, schema=effective_schema) # type: ignore[misc]
|
872
|
+
|
873
|
+
return Success(parsed_items)
|
874
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
875
|
+
# If conversion fails, return a structured error
|
876
|
+
error = BitvavoError(
|
877
|
+
http_status=500,
|
878
|
+
error_code=-1,
|
879
|
+
reason="Model conversion failed",
|
880
|
+
message=str(exc),
|
881
|
+
raw=items_data if isinstance(items_data, dict) else {"raw": items_data},
|
882
|
+
)
|
883
|
+
return Failure(error)
|
884
|
+
|
885
|
+
def transaction_history(
|
886
|
+
self,
|
887
|
+
options: AnyDict | None = None,
|
888
|
+
*,
|
889
|
+
model: type[T] | Any | None = None,
|
890
|
+
schema: dict | None = None,
|
891
|
+
) -> Result[tuple[Any, dict[str, Any]], BitvavoError | httpx.HTTPError]:
|
892
|
+
"""Get account transaction history.
|
893
|
+
|
894
|
+
Returns all past transactions for your account with pagination support.
|
895
|
+
|
896
|
+
Endpoint: GET /v2/account/history
|
897
|
+
Rate limit weight: 1
|
898
|
+
|
899
|
+
Args:
|
900
|
+
options: Optional query parameters:
|
901
|
+
- fromDate (int): Unix timestamp in ms to start from (>=0)
|
902
|
+
- toDate (int): Unix timestamp in ms to end at (<=8640000000000000)
|
903
|
+
- page (int): Page number for pagination (>=1)
|
904
|
+
- maxItems (int): Max number of items per page (1-100, default: 100)
|
905
|
+
- type (str): Transaction type filter:
|
906
|
+
'sell', 'buy', 'staking', 'fixed_staking', 'deposit', 'withdrawal',
|
907
|
+
'affiliate', 'distribution', 'internal_transfer', 'withdrawal_cancelled',
|
908
|
+
'rebate', 'loan', 'external_transferred_funds', 'manually_assigned_bitvavo'
|
909
|
+
model: Optional Pydantic model to validate response
|
910
|
+
schema: Optional schema for DataFrame conversion
|
911
|
+
|
912
|
+
Returns:
|
913
|
+
Result containing transaction history as tuple of (items_converted, metadata_dict):
|
914
|
+
- For DataFrames: Returns tuple of (DataFrame of items, metadata dict)
|
915
|
+
- For Pydantic models: Returns tuple of (Pydantic model of items, metadata dict)
|
916
|
+
- For raw responses: Returns tuple of (list of items, metadata dict)
|
917
|
+
|
918
|
+
Each transaction contains:
|
919
|
+
- transactionId: Unique transaction identifier
|
920
|
+
- executedAt: Execution timestamp (ISO format)
|
921
|
+
- type: Transaction type
|
922
|
+
- priceCurrency/priceAmount: Transaction price info (optional for staking)
|
923
|
+
- sentCurrency/sentAmount: Sent amounts (optional for staking)
|
924
|
+
- receivedCurrency/receivedAmount: Received amounts
|
925
|
+
- feesCurrency/feesAmount: Fee information (optional for staking)
|
926
|
+
- address: Transaction address (nullable)
|
927
|
+
"""
|
928
|
+
postfix = create_postfix(options)
|
929
|
+
|
930
|
+
# Get raw data first
|
931
|
+
raw_result = self.http.request("GET", f"/account/history{postfix}", weight=1)
|
932
|
+
|
933
|
+
# Always split the response into items and metadata for all model types
|
934
|
+
match raw_result:
|
935
|
+
case Success(raw_data):
|
936
|
+
# Validate response structure
|
937
|
+
if not isinstance(raw_data, dict) or "items" not in raw_data:
|
938
|
+
error = BitvavoError(
|
939
|
+
http_status=500,
|
940
|
+
error_code=-1,
|
941
|
+
reason="Response parsing failed",
|
942
|
+
message="Expected response to have 'items' key for transaction history",
|
943
|
+
raw=raw_data if isinstance(raw_data, dict) else {"raw": raw_data},
|
944
|
+
)
|
945
|
+
return Failure(error)
|
946
|
+
|
947
|
+
# Extract items and metadata separately
|
948
|
+
items_data = raw_data["items"]
|
949
|
+
metadata = {k: v for k, v in raw_data.items() if k != "items"}
|
950
|
+
|
951
|
+
effective_model, effective_schema = self._get_effective_model("transaction_history", model, schema)
|
952
|
+
# Convert items using helper method
|
953
|
+
items_result = self._convert_transaction_items(items_data, effective_model, effective_schema)
|
954
|
+
match items_result:
|
955
|
+
case Success(converted_items):
|
956
|
+
return Success((converted_items, metadata))
|
957
|
+
case Failure(error):
|
958
|
+
return Failure(error)
|
959
|
+
case _:
|
960
|
+
# This case should never be reached, but satisfies type checker
|
961
|
+
msg = "Unexpected result type from _convert_transaction_items"
|
962
|
+
raise RuntimeError(msg)
|
963
|
+
|
964
|
+
case Failure(error):
|
965
|
+
return Failure(error)
|
966
|
+
case _:
|
967
|
+
# This case should never be reached, but satisfies type checker
|
968
|
+
msg = "Unexpected result type from HTTP request"
|
969
|
+
raise RuntimeError(msg)
|
970
|
+
|
971
|
+
def trade_history(
|
972
|
+
self,
|
973
|
+
market: str,
|
974
|
+
options: AnyDict | None = None,
|
975
|
+
*,
|
976
|
+
model: type[T] | Any | None = None,
|
977
|
+
schema: dict | None = None,
|
978
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
979
|
+
"""Get trade history for your account.
|
980
|
+
|
981
|
+
Returns the specified number of past trades for your account, excluding Price Guarantee.
|
982
|
+
The returned trades are sorted by their timestamp in descending order, from latest to earliest.
|
983
|
+
|
984
|
+
Endpoint: GET /v2/trades
|
985
|
+
Rate limit weight: 5
|
986
|
+
|
987
|
+
Args:
|
988
|
+
market: The market for which to return the past trades (e.g., 'BTC-EUR')
|
989
|
+
options: Optional query parameters:
|
990
|
+
- limit (int): Max number of trades (1-1000, default: 500)
|
991
|
+
- start (int): Unix timestamp in ms to start from
|
992
|
+
- end (int): Unix timestamp in ms to end at (max: 8640000000000000)
|
993
|
+
- tradeIdFrom (str): Trade ID to start from
|
994
|
+
- tradeIdTo (str): Trade ID to end at
|
995
|
+
- tradeId (str, deprecated): Interpreted as tradeIdTo
|
996
|
+
model: Optional Pydantic model to validate response
|
997
|
+
schema: Optional schema for DataFrame conversion
|
998
|
+
|
999
|
+
Returns:
|
1000
|
+
Result containing list of trade objects with fields like:
|
1001
|
+
- id: Trade identifier
|
1002
|
+
- orderId: Bitvavo order ID
|
1003
|
+
- clientOrderId: Your order ID
|
1004
|
+
- timestamp: Unix timestamp in ms
|
1005
|
+
- market: Market symbol
|
1006
|
+
- side: 'buy' or 'sell'
|
1007
|
+
- amount: Base currency amount
|
1008
|
+
- price: Price per unit
|
1009
|
+
- taker: Whether you were the taker
|
1010
|
+
- fee: Fee paid (negative for rebates)
|
1011
|
+
- feeCurrency: Currency of the fee
|
1012
|
+
- settled: Whether fee was deducted
|
1013
|
+
|
1014
|
+
Note:
|
1015
|
+
This is a private endpoint that returns YOUR trades, different from the public
|
1016
|
+
trades endpoint which returns public market trades.
|
1017
|
+
"""
|
1018
|
+
# Constants for validation
|
1019
|
+
MIN_LIMIT = 1
|
1020
|
+
MAX_LIMIT = 1000
|
1021
|
+
MAX_TIMESTAMP = 8640000000000000
|
1022
|
+
|
1023
|
+
# Validate options if provided
|
1024
|
+
if options:
|
1025
|
+
# Validate limit parameter
|
1026
|
+
if "limit" in options:
|
1027
|
+
limit = options["limit"]
|
1028
|
+
if not isinstance(limit, int) or limit < MIN_LIMIT or limit > MAX_LIMIT:
|
1029
|
+
msg = f"Invalid limit '{limit}'. Must be an integer between {MIN_LIMIT} and {MAX_LIMIT}"
|
1030
|
+
raise ValueError(msg)
|
1031
|
+
|
1032
|
+
# Validate end timestamp
|
1033
|
+
if "end" in options:
|
1034
|
+
end = options["end"]
|
1035
|
+
if not isinstance(end, int) or end > MAX_TIMESTAMP:
|
1036
|
+
msg = f"Invalid end timestamp '{end}'. Must be <= {MAX_TIMESTAMP}"
|
1037
|
+
raise ValueError(msg)
|
1038
|
+
|
1039
|
+
# Validate start/end relationship
|
1040
|
+
if "start" in options and "end" in options and options["start"] > options["end"]:
|
1041
|
+
msg = f"Start timestamp ({options['start']}) cannot be greater than end timestamp ({options['end']})"
|
1042
|
+
raise ValueError(msg)
|
1043
|
+
|
1044
|
+
# Handle deprecated tradeId parameter
|
1045
|
+
if "tradeId" in options and "tradeIdTo" not in options:
|
1046
|
+
# Move deprecated tradeId to tradeIdTo as per documentation
|
1047
|
+
options = options.copy() # Don't mutate the original
|
1048
|
+
options["tradeIdTo"] = options.pop("tradeId")
|
1049
|
+
|
1050
|
+
effective_model, effective_schema = self._get_effective_model("trade_history", model, schema)
|
1051
|
+
|
1052
|
+
# Add market to options
|
1053
|
+
query_options = default(options, {})
|
1054
|
+
query_options["market"] = market
|
1055
|
+
|
1056
|
+
postfix = create_postfix(query_options)
|
1057
|
+
|
1058
|
+
# Get raw data first
|
1059
|
+
raw_result = self.http.request("GET", f"/trades{postfix}", weight=5)
|
1060
|
+
|
1061
|
+
# Convert using the shared method
|
1062
|
+
return self._convert_raw_result(raw_result, "trade_history", effective_model, effective_schema)
|
1063
|
+
|
1064
|
+
def withdraw(
|
1065
|
+
self,
|
1066
|
+
symbol: str,
|
1067
|
+
amount: str,
|
1068
|
+
address: str,
|
1069
|
+
options: AnyDict | None = None,
|
1070
|
+
*,
|
1071
|
+
model: type[T] | Any | None = None,
|
1072
|
+
schema: dict | None = None,
|
1073
|
+
) -> Result[T, BitvavoError | httpx.HTTPError]:
|
1074
|
+
"""Withdraw assets.
|
1075
|
+
|
1076
|
+
Args:
|
1077
|
+
symbol: Asset symbol
|
1078
|
+
amount: Amount to withdraw
|
1079
|
+
address: Withdrawal address
|
1080
|
+
options: Optional parameters
|
1081
|
+
model: Optional Pydantic model to validate response
|
1082
|
+
|
1083
|
+
Returns:
|
1084
|
+
Result containing withdrawal result or error
|
1085
|
+
"""
|
1086
|
+
effective_model, effective_schema = self._get_effective_model("withdraw", model, schema)
|
1087
|
+
body = {"symbol": symbol, "amount": amount, "address": address}
|
1088
|
+
if options:
|
1089
|
+
body.update(options)
|
1090
|
+
return self.http.request("POST", "/withdrawal", body=body, weight=1)
|