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.
Files changed (40) hide show
  1. revolut_x_python-1.0.0/.agents/skills/revolut-x-library/SKILL.md +491 -0
  2. revolut_x_python-1.0.0/.agents/skills/revolut-x-library/references/types.md +289 -0
  3. revolut_x_python-1.0.0/CHANGELOG.md +216 -0
  4. revolut_x_python-1.0.0/LICENSE +21 -0
  5. revolut_x_python-1.0.0/MANIFEST.in +18 -0
  6. revolut_x_python-1.0.0/PKG-INFO +164 -0
  7. revolut_x_python-1.0.0/README.md +120 -0
  8. revolut_x_python-1.0.0/examples/01_public_market_data.py +31 -0
  9. revolut_x_python-1.0.0/examples/02_download_candles_stream.py +23 -0
  10. revolut_x_python-1.0.0/examples/03_account_balances_and_history.py +33 -0
  11. revolut_x_python-1.0.0/examples/04_place_and_cancel_orders.py +46 -0
  12. revolut_x_python-1.0.0/examples/05_fee_calculator_and_validation.py +30 -0
  13. revolut_x_python-1.0.0/examples/06_smart_maker_orders.py +76 -0
  14. revolut_x_python-1.0.0/examples/README.md +34 -0
  15. revolut_x_python-1.0.0/pyproject.toml +72 -0
  16. revolut_x_python-1.0.0/requirements.txt +5 -0
  17. revolut_x_python-1.0.0/setup.cfg +4 -0
  18. revolut_x_python-1.0.0/src/revolut_x/__init__.py +103 -0
  19. revolut_x_python-1.0.0/src/revolut_x/_auth.py +139 -0
  20. revolut_x_python-1.0.0/src/revolut_x/_http.py +270 -0
  21. revolut_x_python-1.0.0/src/revolut_x/_version.py +10 -0
  22. revolut_x_python-1.0.0/src/revolut_x/account.py +203 -0
  23. revolut_x_python-1.0.0/src/revolut_x/client.py +170 -0
  24. revolut_x_python-1.0.0/src/revolut_x/exceptions.py +147 -0
  25. revolut_x_python-1.0.0/src/revolut_x/helpers.py +540 -0
  26. revolut_x_python-1.0.0/src/revolut_x/market.py +469 -0
  27. revolut_x_python-1.0.0/src/revolut_x/orders.py +522 -0
  28. revolut_x_python-1.0.0/src/revolut_x/py.typed +0 -0
  29. revolut_x_python-1.0.0/src/revolut_x/types.py +397 -0
  30. revolut_x_python-1.0.0/src/revolut_x_python.egg-info/PKG-INFO +164 -0
  31. revolut_x_python-1.0.0/src/revolut_x_python.egg-info/SOURCES.txt +38 -0
  32. revolut_x_python-1.0.0/src/revolut_x_python.egg-info/dependency_links.txt +1 -0
  33. revolut_x_python-1.0.0/src/revolut_x_python.egg-info/requires.txt +15 -0
  34. revolut_x_python-1.0.0/src/revolut_x_python.egg-info/top_level.txt +1 -0
  35. revolut_x_python-1.0.0/tests/__init__.py +1 -0
  36. revolut_x_python-1.0.0/tests/test_auth.py +200 -0
  37. revolut_x_python-1.0.0/tests/test_helpers.py +299 -0
  38. revolut_x_python-1.0.0/tests/test_maker_strategy.py +188 -0
  39. revolut_x_python-1.0.0/tests/test_market.py +191 -0
  40. 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
+ ```