revolut-x-python 1.0.0__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/.agents/skills/revolut-x-library/SKILL.md +491 -0
- revolut_x_python-1.0.0/.agents/skills/revolut-x-library/references/types.md +289 -0
- revolut_x_python-1.0.0/CHANGELOG.md +216 -0
- revolut_x_python-1.0.0/LICENSE +21 -0
- revolut_x_python-1.0.0/MANIFEST.in +18 -0
- revolut_x_python-1.0.0/PKG-INFO +164 -0
- revolut_x_python-1.0.0/README.md +120 -0
- revolut_x_python-1.0.0/examples/01_public_market_data.py +31 -0
- revolut_x_python-1.0.0/examples/02_download_candles_stream.py +23 -0
- revolut_x_python-1.0.0/examples/03_account_balances_and_history.py +33 -0
- revolut_x_python-1.0.0/examples/04_place_and_cancel_orders.py +46 -0
- revolut_x_python-1.0.0/examples/05_fee_calculator_and_validation.py +30 -0
- revolut_x_python-1.0.0/examples/06_smart_maker_orders.py +76 -0
- revolut_x_python-1.0.0/examples/README.md +34 -0
- revolut_x_python-1.0.0/pyproject.toml +72 -0
- revolut_x_python-1.0.0/requirements.txt +5 -0
- revolut_x_python-1.0.0/setup.cfg +4 -0
- revolut_x_python-1.0.0/src/revolut_x/__init__.py +103 -0
- revolut_x_python-1.0.0/src/revolut_x/_auth.py +139 -0
- revolut_x_python-1.0.0/src/revolut_x/_http.py +270 -0
- revolut_x_python-1.0.0/src/revolut_x/_version.py +10 -0
- revolut_x_python-1.0.0/src/revolut_x/account.py +203 -0
- revolut_x_python-1.0.0/src/revolut_x/client.py +170 -0
- revolut_x_python-1.0.0/src/revolut_x/exceptions.py +147 -0
- revolut_x_python-1.0.0/src/revolut_x/helpers.py +540 -0
- revolut_x_python-1.0.0/src/revolut_x/market.py +469 -0
- revolut_x_python-1.0.0/src/revolut_x/orders.py +522 -0
- revolut_x_python-1.0.0/src/revolut_x/py.typed +0 -0
- revolut_x_python-1.0.0/src/revolut_x/types.py +397 -0
- revolut_x_python-1.0.0/src/revolut_x_python.egg-info/PKG-INFO +164 -0
- revolut_x_python-1.0.0/src/revolut_x_python.egg-info/SOURCES.txt +38 -0
- revolut_x_python-1.0.0/src/revolut_x_python.egg-info/dependency_links.txt +1 -0
- revolut_x_python-1.0.0/src/revolut_x_python.egg-info/requires.txt +15 -0
- revolut_x_python-1.0.0/src/revolut_x_python.egg-info/top_level.txt +1 -0
- revolut_x_python-1.0.0/tests/__init__.py +1 -0
- revolut_x_python-1.0.0/tests/test_auth.py +200 -0
- revolut_x_python-1.0.0/tests/test_helpers.py +299 -0
- revolut_x_python-1.0.0/tests/test_maker_strategy.py +188 -0
- revolut_x_python-1.0.0/tests/test_market.py +191 -0
- revolut_x_python-1.0.0/tests/test_orders.py +131 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: revolut-x-library
|
|
3
|
+
description: >-
|
|
4
|
+
Use this skill when the user asks to interact with the Revolut X Crypto
|
|
5
|
+
Exchange API using Python, when they need to fetch market data (tickers,
|
|
6
|
+
order book, candles, trades), place or manage orders, check account
|
|
7
|
+
balances, or work with any part of the revolut-x-python SDK — including
|
|
8
|
+
authentication setup, fee calculation, order payload building, or error handling.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# revolut-x-python SDK
|
|
12
|
+
|
|
13
|
+
Python SDK for the **Revolut X Crypto Exchange** REST API (v1.0).
|
|
14
|
+
|
|
15
|
+
- **Python ≥ 3.10** required
|
|
16
|
+
- **Dependencies**: `requests>=2.31.0`, `cryptography>=41.0.0`
|
|
17
|
+
- **Source**: [src/revolut_x/](./../../src/revolut_x/)
|
|
18
|
+
- **Online Documentation (GitHub Pages)**: [https://janprikryl1.github.io/revolut-x-python/](https://janprikryl1.github.io/revolut-x-python/)
|
|
19
|
+
- **Repository**: [https://github.com/janprikryl1/revolut-x-python](https://github.com/janprikryl1/revolut-x-python)
|
|
20
|
+
- **Sister SDK (PHP)**: [https://github.com/janprikryl1/revolut-x-php](https://github.com/janprikryl1/revolut-x-php)
|
|
21
|
+
- **All numeric values from the API are strings** — use `Decimal` for precision
|
|
22
|
+
|
|
23
|
+
## Installation & Download
|
|
24
|
+
|
|
25
|
+
### 1. From PyPI
|
|
26
|
+
```bash
|
|
27
|
+
pip install revolut-x-python
|
|
28
|
+
|
|
29
|
+
# Pre-release builds are staged on TestPyPI first. `--extra-index-url` (not
|
|
30
|
+
# `--index-url`) keeps requests/cryptography resolvable from PyPI:
|
|
31
|
+
pip install --extra-index-url https://test.pypi.org/simple/ revolut-x-python
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Direct Install from GitHub
|
|
35
|
+
```bash
|
|
36
|
+
pip install "git+https://github.com/janprikryl1/revolut-x-python.git"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Clone Repository & Install from Source
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/janprikryl1/revolut-x-python.git
|
|
42
|
+
cd revolut-x-python
|
|
43
|
+
pip install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## RevolutXClient — Single Entry Point
|
|
49
|
+
|
|
50
|
+
Composes `MarketMixin` (public) + `OrdersMixin` (auth) + `AccountMixin` (auth).
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from revolut_x import RevolutXClient, OrderSide, Interval
|
|
54
|
+
|
|
55
|
+
# Public data only (no credentials needed)
|
|
56
|
+
client = RevolutXClient()
|
|
57
|
+
|
|
58
|
+
# Authenticated (orders + account)
|
|
59
|
+
client = RevolutXClient(
|
|
60
|
+
api_key="pk_live_...",
|
|
61
|
+
private_key_path="keys/private.pem",
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Constructor (all keyword-only)
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Default | Description |
|
|
68
|
+
|-----------|------|---------|-------------|
|
|
69
|
+
| `api_key` | `str \| None` | `None` | API key (required for auth endpoints) |
|
|
70
|
+
| `private_key_path` | `str \| Path \| None` | `None` | Path to Ed25519 PEM private key |
|
|
71
|
+
| `private_key_bytes` | `bytes \| None` | `None` | Raw PEM bytes (alternative to path) |
|
|
72
|
+
| `base_url` | `str` | `"https://revx.revolut.com/api"` | API base URL |
|
|
73
|
+
| `api_version` | `str` | `"1.0"` | API version |
|
|
74
|
+
| `request_delay` | `float` | `0.85` | Min seconds between requests |
|
|
75
|
+
| `timeout` | `int` | `15` | HTTP timeout (seconds) |
|
|
76
|
+
| `max_retries` | `int` | `3` | Retries on 429 / network errors |
|
|
77
|
+
| `timestamp_offset_ms` | `int` | `0` | Manual clock offset (ms) for request signing |
|
|
78
|
+
|
|
79
|
+
`client.is_authenticated → bool` — `True` if both API key and signing key are configured.
|
|
80
|
+
|
|
81
|
+
### Clock Synchronization
|
|
82
|
+
|
|
83
|
+
Signed requests carry `X-Revx-Timestamp`; a local clock that drifts more than a
|
|
84
|
+
few seconds makes the exchange reject them. The client handles this itself —
|
|
85
|
+
before the first authenticated request it reads the server `Date` header and
|
|
86
|
+
stores an offset, and it re-syncs automatically on an HTTP 409 timestamp error.
|
|
87
|
+
Only touch these if you need to override that:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client.sync_time() # force a re-sync, returns the new offset in ms
|
|
91
|
+
client.timestamp_offset # → int (ms); alias: client.timestamp_offset_ms
|
|
92
|
+
client.timestamp_offset = -500 # manual override
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Authentication Setup
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 1. Generate Ed25519 key pair
|
|
101
|
+
openssl genpkey -algorithm ed25519 -out private.pem
|
|
102
|
+
openssl pkey -in private.pem -pubout -out public.pem
|
|
103
|
+
|
|
104
|
+
# 2. Upload public.pem to Revolut X → Settings → API
|
|
105
|
+
# 3. Pass api_key + private_key_path to the client
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Headers auto-set: `X-Revx-API-Key`, `X-Revx-Timestamp`, `X-Revx-Signature`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Market Data (No Auth Required)
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
client = RevolutXClient()
|
|
116
|
+
|
|
117
|
+
# Pairs & currencies
|
|
118
|
+
pairs = client.get_pairs() # → dict[str, PairConfig] (keyed by "BTC/EUR")
|
|
119
|
+
pair = client.get_pair("BTC-EUR") # → PairConfig (accepts any format)
|
|
120
|
+
currencies = client.get_currencies() # → dict[str, CurrencyConfig]
|
|
121
|
+
|
|
122
|
+
# Tickers
|
|
123
|
+
tickers = client.get_tickers() # → list[Ticker]
|
|
124
|
+
ticker = client.get_ticker("BTC-EUR") # → Ticker
|
|
125
|
+
# ticker["last_price"], ticker["bid"], ticker["ask"], ticker["volume_24h"]
|
|
126
|
+
# NOTE: 24h fields are suffixed: volume_24h / high_24h / low_24h
|
|
127
|
+
# (there are no plain "volume" / "high" / "low" keys)
|
|
128
|
+
|
|
129
|
+
# Order book — always 5 levels per side; the depth argument is ignored
|
|
130
|
+
book = client.get_order_book("BTC-EUR") # → OrderBook
|
|
131
|
+
# Levels are DICTS, not [price, qty] pairs:
|
|
132
|
+
# book["bids"][0] → {"p": "95000.00", "q": "0.5", "s": "BUYI", "pc": "EUR", ...}
|
|
133
|
+
# price = book["bids"][0]["p"] quantity = book["bids"][0]["q"]
|
|
134
|
+
|
|
135
|
+
# Candles (OHLCV) — see the dedicated section below
|
|
136
|
+
candles = client.get_candles("BTC-EUR", Interval.HOUR_1)
|
|
137
|
+
|
|
138
|
+
# Public trades — single page + cursor.
|
|
139
|
+
# start_date / end_date are Unix MILLISECONDS (int), never date strings —
|
|
140
|
+
# a string like "2024-01-01" raises ValueError.
|
|
141
|
+
import time
|
|
142
|
+
day_ago = int(time.time() * 1000) - 86_400_000
|
|
143
|
+
trades, next_cursor = client.get_trades("BTC-EUR", start_date=day_ago, limit=100)
|
|
144
|
+
|
|
145
|
+
# Auto-paginating trade iterator
|
|
146
|
+
for trade in client.iter_trades("BTC-EUR", start_date=day_ago):
|
|
147
|
+
print(trade["price"], trade["side"])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Candles (OHLCV)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
candles = client.get_candles("BTC-EUR", Interval.HOUR_1)
|
|
156
|
+
candles = client.get_candles("BTC-EUR", Interval.DAY_1, since=ts_ms, until=ts_ms)
|
|
157
|
+
|
|
158
|
+
# Auto-paginating iterator — use for any window wider than 1000 candles
|
|
159
|
+
for c in client.iter_candles("BTC-EUR", Interval.MIN_15, since=start, until=end):
|
|
160
|
+
print(c["close"])
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Each candle is a dict — **not** a list or tuple:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
{"start": 1788058800000, "open": "67419.13", "high": "67547.81",
|
|
167
|
+
"low": "67356.94", "close": "67469.02", "volume": "0.09977242"}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- `start` — candle **open** time, `int` Unix **milliseconds**, always **UTC**
|
|
171
|
+
- `open` / `high` / `low` / `close` / `volume` — **strings**; `volume` is in the
|
|
172
|
+
base currency. Wrap in `Decimal`, never `float`.
|
|
173
|
+
- `since` / `until` are also Unix **milliseconds** (`int`), inclusive on both ends.
|
|
174
|
+
|
|
175
|
+
Three things that make naive candle code wrong:
|
|
176
|
+
|
|
177
|
+
1. **No `since` ≠ "today".** A bare `get_candles(sym, interval)` returns the most
|
|
178
|
+
recent **1000 candles** (for `HOUR_1` that is ~41 days, not today). Always pass
|
|
179
|
+
an explicit `since` when the user asks for a specific period.
|
|
180
|
+
2. **The last candle is still forming.** Its `high`/`low`/`close`/`volume` keep
|
|
181
|
+
changing until the interval ends, so two runs minutes apart legitimately
|
|
182
|
+
return different values for it. Label it or drop it — never present it as a
|
|
183
|
+
closed candle.
|
|
184
|
+
3. **A single call spans at most 1000 candles.** A wider `since`/`until` window is
|
|
185
|
+
rejected with HTTP 400 (`Lookup window interval exceeds the limit of 1000
|
|
186
|
+
candles`) — it is *not* silently truncated. Use `iter_candles` instead.
|
|
187
|
+
|
|
188
|
+
### Reproducible recipe: candles for a calendar day
|
|
189
|
+
|
|
190
|
+
Use this shape whenever the user asks for "today" / "yesterday" / a given date.
|
|
191
|
+
It pins the day to UTC, converts `start` to a readable timestamp, and keeps the
|
|
192
|
+
unfinished candle visibly separate — so the same request gives the same answer
|
|
193
|
+
regardless of who runs it.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
import time
|
|
197
|
+
from datetime import datetime, timezone, time as dtime
|
|
198
|
+
from decimal import Decimal
|
|
199
|
+
from revolut_x import RevolutXClient, Interval
|
|
200
|
+
|
|
201
|
+
client = RevolutXClient()
|
|
202
|
+
interval = Interval.HOUR_1
|
|
203
|
+
|
|
204
|
+
# The exchange timestamps everything in UTC — define the day in UTC too,
|
|
205
|
+
# otherwise the row set shifts with the machine's local timezone.
|
|
206
|
+
day = datetime.now(timezone.utc).date() # or date(2026, 10, 10)
|
|
207
|
+
midnight = datetime.combine(day, dtime.min, tzinfo=timezone.utc)
|
|
208
|
+
since = int(midnight.timestamp() * 1000)
|
|
209
|
+
until = min(since + 86_400_000 - 1, int(time.time() * 1000))
|
|
210
|
+
|
|
211
|
+
candles = client.get_candles("BTC-EUR", interval, since=since, until=until)
|
|
212
|
+
|
|
213
|
+
interval_ms = int(interval) * 60_000
|
|
214
|
+
now_ms = int(time.time() * 1000)
|
|
215
|
+
closed = [c for c in candles if c["start"] + interval_ms <= now_ms]
|
|
216
|
+
forming = [c for c in candles if c["start"] + interval_ms > now_ms]
|
|
217
|
+
|
|
218
|
+
for c in closed:
|
|
219
|
+
opened = datetime.fromtimestamp(c["start"] / 1000, tz=timezone.utc)
|
|
220
|
+
print(f"{opened:%Y-%m-%d %H:%M} UTC O={c['open']:>12} H={c['high']:>12} "
|
|
221
|
+
f"L={c['low']:>12} C={c['close']:>12} V={c['volume']:>14}")
|
|
222
|
+
|
|
223
|
+
# Daily aggregate over CLOSED candles only — reproducible
|
|
224
|
+
if closed:
|
|
225
|
+
print("open ", closed[0]["open"])
|
|
226
|
+
print("high ", max(Decimal(c["high"]) for c in closed))
|
|
227
|
+
print("low ", min(Decimal(c["low"]) for c in closed))
|
|
228
|
+
print("close ", closed[-1]["close"])
|
|
229
|
+
print("volume", sum(Decimal(c["volume"]) for c in closed))
|
|
230
|
+
|
|
231
|
+
for c in forming:
|
|
232
|
+
opened = datetime.fromtimestamp(c["start"] / 1000, tz=timezone.utc)
|
|
233
|
+
print(f"{opened:%H:%M} UTC — in progress, not final: C={c['close']}")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Output conventions to follow unless the user asks otherwise: print a table to
|
|
237
|
+
stdout (don't export a file), timestamps as `YYYY-MM-DD HH:MM` **UTC** (never raw
|
|
238
|
+
epoch numbers), prices and volume verbatim as returned by the API — formatting a
|
|
239
|
+
`str` price through `float` loses precision.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Orders (Auth Required)
|
|
244
|
+
|
|
245
|
+
### Placing Orders
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from revolut_x import OrderSide
|
|
249
|
+
|
|
250
|
+
# Market buy — by quote amount (buy 100 EUR worth of BTC)
|
|
251
|
+
order = client.place_market_order("BTC-EUR", OrderSide.BUY, quote_size="100")
|
|
252
|
+
|
|
253
|
+
# Market sell — by base amount (sell 0.5 BTC)
|
|
254
|
+
order = client.place_market_order("BTC-EUR", OrderSide.SELL, base_size="0.5")
|
|
255
|
+
|
|
256
|
+
# Limit order
|
|
257
|
+
order = client.place_limit_order(
|
|
258
|
+
"BTC-EUR", OrderSide.SELL, price="95000",
|
|
259
|
+
base_size="0.001", time_in_force="gtc",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Zero-fee maker order (limit + post_only, auto-calculates price if None)
|
|
263
|
+
order = client.place_maker_order("BTC-EUR", OrderSide.BUY, quote_size="50")
|
|
264
|
+
|
|
265
|
+
# Maker order with explicit price
|
|
266
|
+
order = client.place_maker_order("BTC-EUR", OrderSide.BUY, price="94000", base_size="0.001")
|
|
267
|
+
|
|
268
|
+
# Calculate optimal maker price without placing order
|
|
269
|
+
price = client.calculate_maker_price("BTC-EUR", OrderSide.BUY, offset="0.10", tick_size="0.01")
|
|
270
|
+
|
|
271
|
+
# Generic place_order accepts either individual args or a pre-built payload.
|
|
272
|
+
# Only symbol and side are positional — everything else is KEYWORD-ONLY.
|
|
273
|
+
order = client.place_order("BTC-EUR", OrderSide.BUY, order_type=OrderType.MARKET, quote_size="100")
|
|
274
|
+
order = client.place_order("BTC-EUR", OrderSide.BUY, order_type="limit", price="94000", quote_size="50")
|
|
275
|
+
# or:
|
|
276
|
+
payload = build_market_order("BTC-EUR", OrderSide.BUY, quote_size="100")
|
|
277
|
+
order = client.place_order(payload)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Order Response
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
order["venue_order_id"] # exchange-assigned ID
|
|
284
|
+
order["client_order_id"] # your UUID
|
|
285
|
+
order["state"] # e.g. "new"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Querying Orders
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
detail = client.get_order("order-id") # → OrderDetail
|
|
292
|
+
fills = client.get_order_fills("order-id") # → list[Fill]
|
|
293
|
+
active = client.get_active_orders() # → list[OrderDetail]
|
|
294
|
+
active = client.get_active_orders(symbol="BTC-EUR") # filtered
|
|
295
|
+
history, cursor = client.get_historical_orders(limit=50) # paginated
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Cancelling
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
client.cancel_order("order-id")
|
|
302
|
+
client.cancel_all_orders() # all pairs
|
|
303
|
+
client.cancel_all_orders(symbol="BTC-EUR") # specific pair
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Account (Auth Required)
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
balances = client.get_balances() # → list[Balance]
|
|
312
|
+
btc = client.get_balance("BTC") # → Balance
|
|
313
|
+
txns, cursor = client.get_transactions(limit=50) # → (list[dict], cursor)
|
|
314
|
+
trades, cursor = client.get_account_trades(symbol="BTC-EUR", limit=100)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Order Payload Builders (Pure Functions)
|
|
320
|
+
|
|
321
|
+
Build payloads without making API calls. Useful for validation before submission.
|
|
322
|
+
|
|
323
|
+
> **Important**: The payload format uses nested `order_configuration` with `"market"` or `"limit"` key.
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
from revolut_x import (
|
|
327
|
+
OrderPayloadBuilder, build_market_order,
|
|
328
|
+
build_limit_order, build_maker_order, build_order,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Market order payload
|
|
332
|
+
payload = build_market_order("BTC-EUR", OrderSide.BUY, quote_size="100")
|
|
333
|
+
# → {"client_order_id": "...", "symbol": "BTC-EUR", "side": "buy",
|
|
334
|
+
# "order_configuration": {"market": {"quote_size": "100"}}}
|
|
335
|
+
|
|
336
|
+
# Limit order payload
|
|
337
|
+
payload = build_limit_order("BTC-EUR", OrderSide.SELL, "95000", base_size="0.001")
|
|
338
|
+
|
|
339
|
+
# Maker order payload (limit + post_only for 0% fee)
|
|
340
|
+
payload = build_maker_order("BTC-EUR", OrderSide.BUY, "94000", quote_size="50")
|
|
341
|
+
|
|
342
|
+
# Generic builder
|
|
343
|
+
payload = build_order("BTC-EUR", "buy", "limit", price="95000", base_size="0.001")
|
|
344
|
+
|
|
345
|
+
# Validate against exchange pair rules BEFORE sending
|
|
346
|
+
pair_rules = client.get_pair("BTC-EUR")
|
|
347
|
+
is_valid, errors = OrderPayloadBuilder.validate_against_pair_rules(payload, pair_rules)
|
|
348
|
+
# is_valid: bool, errors: list[str]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Fee Calculator
|
|
354
|
+
|
|
355
|
+
Maker fee: **0.00%**, Taker fee: **0.09%**
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from revolut_x import FeeCalculator, OrderSide
|
|
359
|
+
|
|
360
|
+
est = FeeCalculator.calculate(
|
|
361
|
+
OrderSide.BUY, price="95000", is_maker=False, quote_size="1000"
|
|
362
|
+
)
|
|
363
|
+
print(est.fee_amount) # Decimal, e.g. 0.9000
|
|
364
|
+
print(est.fee_rate_percent) # Decimal, e.g. 0.09
|
|
365
|
+
print(est.net_received) # Decimal — what you actually get
|
|
366
|
+
print(est.explanation) # human-readable multi-line summary
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### `FeeEstimate` fields
|
|
370
|
+
|
|
371
|
+
`side`, `order_type`, `is_maker`, `fee_rate_percent`, `trade_value_eur`, `estimated_base_qty`, `price`, `fee_amount`, `fee_currency`, `net_received`, `net_received_currency`, `explanation`
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Symbol Normalization
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
from revolut_x import normalize_symbol
|
|
379
|
+
|
|
380
|
+
normalize_symbol("btceur") # → "BTC-EUR"
|
|
381
|
+
normalize_symbol("BTC/EUR") # → "BTC-EUR"
|
|
382
|
+
normalize_symbol("btc-eur") # → "BTC-EUR"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Supported quote currencies for auto-detection: `EUR`, `USD`, `USDC`, `GBP`, `CZK`.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Maker Price Calculation
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
from revolut_x import calculate_maker_price, OrderSide
|
|
393
|
+
|
|
394
|
+
# BUY: price = best_bid - offset → enters order book below best bid
|
|
395
|
+
price = calculate_maker_price(
|
|
396
|
+
OrderSide.BUY, best_bid="80000.00", offset="0.10"
|
|
397
|
+
) # → Decimal("79999.90")
|
|
398
|
+
|
|
399
|
+
# SELL: price = best_ask + offset → enters order book above best ask
|
|
400
|
+
price = calculate_maker_price(
|
|
401
|
+
OrderSide.SELL, best_ask="80000.50", offset="0.10"
|
|
402
|
+
) # → Decimal("80000.60")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Error Handling
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from revolut_x import (
|
|
411
|
+
RevolutXError, AuthenticationError, RateLimitError,
|
|
412
|
+
ApiError, OrderValidationError, NetworkError,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
client.place_market_order(...)
|
|
417
|
+
except AuthenticationError as e:
|
|
418
|
+
print(e.hint) # human-readable suggestion
|
|
419
|
+
except RateLimitError as e:
|
|
420
|
+
print(e.retry_after_seconds) # Retry-After header value
|
|
421
|
+
print(e.endpoint)
|
|
422
|
+
except OrderValidationError as e:
|
|
423
|
+
print(e.errors) # list[str] of validation messages
|
|
424
|
+
except ApiError as e:
|
|
425
|
+
print(e.status_code, e.response_body, e.endpoint, e.method)
|
|
426
|
+
except NetworkError as e:
|
|
427
|
+
print(e.original_error) # underlying requests exception
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Exception hierarchy:
|
|
431
|
+
```
|
|
432
|
+
RevolutXError (base)
|
|
433
|
+
├── AuthenticationError → 401/403 or missing credentials (.hint)
|
|
434
|
+
├── RateLimitError → 429 after retries (.retry_after_seconds, .endpoint)
|
|
435
|
+
├── ApiError → other HTTP errors (.status_code, .response_body)
|
|
436
|
+
├── OrderValidationError → client-side validation (.errors: list[str])
|
|
437
|
+
│ also inherits ValueError
|
|
438
|
+
└── NetworkError → connection/timeout (.original_error)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Enums
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from revolut_x import OrderSide, OrderType, Interval, TimeInForce
|
|
447
|
+
|
|
448
|
+
OrderSide.BUY / OrderSide.SELL
|
|
449
|
+
OrderType.MARKET / OrderType.LIMIT
|
|
450
|
+
TimeInForce.GTC / TimeInForce.IOC
|
|
451
|
+
|
|
452
|
+
# Interval — candlestick resolution in minutes
|
|
453
|
+
Interval.MIN_1 # 1 Interval.HOUR_1 # 60 Interval.DAY_1 # 1440
|
|
454
|
+
Interval.MIN_5 # 5 Interval.HOUR_4 # 240 Interval.DAY_2 # 2880
|
|
455
|
+
Interval.MIN_15 # 15 Interval.DAY_4 # 5760
|
|
456
|
+
Interval.MIN_30 # 30 Interval.WEEK_1 # 10080 Interval.WEEK_2 # 20160
|
|
457
|
+
Interval.WEEK_4 # 40320
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Key Design Notes
|
|
463
|
+
|
|
464
|
+
- **Mixin architecture**: `RevolutXClient(MarketMixin, OrdersMixin, AccountMixin)` — all share `self._http`
|
|
465
|
+
- **Rate limiting**: built-in 0.85s delay + exponential backoff on 429
|
|
466
|
+
- **All API numeric values are strings** — use `Decimal` for precision, never `float`
|
|
467
|
+
- **Symbol format**: API uses `BASE/QUOTE` (e.g. `"BTC/EUR"`) but `normalize_symbol()` accepts `BTC-EUR`, `btceur`, etc.
|
|
468
|
+
- **Pagination**: `get_trades`, `get_historical_orders`, `get_transactions`, `get_account_trades` return `(items, next_cursor)` tuples; `iter_candles` / `iter_trades` auto-paginate
|
|
469
|
+
- **Order payload format**: nested `order_configuration` with `"market"` or `"limit"` key
|
|
470
|
+
- **Fee model**: Maker 0.00%, Taker 0.09%
|
|
471
|
+
|
|
472
|
+
For full type definitions, see [references/types.md](./references/types.md).
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Development & Documentation
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
# Development install (tests + dev tools)
|
|
480
|
+
pip install -e ".[dev]"
|
|
481
|
+
pytest # run test suite (43 tests)
|
|
482
|
+
|
|
483
|
+
# Documentation install & commands
|
|
484
|
+
pip install -e ".[docs]"
|
|
485
|
+
mkdocs serve # local live preview at http://127.0.0.1:8000
|
|
486
|
+
mkdocs build # build static HTML to site/
|
|
487
|
+
mkdocs gh-deploy # deploy static docs to GitHub Pages
|
|
488
|
+
|
|
489
|
+
# Online Docs: https://janprikryl1.github.io/revolut-x-python/
|
|
490
|
+
# GitHub Repo: https://github.com/janprikryl1/revolut-x-python
|
|
491
|
+
```
|