revolut-x-python 1.0.0__tar.gz → 1.0.1__tar.gz
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.
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/.agents/skills/revolut-x-library/SKILL.md +13 -9
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/.agents/skills/revolut-x-library/references/types.md +3 -1
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/CHANGELOG.md +24 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/PKG-INFO +1 -15
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/pyproject.toml +0 -16
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/__init__.py +4 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/_version.py +1 -1
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/helpers.py +92 -1
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/market.py +55 -33
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/types.py +16 -1
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/PKG-INFO +1 -15
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/SOURCES.txt +0 -1
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/test_helpers.py +72 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/test_market.py +69 -0
- revolut_x_python-1.0.0/MANIFEST.in +0 -18
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/LICENSE +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/README.md +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/01_public_market_data.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/02_download_candles_stream.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/03_account_balances_and_history.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/04_place_and_cancel_orders.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/05_fee_calculator_and_validation.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/06_smart_maker_orders.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/README.md +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/requirements.txt +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/setup.cfg +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/_auth.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/_http.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/account.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/client.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/exceptions.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/orders.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x/py.typed +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/dependency_links.txt +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/requires.txt +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/top_level.txt +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/__init__.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/test_auth.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/test_maker_strategy.py +0 -0
- {revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/tests/test_orders.py +0 -0
|
@@ -136,10 +136,11 @@ book = client.get_order_book("BTC-EUR") # → OrderBook
|
|
|
136
136
|
candles = client.get_candles("BTC-EUR", Interval.HOUR_1)
|
|
137
137
|
|
|
138
138
|
# Public trades — single page + cursor.
|
|
139
|
-
# start_date / end_date
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
# start_date / end_date accept a datetime, a date, an ISO-8601 string, or a Unix
|
|
140
|
+
# timestamp in MILLISECONDS. Numbers are never rescaled, so a seconds timestamp
|
|
141
|
+
# silently means 1970 — prefer a datetime when you are not sure.
|
|
142
|
+
from datetime import datetime, timedelta, timezone
|
|
143
|
+
day_ago = datetime.now(timezone.utc) - timedelta(days=1)
|
|
143
144
|
trades, next_cursor = client.get_trades("BTC-EUR", start_date=day_ago, limit=100)
|
|
144
145
|
|
|
145
146
|
# Auto-paginating trade iterator
|
|
@@ -170,7 +171,10 @@ Each candle is a dict — **not** a list or tuple:
|
|
|
170
171
|
- `start` — candle **open** time, `int` Unix **milliseconds**, always **UTC**
|
|
171
172
|
- `open` / `high` / `low` / `close` / `volume` — **strings**; `volume` is in the
|
|
172
173
|
base currency. Wrap in `Decimal`, never `float`.
|
|
173
|
-
- `since` / `until` are
|
|
174
|
+
- `since` / `until` are inclusive on both ends and accept a `datetime`, a `date`,
|
|
175
|
+
an ISO-8601 string, or an `int` of Unix **milliseconds**. Naive `datetime`s and
|
|
176
|
+
strings are read as **UTC**; raw numbers are taken as milliseconds as-is
|
|
177
|
+
(`revolut_x.to_timestamp_ms` does the conversion if you need it yourself).
|
|
174
178
|
|
|
175
179
|
Three things that make naive candle code wrong:
|
|
176
180
|
|
|
@@ -194,7 +198,7 @@ regardless of who runs it.
|
|
|
194
198
|
|
|
195
199
|
```python
|
|
196
200
|
import time
|
|
197
|
-
from datetime import datetime, timezone, time as dtime
|
|
201
|
+
from datetime import datetime, timedelta, timezone, time as dtime
|
|
198
202
|
from decimal import Decimal
|
|
199
203
|
from revolut_x import RevolutXClient, Interval
|
|
200
204
|
|
|
@@ -205,10 +209,10 @@ interval = Interval.HOUR_1
|
|
|
205
209
|
# otherwise the row set shifts with the machine's local timezone.
|
|
206
210
|
day = datetime.now(timezone.utc).date() # or date(2026, 10, 10)
|
|
207
211
|
midnight = datetime.combine(day, dtime.min, tzinfo=timezone.utc)
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
until = min(midnight + timedelta(days=1) - timedelta(milliseconds=1),
|
|
213
|
+
datetime.now(timezone.utc))
|
|
210
214
|
|
|
211
|
-
candles = client.get_candles("BTC-EUR", interval, since=
|
|
215
|
+
candles = client.get_candles("BTC-EUR", interval, since=midnight, until=until)
|
|
212
216
|
|
|
213
217
|
interval_ms = int(interval) * 60_000
|
|
214
218
|
now_ms = int(time.time() * 1000)
|
|
@@ -39,7 +39,9 @@ class Interval(int, Enum):
|
|
|
39
39
|
## TypedDicts (API Responses)
|
|
40
40
|
|
|
41
41
|
> All numeric values are **strings** (not floats) to preserve decimal precision.
|
|
42
|
-
> Timestamps are `int` (Unix milliseconds).
|
|
42
|
+
> Timestamps in responses are `int` (Unix milliseconds). On the way *in*, time
|
|
43
|
+
> range arguments are typed `TimeLike` — a `datetime`, a `date`, an ISO-8601
|
|
44
|
+
> string, or an `int` of milliseconds.
|
|
43
45
|
|
|
44
46
|
### Candle
|
|
45
47
|
|
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`datetime` support for time ranges**: `since` / `until` on `get_candles()`
|
|
14
|
+
and `iter_candles()`, and `start_date` / `end_date` on `get_trades()` and
|
|
15
|
+
`iter_trades()`, now accept a `datetime`, a `date`, an ISO-8601 string
|
|
16
|
+
(trailing `Z` included), a digits-only string of 12+ digits (milliseconds, for
|
|
17
|
+
values arriving from JSON or query parameters) or the millisecond `int` they
|
|
18
|
+
always took. A shorter run of digits — `'2024'`, `'20240101'` — raises
|
|
19
|
+
`ValueError` instead of silently resolving to 1970 or to a date, because it is
|
|
20
|
+
ambiguous between the two; note this also makes `'20240101'` behave the same
|
|
21
|
+
on every supported interpreter, where `datetime.fromisoformat` accepted it as
|
|
22
|
+
a basic-format date on 3.11+ but rejected it on 3.10. Naive
|
|
23
|
+
`datetime`s and strings are interpreted as **UTC** rather than the machine's
|
|
24
|
+
local timezone, so the same code selects the same window everywhere. Raw
|
|
25
|
+
numbers are still passed through untouched — seconds are deliberately *not*
|
|
26
|
+
auto-detected, because inferring the unit from the magnitude would silently
|
|
27
|
+
change the meaning of the caller's input.
|
|
28
|
+
- **`to_timestamp_ms()`** (exported from `revolut_x`, together with the
|
|
29
|
+
`TimeLike` type alias) performs that conversion and is reusable in
|
|
30
|
+
application code.
|
|
31
|
+
|
|
32
|
+
Existing millisecond integers keep working unchanged.
|
|
33
|
+
|
|
10
34
|
## [1.0.0] - 2026-10-11
|
|
11
35
|
|
|
12
36
|
First release published to **PyPI**. Versions `0.0.1` – `0.1.0` were only ever
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revolut-x-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python SDK for the Revolut X Crypto Exchange REST API (v1.0)
|
|
5
5
|
Author: Jan Přikryl
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,20 +10,6 @@ Project-URL: Repository, https://github.com/janprikryl1/revolut-x-python
|
|
|
10
10
|
Project-URL: Changelog, https://github.com/janprikryl1/revolut-x-python/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/janprikryl1/revolut-x-python/issues
|
|
12
12
|
Keywords: revolut,revolut-x,crypto,exchange,trading,api,sdk,bitcoin
|
|
13
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
-
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
-
Classifier: Typing :: Typed
|
|
27
13
|
Requires-Python: >=3.10
|
|
28
14
|
Description-Content-Type: text/markdown
|
|
29
15
|
License-File: LICENSE
|
|
@@ -15,22 +15,6 @@ authors = [
|
|
|
15
15
|
{ name = "Jan Přikryl" },
|
|
16
16
|
]
|
|
17
17
|
keywords = ["revolut", "revolut-x", "crypto", "exchange", "trading", "api", "sdk", "bitcoin"]
|
|
18
|
-
classifiers = [
|
|
19
|
-
"Development Status :: 5 - Production/Stable",
|
|
20
|
-
"Intended Audience :: Developers",
|
|
21
|
-
"Intended Audience :: Financial and Insurance Industry",
|
|
22
|
-
"Operating System :: OS Independent",
|
|
23
|
-
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
-
"Programming Language :: Python :: 3.10",
|
|
26
|
-
"Programming Language :: Python :: 3.11",
|
|
27
|
-
"Programming Language :: Python :: 3.12",
|
|
28
|
-
"Programming Language :: Python :: 3.13",
|
|
29
|
-
"Programming Language :: Python :: 3.14",
|
|
30
|
-
"Topic :: Office/Business :: Financial :: Investment",
|
|
31
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
-
"Typing :: Typed",
|
|
33
|
-
]
|
|
34
18
|
dependencies = [
|
|
35
19
|
"requests>=2.31.0",
|
|
36
20
|
"cryptography>=41.0.0",
|
|
@@ -39,6 +39,7 @@ from revolut_x.types import (
|
|
|
39
39
|
OrderResponse,
|
|
40
40
|
PairConfig,
|
|
41
41
|
Ticker,
|
|
42
|
+
TimeLike,
|
|
42
43
|
Trade,
|
|
43
44
|
)
|
|
44
45
|
from revolut_x.helpers import (
|
|
@@ -51,6 +52,7 @@ from revolut_x.helpers import (
|
|
|
51
52
|
build_order,
|
|
52
53
|
calculate_maker_price,
|
|
53
54
|
normalize_symbol,
|
|
55
|
+
to_timestamp_ms,
|
|
54
56
|
)
|
|
55
57
|
from revolut_x.exceptions import (
|
|
56
58
|
ApiError,
|
|
@@ -76,6 +78,7 @@ __all__ = [
|
|
|
76
78
|
"FeeCalculator",
|
|
77
79
|
"FeeEstimate",
|
|
78
80
|
"normalize_symbol",
|
|
81
|
+
"to_timestamp_ms",
|
|
79
82
|
"calculate_maker_price",
|
|
80
83
|
"build_order",
|
|
81
84
|
"build_market_order",
|
|
@@ -92,6 +95,7 @@ __all__ = [
|
|
|
92
95
|
"OrderResponse",
|
|
93
96
|
"PairConfig",
|
|
94
97
|
"Ticker",
|
|
98
|
+
"TimeLike",
|
|
95
99
|
"Trade",
|
|
96
100
|
# Exceptions
|
|
97
101
|
"RevolutXError",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import uuid
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from datetime import date, datetime, time, timezone
|
|
4
5
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
|
-
from revolut_x.types import OrderSide, OrderType, TimeInForce
|
|
8
|
+
from revolut_x.types import OrderSide, OrderType, TimeInForce, TimeLike
|
|
8
9
|
from revolut_x.exceptions import OrderValidationError
|
|
9
10
|
|
|
10
11
|
|
|
@@ -35,6 +36,96 @@ def normalize_symbol(symbol: str) -> str:
|
|
|
35
36
|
return symbol.replace("/", "-").upper()
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
#: Minimum number of digits a digits-only string needs before it is read as a
|
|
40
|
+
#: millisecond timestamp. 12 digits reach back to 1973 and real timestamps have
|
|
41
|
+
#: had 13 since 2001, so anything shorter is almost certainly a date that lost
|
|
42
|
+
#: its separators (``'2024'``, ``'20240101'``) rather than an instant in 1970.
|
|
43
|
+
MIN_MS_DIGITS = 12
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_timestamp_ms(value: TimeLike) -> int:
|
|
47
|
+
"""Converts a point in time to a Unix timestamp in milliseconds.
|
|
48
|
+
|
|
49
|
+
Used by the market data methods so that time ranges can be expressed with
|
|
50
|
+
``datetime`` objects instead of raw millisecond integers.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value: One of
|
|
54
|
+
|
|
55
|
+
* ``int`` / ``float`` — already a Unix timestamp in
|
|
56
|
+
**milliseconds**; returned as an int (floats are truncated).
|
|
57
|
+
Seconds are *not* auto-detected — multiply by 1000 yourself.
|
|
58
|
+
* :class:`~datetime.datetime` — timezone-aware values use their own
|
|
59
|
+
offset, naive values are interpreted as **UTC**.
|
|
60
|
+
* :class:`~datetime.date` — midnight UTC on that day.
|
|
61
|
+
* ``str`` — an ISO-8601 timestamp (a trailing ``'Z'`` is accepted);
|
|
62
|
+
naive strings are again interpreted as UTC. A digits-only string
|
|
63
|
+
of at least :data:`MIN_MS_DIGITS` digits is read as milliseconds
|
|
64
|
+
instead, for values arriving from JSON or query parameters.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
int: The Unix timestamp in milliseconds.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
TypeError: If the value is not one of the supported types.
|
|
71
|
+
ValueError: If a string is neither a valid ISO-8601 timestamp nor a
|
|
72
|
+
long enough run of digits.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> from datetime import datetime, timezone
|
|
76
|
+
>>> to_timestamp_ms(datetime(2024, 1, 1, tzinfo=timezone.utc))
|
|
77
|
+
1704067200000
|
|
78
|
+
>>> to_timestamp_ms("2024-01-01")
|
|
79
|
+
1704067200000
|
|
80
|
+
>>> to_timestamp_ms(1704067200000)
|
|
81
|
+
1704067200000
|
|
82
|
+
>>> to_timestamp_ms("1704067200000")
|
|
83
|
+
1704067200000
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(value, bool):
|
|
86
|
+
raise TypeError("Timestamp must be a number, datetime, date, or ISO-8601 string")
|
|
87
|
+
|
|
88
|
+
if isinstance(value, (int, float)):
|
|
89
|
+
return int(value)
|
|
90
|
+
|
|
91
|
+
if isinstance(value, str):
|
|
92
|
+
text = value.strip()
|
|
93
|
+
|
|
94
|
+
digits = text[1:] if text[:1] in "+-" else text
|
|
95
|
+
# str.isdigit() also accepts non-ASCII digits that int() cannot parse.
|
|
96
|
+
if digits.isascii() and digits.isdigit():
|
|
97
|
+
if len(digits) >= MIN_MS_DIGITS:
|
|
98
|
+
return int(text)
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Ambiguous numeric timestamp string: {value!r}. A digits-only string is "
|
|
101
|
+
f"read as Unix milliseconds and needs at least {MIN_MS_DIGITS} digits — "
|
|
102
|
+
f"anything shorter lands in 1970 and is far more likely a date that lost "
|
|
103
|
+
f"its separators. Pass an int for milliseconds, or a date like "
|
|
104
|
+
f"'2024-01-01'."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if text.endswith(("Z", "z")):
|
|
108
|
+
text = f"{text[:-1]}+00:00"
|
|
109
|
+
try:
|
|
110
|
+
value = datetime.fromisoformat(text)
|
|
111
|
+
except ValueError as exc:
|
|
112
|
+
raise ValueError(f"Invalid ISO-8601 timestamp: {value!r}") from exc
|
|
113
|
+
|
|
114
|
+
# datetime is a subclass of date, so it has to be checked first.
|
|
115
|
+
if isinstance(value, datetime):
|
|
116
|
+
if value.tzinfo is None:
|
|
117
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
118
|
+
return int(value.timestamp() * 1000)
|
|
119
|
+
|
|
120
|
+
if isinstance(value, date):
|
|
121
|
+
return int(datetime.combine(value, time.min, tzinfo=timezone.utc).timestamp() * 1000)
|
|
122
|
+
|
|
123
|
+
raise TypeError(
|
|
124
|
+
f"Timestamp must be a number, datetime, date, or ISO-8601 string, "
|
|
125
|
+
f"got {type(value).__name__}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
38
129
|
class OrderPayloadBuilder:
|
|
39
130
|
"""Builder and validator for Revolut X API order payloads."""
|
|
40
131
|
|
|
@@ -19,9 +19,11 @@ from revolut_x.types import (
|
|
|
19
19
|
OrderBook,
|
|
20
20
|
PairConfig,
|
|
21
21
|
Ticker,
|
|
22
|
+
TimeLike,
|
|
22
23
|
Trade,
|
|
23
24
|
)
|
|
24
25
|
from revolut_x.helpers import normalize_symbol as _normalize_symbol
|
|
26
|
+
from revolut_x.helpers import to_timestamp_ms as _to_ms
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
27
29
|
from revolut_x._http import HttpClient
|
|
@@ -232,8 +234,8 @@ class MarketMixin:
|
|
|
232
234
|
symbol: str,
|
|
233
235
|
interval: Interval | int = Interval.HOUR_1,
|
|
234
236
|
*,
|
|
235
|
-
since:
|
|
236
|
-
until:
|
|
237
|
+
since: TimeLike | None = None,
|
|
238
|
+
until: TimeLike | None = None,
|
|
237
239
|
) -> list[Candle]:
|
|
238
240
|
"""Fetch up to 1000 OHLCV candlesticks for a trading pair.
|
|
239
241
|
|
|
@@ -242,8 +244,11 @@ class MarketMixin:
|
|
|
242
244
|
interval: Candle interval — use the :class:`~revolut_x.types.Interval`
|
|
243
245
|
enum (e.g. ``Interval.MIN_1``, ``Interval.HOUR_1``) or an integer
|
|
244
246
|
number of minutes.
|
|
245
|
-
since: Start time
|
|
246
|
-
|
|
247
|
+
since: Start time, inclusive — a Unix timestamp in
|
|
248
|
+
**milliseconds**, a :class:`~datetime.datetime` /
|
|
249
|
+
:class:`~datetime.date`, or an ISO-8601 string (see
|
|
250
|
+
:func:`~revolut_x.helpers.to_timestamp_ms`).
|
|
251
|
+
until: End time, inclusive — same accepted formats as ``since``.
|
|
247
252
|
|
|
248
253
|
Returns:
|
|
249
254
|
A list of :class:`~revolut_x.types.Candle` dictionaries with keys:
|
|
@@ -263,20 +268,21 @@ class MarketMixin:
|
|
|
263
268
|
|
|
264
269
|
Example::
|
|
265
270
|
|
|
271
|
+
>>> from datetime import datetime, timedelta, timezone
|
|
266
272
|
>>> from revolut_x import Interval
|
|
267
273
|
>>> # last 500 hours — comfortably inside the 1000-candle limit
|
|
268
|
-
>>>
|
|
274
|
+
>>> start = datetime.now(timezone.utc) - timedelta(hours=500)
|
|
269
275
|
>>> candles = client.get_candles("BTC-EUR", Interval.HOUR_1,
|
|
270
|
-
... since=
|
|
276
|
+
... since=start)
|
|
271
277
|
>>> for c in candles[:3]:
|
|
272
278
|
... print(f"{c['start']}: O={c['open']} H={c['high']}")
|
|
273
279
|
"""
|
|
274
280
|
api_symbol = _normalize_symbol(symbol)
|
|
275
281
|
params: dict[str, Any] = {"interval": int(interval)}
|
|
276
282
|
if since is not None:
|
|
277
|
-
params["since"] =
|
|
283
|
+
params["since"] = _to_ms(since)
|
|
278
284
|
if until is not None:
|
|
279
|
-
params["until"] =
|
|
285
|
+
params["until"] = _to_ms(until)
|
|
280
286
|
|
|
281
287
|
_status, data = self._http.request(
|
|
282
288
|
"GET",
|
|
@@ -293,8 +299,8 @@ class MarketMixin:
|
|
|
293
299
|
symbol: str,
|
|
294
300
|
interval: Interval | int = Interval.HOUR_1,
|
|
295
301
|
*,
|
|
296
|
-
since:
|
|
297
|
-
until:
|
|
302
|
+
since: TimeLike | None = None,
|
|
303
|
+
until: TimeLike | None = None,
|
|
298
304
|
) -> Iterator[Candle]:
|
|
299
305
|
"""Iterate over all candles in a time range, automatically paginating.
|
|
300
306
|
|
|
@@ -305,8 +311,10 @@ class MarketMixin:
|
|
|
305
311
|
Args:
|
|
306
312
|
symbol: Trading pair (e.g. ``'BTC-EUR'``).
|
|
307
313
|
interval: Candle interval (see :meth:`get_candles`).
|
|
308
|
-
since: Start time
|
|
309
|
-
|
|
314
|
+
since: Start time — Unix timestamp in **milliseconds**,
|
|
315
|
+
:class:`~datetime.datetime` / :class:`~datetime.date`, or an
|
|
316
|
+
ISO-8601 string.
|
|
317
|
+
until: End time — same accepted formats as ``since``.
|
|
310
318
|
|
|
311
319
|
Yields:
|
|
312
320
|
:class:`~revolut_x.types.Candle` dictionaries in chronological
|
|
@@ -316,7 +324,7 @@ class MarketMixin:
|
|
|
316
324
|
Example::
|
|
317
325
|
|
|
318
326
|
>>> for candle in client.iter_candles("BTC-EUR", Interval.MIN_1,
|
|
319
|
-
... since=
|
|
327
|
+
... since="2024-01-01"):
|
|
320
328
|
... process(candle) # each candle is a dict
|
|
321
329
|
"""
|
|
322
330
|
interval_ms = int(interval) * 60 * 1000 # interval in milliseconds
|
|
@@ -324,19 +332,22 @@ class MarketMixin:
|
|
|
324
332
|
# window is inclusive on both ends — so 999 intervals == 1000 candles.
|
|
325
333
|
max_batch_ms = 999 * interval_ms
|
|
326
334
|
|
|
327
|
-
if since is None
|
|
335
|
+
since_ms = _to_ms(since) if since is not None else None
|
|
336
|
+
until_ms = _to_ms(until) if until is not None else None
|
|
337
|
+
|
|
338
|
+
if since_ms is None:
|
|
328
339
|
# Without a start there is nothing to paginate: a single request
|
|
329
340
|
# already returns the most recent batch.
|
|
330
|
-
yield from self.get_candles(symbol, interval, until=
|
|
341
|
+
yield from self.get_candles(symbol, interval, until=until_ms)
|
|
331
342
|
return
|
|
332
343
|
|
|
333
|
-
current_since =
|
|
344
|
+
current_since = since_ms
|
|
334
345
|
|
|
335
346
|
while True:
|
|
336
347
|
# Walk fixed-size windows forward rather than chaining off the last
|
|
337
348
|
# returned candle, so that gaps in the series (illiquid pairs with
|
|
338
349
|
# no trades in an interval) cannot cut the iteration short.
|
|
339
|
-
hard_end =
|
|
350
|
+
hard_end = until_ms if until_ms is not None else int(time.time() * 1000)
|
|
340
351
|
batch_until = min(current_since + max_batch_ms, hard_end)
|
|
341
352
|
# The API rejects a window where 'until' equals 'since', which is
|
|
342
353
|
# what a final single-candle window would produce. Always ask for
|
|
@@ -346,8 +357,8 @@ class MarketMixin:
|
|
|
346
357
|
batch = self.get_candles(
|
|
347
358
|
symbol, interval, since=current_since, until=batch_until
|
|
348
359
|
)
|
|
349
|
-
if
|
|
350
|
-
batch = [c for c in batch if int(c.get("start", 0)) <=
|
|
360
|
+
if until_ms is not None:
|
|
361
|
+
batch = [c for c in batch if int(c.get("start", 0)) <= until_ms]
|
|
351
362
|
|
|
352
363
|
yield from batch
|
|
353
364
|
|
|
@@ -364,8 +375,8 @@ class MarketMixin:
|
|
|
364
375
|
self,
|
|
365
376
|
symbol: str,
|
|
366
377
|
*,
|
|
367
|
-
start_date:
|
|
368
|
-
end_date:
|
|
378
|
+
start_date: TimeLike | None = None,
|
|
379
|
+
end_date: TimeLike | None = None,
|
|
369
380
|
cursor: str | None = None,
|
|
370
381
|
limit: int = MAX_TRADES_PER_PAGE,
|
|
371
382
|
) -> tuple[list[Trade], str | None]:
|
|
@@ -373,8 +384,11 @@ class MarketMixin:
|
|
|
373
384
|
|
|
374
385
|
Args:
|
|
375
386
|
symbol: Trading pair (e.g. ``'BTC-EUR'``).
|
|
376
|
-
start_date: Start time
|
|
377
|
-
|
|
387
|
+
start_date: Start time — Unix timestamp in **milliseconds**,
|
|
388
|
+
:class:`~datetime.datetime` / :class:`~datetime.date`, or an
|
|
389
|
+
ISO-8601 string (see
|
|
390
|
+
:func:`~revolut_x.helpers.to_timestamp_ms`).
|
|
391
|
+
end_date: End time — same accepted formats as ``start_date``.
|
|
378
392
|
cursor: Pagination cursor from a previous response's
|
|
379
393
|
``metadata.next_cursor`` field.
|
|
380
394
|
limit: Maximum number of trades to return. The exchange accepts
|
|
@@ -401,9 +415,9 @@ class MarketMixin:
|
|
|
401
415
|
"limit": max(1, min(limit, MAX_TRADES_PER_PAGE)),
|
|
402
416
|
}
|
|
403
417
|
if start_date is not None:
|
|
404
|
-
params["start_date"] =
|
|
418
|
+
params["start_date"] = _to_ms(start_date)
|
|
405
419
|
if end_date is not None:
|
|
406
|
-
params["end_date"] =
|
|
420
|
+
params["end_date"] = _to_ms(end_date)
|
|
407
421
|
if cursor:
|
|
408
422
|
params["cursor"] = cursor
|
|
409
423
|
|
|
@@ -424,8 +438,8 @@ class MarketMixin:
|
|
|
424
438
|
self,
|
|
425
439
|
symbol: str,
|
|
426
440
|
*,
|
|
427
|
-
start_date:
|
|
428
|
-
end_date:
|
|
441
|
+
start_date: TimeLike | None = None,
|
|
442
|
+
end_date: TimeLike | None = None,
|
|
429
443
|
limit: int = MAX_TRADES_PER_PAGE,
|
|
430
444
|
) -> Iterator[Trade]:
|
|
431
445
|
"""Iterate over all public trades in a time range, automatically paginating.
|
|
@@ -435,8 +449,10 @@ class MarketMixin:
|
|
|
435
449
|
|
|
436
450
|
Args:
|
|
437
451
|
symbol: Trading pair (e.g. ``'BTC-EUR'``).
|
|
438
|
-
start_date: Start time
|
|
439
|
-
|
|
452
|
+
start_date: Start time — Unix timestamp in **milliseconds**,
|
|
453
|
+
:class:`~datetime.datetime` / :class:`~datetime.date`, or an
|
|
454
|
+
ISO-8601 string.
|
|
455
|
+
end_date: End time — same accepted formats as ``start_date``.
|
|
440
456
|
limit: Page size per request (1–100, clamped into that range).
|
|
441
457
|
|
|
442
458
|
Yields:
|
|
@@ -444,17 +460,23 @@ class MarketMixin:
|
|
|
444
460
|
|
|
445
461
|
Example::
|
|
446
462
|
|
|
447
|
-
>>>
|
|
448
|
-
|
|
463
|
+
>>> from datetime import datetime, timezone
|
|
464
|
+
>>> start = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
465
|
+
>>> for trade in client.iter_trades("BTC-EUR", start_date=start):
|
|
449
466
|
... print(f"{trade['price']} EUR x {trade['quantity']} BTC")
|
|
450
467
|
"""
|
|
468
|
+
# Convert once so every page sends the same window and a bad value
|
|
469
|
+
# fails on the first iteration rather than halfway through.
|
|
470
|
+
start_ms = _to_ms(start_date) if start_date is not None else None
|
|
471
|
+
end_ms = _to_ms(end_date) if end_date is not None else None
|
|
472
|
+
|
|
451
473
|
cursor: str | None = None
|
|
452
474
|
|
|
453
475
|
while True:
|
|
454
476
|
trades, next_cursor = self.get_trades(
|
|
455
477
|
symbol,
|
|
456
|
-
start_date=
|
|
457
|
-
end_date=
|
|
478
|
+
start_date=start_ms,
|
|
479
|
+
end_date=end_ms,
|
|
458
480
|
cursor=cursor,
|
|
459
481
|
limit=limit,
|
|
460
482
|
)
|
|
@@ -7,11 +7,26 @@ This module provides:
|
|
|
7
7
|
* **TypedDicts** — :class:`Candle`, :class:`Trade`, :class:`Ticker`,
|
|
8
8
|
:class:`Balance`, :class:`PairConfig`, etc. for IDE autocompletion
|
|
9
9
|
on API response data.
|
|
10
|
+
* **Type aliases** — :data:`TimeLike` for time range parameters.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
14
|
+
from datetime import date, datetime
|
|
13
15
|
from enum import Enum
|
|
14
|
-
from typing import TypedDict
|
|
16
|
+
from typing import TypedDict, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Type aliases
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
TimeLike = Union[int, float, datetime, date, str]
|
|
24
|
+
"""Anything accepted where the API expects a Unix timestamp in milliseconds.
|
|
25
|
+
|
|
26
|
+
Numbers are taken as milliseconds as-is; :class:`~datetime.datetime` and
|
|
27
|
+
:class:`~datetime.date` objects and ISO-8601 strings are converted by
|
|
28
|
+
:func:`~revolut_x.helpers.to_timestamp_ms`.
|
|
29
|
+
"""
|
|
15
30
|
|
|
16
31
|
|
|
17
32
|
# ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revolut-x-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python SDK for the Revolut X Crypto Exchange REST API (v1.0)
|
|
5
5
|
Author: Jan Přikryl
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,20 +10,6 @@ Project-URL: Repository, https://github.com/janprikryl1/revolut-x-python
|
|
|
10
10
|
Project-URL: Changelog, https://github.com/janprikryl1/revolut-x-python/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/janprikryl1/revolut-x-python/issues
|
|
12
12
|
Keywords: revolut,revolut-x,crypto,exchange,trading,api,sdk,bitcoin
|
|
13
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
-
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
-
Classifier: Typing :: Typed
|
|
27
13
|
Requires-Python: >=3.10
|
|
28
14
|
Description-Content-Type: text/markdown
|
|
29
15
|
License-File: LICENSE
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from datetime import date, datetime, timedelta, timezone
|
|
5
6
|
from decimal import Decimal
|
|
6
7
|
from typing import Any, Dict
|
|
7
8
|
|
|
@@ -16,6 +17,7 @@ from revolut_x.helpers import (
|
|
|
16
17
|
build_market_order,
|
|
17
18
|
build_order,
|
|
18
19
|
normalize_symbol,
|
|
20
|
+
to_timestamp_ms,
|
|
19
21
|
validate_against_pair_rules,
|
|
20
22
|
)
|
|
21
23
|
from revolut_x.types import OrderSide, OrderType, TimeInForce
|
|
@@ -297,3 +299,73 @@ def test_fee_calculator_taker() -> None:
|
|
|
297
299
|
# Trade value 1000.00 * 0.0009 = 0.9000 EUR
|
|
298
300
|
assert estimate.fee_amount == Decimal("0.9000")
|
|
299
301
|
assert estimate.fee_currency == "EUR"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# to_timestamp_ms
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
#: 2024-01-01T00:00:00Z
|
|
309
|
+
EPOCH_2024_MS = 1_704_067_200_000
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_to_timestamp_ms_passes_numbers_through() -> None:
|
|
313
|
+
"""Integers are already milliseconds — seconds must not be auto-detected."""
|
|
314
|
+
assert to_timestamp_ms(EPOCH_2024_MS) == EPOCH_2024_MS
|
|
315
|
+
assert to_timestamp_ms(EPOCH_2024_MS / 1000) == EPOCH_2024_MS // 1000
|
|
316
|
+
assert to_timestamp_ms(1_704_067_200_123.9) == 1_704_067_200_123
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_to_timestamp_ms_converts_aware_datetime() -> None:
|
|
320
|
+
assert to_timestamp_ms(datetime(2024, 1, 1, tzinfo=timezone.utc)) == EPOCH_2024_MS
|
|
321
|
+
# A +02:00 wall clock of 02:00 is the same instant as 00:00 UTC.
|
|
322
|
+
offset = timezone(timedelta(hours=2))
|
|
323
|
+
assert to_timestamp_ms(datetime(2024, 1, 1, 2, tzinfo=offset)) == EPOCH_2024_MS
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_to_timestamp_ms_treats_naive_datetime_as_utc() -> None:
|
|
327
|
+
"""Naive values must not depend on the machine's local timezone."""
|
|
328
|
+
assert to_timestamp_ms(datetime(2024, 1, 1)) == EPOCH_2024_MS
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_to_timestamp_ms_converts_date_to_midnight_utc() -> None:
|
|
332
|
+
assert to_timestamp_ms(date(2024, 1, 1)) == EPOCH_2024_MS
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_to_timestamp_ms_parses_iso_strings() -> None:
|
|
336
|
+
assert to_timestamp_ms("2024-01-01") == EPOCH_2024_MS
|
|
337
|
+
assert to_timestamp_ms("2024-01-01T00:00:00") == EPOCH_2024_MS
|
|
338
|
+
assert to_timestamp_ms("2024-01-01T00:00:00+00:00") == EPOCH_2024_MS
|
|
339
|
+
# A trailing 'Z' is not accepted by fromisoformat before Python 3.11.
|
|
340
|
+
assert to_timestamp_ms("2024-01-01T00:00:00Z") == EPOCH_2024_MS
|
|
341
|
+
assert to_timestamp_ms(" 2024-01-01T00:00:00Z ") == EPOCH_2024_MS
|
|
342
|
+
assert to_timestamp_ms("2024-01-01T00:00:00.500Z") == EPOCH_2024_MS + 500
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_to_timestamp_ms_rejects_bad_input() -> None:
|
|
346
|
+
with pytest.raises(ValueError):
|
|
347
|
+
to_timestamp_ms("last tuesday")
|
|
348
|
+
with pytest.raises(TypeError):
|
|
349
|
+
to_timestamp_ms(None) # type: ignore[arg-type]
|
|
350
|
+
with pytest.raises(TypeError):
|
|
351
|
+
to_timestamp_ms(True) # type: ignore[arg-type]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_to_timestamp_ms_accepts_long_digit_strings_as_milliseconds() -> None:
|
|
355
|
+
"""Values arriving from JSON or query parameters are strings."""
|
|
356
|
+
assert to_timestamp_ms("1704067200000") == EPOCH_2024_MS
|
|
357
|
+
assert to_timestamp_ms("-1704067200000") == -EPOCH_2024_MS
|
|
358
|
+
# Exactly at the threshold: 1973-03-03, the oldest value still read as ms.
|
|
359
|
+
assert to_timestamp_ms("100000000000") == 100_000_000_000
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.mark.parametrize("value", ["2024", "20240101", "0", "1704067200", "-500"])
|
|
363
|
+
def test_to_timestamp_ms_rejects_short_digit_strings(value: str) -> None:
|
|
364
|
+
"""A separatorless date must not be read as an instant in 1970.
|
|
365
|
+
|
|
366
|
+
``'20240101'`` is valid ISO-8601 basic format, so on Python 3.11+ it used to
|
|
367
|
+
parse as 2024-01-01 here while the PHP SDK read it as 20240101 ms — a silent
|
|
368
|
+
cross-SDK discrepancy, and on Python 3.10 it raised. Both SDKs now refuse it.
|
|
369
|
+
"""
|
|
370
|
+
with pytest.raises(ValueError, match="Ambiguous numeric timestamp string"):
|
|
371
|
+
to_timestamp_ms(value)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Unit tests for MarketMixin in revolut_x.market."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
from datetime import date, datetime, timedelta, timezone
|
|
4
5
|
from typing import Any
|
|
5
6
|
from unittest.mock import MagicMock
|
|
6
7
|
|
|
@@ -189,3 +190,71 @@ def test_get_ticker_matches_slash_and_dash_symbols() -> None:
|
|
|
189
190
|
for requested in ("BTC-EUR", "BTC/EUR", "btc-eur"):
|
|
190
191
|
client._http.request.return_value = (200, {"data": [{"symbol": "ETH/EUR"}, btc]})
|
|
191
192
|
assert client.get_ticker(requested) == btc
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# datetime-friendly time ranges
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def test_get_candles_accepts_datetime_bounds() -> None:
|
|
200
|
+
"""datetime/str bounds must reach the API as millisecond integers."""
|
|
201
|
+
client = DummyMarketClient()
|
|
202
|
+
client._http.request.return_value = (200, {"data": []})
|
|
203
|
+
|
|
204
|
+
client.get_candles(
|
|
205
|
+
"BTC-EUR",
|
|
206
|
+
INTERVAL_MIN,
|
|
207
|
+
since=datetime(2024, 1, 1, tzinfo=timezone.utc),
|
|
208
|
+
until="2024-01-02",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
params = client._http.request.call_args.kwargs["params"]
|
|
212
|
+
assert params["since"] == 1_704_067_200_000
|
|
213
|
+
assert params["until"] == 1_704_153_600_000
|
|
214
|
+
assert all(isinstance(params[k], int) for k in ("since", "until"))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_get_trades_accepts_datetime_bounds() -> None:
|
|
218
|
+
client = DummyMarketClient()
|
|
219
|
+
client._http.request.return_value = (200, {"data": [], "metadata": {}})
|
|
220
|
+
|
|
221
|
+
client.get_trades(
|
|
222
|
+
"BTC-EUR",
|
|
223
|
+
start_date=date(2024, 1, 1),
|
|
224
|
+
end_date=datetime(2024, 1, 2, tzinfo=timezone.utc),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
params = client._http.request.call_args.kwargs["params"]
|
|
228
|
+
assert params["start_date"] == 1_704_067_200_000
|
|
229
|
+
assert params["end_date"] == 1_704_153_600_000
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_iter_candles_paginates_from_a_datetime() -> None:
|
|
233
|
+
"""Pagination arithmetic must run on the converted value, not the object."""
|
|
234
|
+
base = 1_704_067_200_000 # 2024-01-01T00:00:00Z
|
|
235
|
+
client = FakeCandleClient(base, 3000)
|
|
236
|
+
since = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
237
|
+
until = since + timedelta(minutes=1500)
|
|
238
|
+
|
|
239
|
+
got = _starts(list(client.iter_candles("BTC-EUR", INTERVAL_MIN, since=since, until=until)))
|
|
240
|
+
|
|
241
|
+
assert len(got) == 1501
|
|
242
|
+
assert got[0] == base
|
|
243
|
+
assert got[-1] == base + 1500 * INTERVAL_MS
|
|
244
|
+
assert len(client.requests) > 1, "a 1501-candle range must span several pages"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_iter_trades_converts_bounds_once_for_every_page() -> None:
|
|
248
|
+
client = DummyMarketClient()
|
|
249
|
+
pages = [
|
|
250
|
+
(200, {"data": [{"timestamp": 1}], "metadata": {"next_cursor": "c2"}}),
|
|
251
|
+
(200, {"data": [{"timestamp": 2}], "metadata": {}}),
|
|
252
|
+
]
|
|
253
|
+
client._http.request.side_effect = pages
|
|
254
|
+
|
|
255
|
+
got = list(client.iter_trades("BTC-EUR", start_date="2024-01-01T00:00:00Z"))
|
|
256
|
+
|
|
257
|
+
assert len(got) == 2
|
|
258
|
+
sent = [c.kwargs["params"] for c in client._http.request.call_args_list]
|
|
259
|
+
assert [p["start_date"] for p in sent] == [1_704_067_200_000] * 2
|
|
260
|
+
assert sent[1]["cursor"] == "c2"
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
include LICENSE
|
|
2
|
-
include README.md
|
|
3
|
-
include CHANGELOG.md
|
|
4
|
-
include requirements.txt
|
|
5
|
-
|
|
6
|
-
# The AI agent skill is an advertised feature of the package, so it travels
|
|
7
|
-
# with the sdist instead of being GitHub-only.
|
|
8
|
-
recursive-include .agents *.md
|
|
9
|
-
|
|
10
|
-
recursive-include tests *.py
|
|
11
|
-
recursive-include examples *.py *.md
|
|
12
|
-
|
|
13
|
-
global-exclude *.py[cod]
|
|
14
|
-
global-exclude .DS_Store
|
|
15
|
-
prune docs
|
|
16
|
-
prune site
|
|
17
|
-
prune dist
|
|
18
|
-
prune venv
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/03_account_balances_and_history.py
RENAMED
|
File without changes
|
|
File without changes
|
{revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/examples/05_fee_calculator_and_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{revolut_x_python-1.0.0 → revolut_x_python-1.0.1}/src/revolut_x_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|