xtb-api-python 0.5.2__tar.gz → 0.5.4__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 (50) hide show
  1. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/CHANGELOG.md +98 -0
  2. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/PKG-INFO +26 -3
  3. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/README.md +25 -2
  4. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/pyproject.toml +1 -1
  5. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/ws/parsers.py +28 -23
  6. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/ws/ws_client.py +8 -2
  7. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/.gitignore +0 -0
  8. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/LICENSE +0 -0
  9. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/examples/basic_usage.py +0 -0
  10. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/examples/grpc_trade.py +0 -0
  11. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/examples/live_quotes.py +0 -0
  12. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/__init__.py +0 -0
  13. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/__main__.py +0 -0
  14. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/auth/__init__.py +0 -0
  15. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/auth/auth_manager.py +0 -0
  16. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/auth/browser_auth.py +0 -0
  17. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/auth/cas_client.py +0 -0
  18. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/client.py +0 -0
  19. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/exceptions.py +0 -0
  20. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/grpc/__init__.py +0 -0
  21. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/grpc/client.py +0 -0
  22. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/grpc/proto.py +0 -0
  23. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/grpc/types.py +0 -0
  24. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/instruments.py +0 -0
  25. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/py.typed +0 -0
  26. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/types/__init__.py +0 -0
  27. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/types/enums.py +0 -0
  28. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/types/instrument.py +0 -0
  29. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/types/trading.py +0 -0
  30. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/types/websocket.py +0 -0
  31. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/utils.py +0 -0
  32. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/src/xtb_api/ws/__init__.py +0 -0
  33. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/__init__.py +0 -0
  34. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/conftest.py +0 -0
  35. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_auth.py +0 -0
  36. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_auth_manager.py +0 -0
  37. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_browser_auth.py +0 -0
  38. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_browser_auth_chromium_missing.py +0 -0
  39. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_cas_cookies.py +0 -0
  40. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_client.py +0 -0
  41. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_client_fill_price.py +0 -0
  42. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_client_volume_validation.py +0 -0
  43. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_doctor.py +0 -0
  44. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_exceptions.py +0 -0
  45. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_grpc_client.py +0 -0
  46. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_instruments.py +0 -0
  47. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_parsers.py +0 -0
  48. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_proto.py +0 -0
  49. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_version.py +0 -0
  50. {xtb_api_python-0.5.2 → xtb_api_python-0.5.4}/tests/test_ws_client.py +0 -0
@@ -1,6 +1,104 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.5.4 (2026-04-15)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **ws**: Revert get_positions to reqId-based send
9
+ ([#9](https://github.com/liskeee/xtb-api-unofficial-python/pull/9),
10
+ [`d197b08`](https://github.com/liskeee/xtb-api-unofficial-python/commit/d197b08350e57a46b82adee5dcb206b2d068b852))
11
+
12
+ v0.5.3's push-channel collection was based on a wrong diagnosis. Live API probing confirms the
13
+ xStation5 CoreAPI echoes getPositions on the NORMAL reqId response channel (status=0,
14
+ response=[...]), not as push events. The original implementation was correct.
15
+
16
+ The 30s timeouts observed on the original 0.5.2 bot were something else — probably container-level
17
+ networking / first-call timing — not a protocol mismatch. The wrong fix in 0.5.3 made the problem
18
+ worse because get_positions() started returning [] instantly, causing the downstream bot to
19
+ auto-close positions it mistakenly thought were gone from the broker.
20
+
21
+ Reverts the get_positions implementation to: res = await self.send("getPositions",
22
+ {"getAndSubscribeElement": {"eid": POSITIONS}}, timeout_ms=30000) return
23
+ parse_positions(self._extract_elements(res))
24
+
25
+ Keeps `parse_position_trade` helper in parsers.py — harmless refactor. Removes
26
+ tests/test_get_positions_push.py — tested wrong behavior.
27
+
28
+ Verified against real XTB account: all 5 open positions returned in ~1-2s via normal reqId response.
29
+
30
+ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
31
+
32
+
33
+ ## v0.5.3 (2026-04-15)
34
+
35
+ ### Bug Fixes
36
+
37
+ - **ws**: Consume POSITIONS push channel in get_positions
38
+ ([`b0156bc`](https://github.com/liskeee/xtb-api-unofficial-python/commit/b0156bcc328630fb5e80952d87794780a8e30c73))
39
+
40
+ XTB's xStation5 CoreAPI does not echo a reqId-correlated response for the `getPositions` RPC;
41
+ position data arrives exclusively via status=1 push events with eid=POSITIONS. The previous
42
+ implementation awaited a regular reqId-matched response and timed out after 30s on every call
43
+ against a live account.
44
+
45
+ Fix: subscribe and consume the push burst. Register a one-shot 'position' handler, fire the
46
+ subscribe RPC (do not await its reply), and collect pushed events until either a quiet period (no
47
+ new position for 500 ms) or a max-wait ceiling (5 s) closes the window. Dedup by positionId so a
48
+ retriggered snapshot does not duplicate entries.
49
+
50
+ parse_positions is refactored around a new parse_position_trade(trade) helper so the push handler
51
+ and the (still-supported) element-list path share the single source of truth for xcfdtrade →
52
+ Position mapping.
53
+
54
+ Tests: 8 new tests cover parser extraction, burst collection, dedup, empty-on-timeout, listener
55
+ cleanup, and the not-connected guard.
56
+
57
+ Fixes the downstream xtb-investor-pro bot's "get_positions failed" timeouts observed immediately
58
+ after the broker abstraction merge.
59
+
60
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
61
+
62
+ - **ws**: Drop unused parse_positions import
63
+ ([`63b3263`](https://github.com/liskeee/xtb-api-unofficial-python/commit/63b3263b64ef76ad7759b26a70e8d6ec26e6c9b3))
64
+
65
+ ### Code Style
66
+
67
+ - Ruff format test_get_positions_push.py
68
+ ([`6e376a8`](https://github.com/liskeee/xtb-api-unofficial-python/commit/6e376a8cc1578d388f8469da3b49b02dee021841))
69
+
70
+ ### Documentation
71
+
72
+ - Add design spec for v0.5 docs refresh
73
+ ([`c55d839`](https://github.com/liskeee/xtb-api-unofficial-python/commit/c55d839534ff39f5da55ec920fe6457eb82fb458))
74
+
75
+ Captures the README/CONTRIBUTING/SECURITY drift between v0.4.x docs and the v0.5.2 surface (XTBAuth,
76
+ InstrumentRegistry, fill-price polling, volume guard, inlined publish jobs, supported-versions
77
+ table).
78
+
79
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80
+
81
+ - Add implementation plan for v0.5 docs refresh
82
+ ([`0a4e3dd`](https://github.com/liskeee/xtb-api-unofficial-python/commit/0a4e3ddab03ad6cc87b4789cf39c772d85ca6d75))
83
+
84
+ Pairs with the design spec at docs/superpowers/specs/2026-04-15-docs-refresh-v0.5-design.md and
85
+ tracks the four tasks executed in 97bbada.
86
+
87
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88
+
89
+ - Refresh README, CONTRIBUTING, SECURITY to v0.5 state
90
+ ([`97bbada`](https://github.com/liskeee/xtb-api-unofficial-python/commit/97bbada41434dacbf3139af6e26b70ad8a632ba6))
91
+
92
+ - README: document XTBAuth alias, InstrumentRegistry, post-fill TradeResult.price, and the
93
+ volume-validation guard added in v0.5.0; drop stale 11,888+ symbol count. - CONTRIBUTING: trim
94
+ "before enabling on master" framing now that PSR is live; list both semantic-release.yml and
95
+ release.yml as required Trusted Publishers (PyPI matches the OIDC token's workflow filename
96
+ exactly, and v0.5.2 inlined the publish jobs into semantic-release.yml). - SECURITY: bump
97
+ supported-versions table from 0.3.x to 0.5.x.
98
+
99
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100
+
101
+
4
102
  ## v0.5.2 (2026-04-15)
5
103
 
6
104
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtb-api-python
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Python port of unofficial XTB xStation5 API client
5
5
  Project-URL: Homepage, https://github.com/liskeee/xtb-api-python
6
6
  Project-URL: Repository, https://github.com/liskeee/xtb-api-python
@@ -52,7 +52,9 @@ Python client for the XTB xStation5 trading platform. Dead-simple API that handl
52
52
  - **2FA Support** — Automatic TOTP handling when `totp_secret` is provided
53
53
  - **Real-time Data** — Live quotes, positions, balance via WebSocket push events
54
54
  - **Trading** — Buy/sell market orders with SL/TP via gRPC-web
55
- - **11,888+ Instruments** — Full symbol search with caching
55
+ - **Volume-Validated Orders** — `buy`/`sell` reject `volume < 1` before touching the wire
56
+ - **Persistent Instrument Cache** — `InstrumentRegistry` caches symbol → instrument-ID lookups to disk
57
+ - **Full Symbol Search** — Search and resolve all listed instruments with caching
56
58
  - **Modern Python** — async/await, Pydantic models, strict typing, Python 3.12+
57
59
 
58
60
  ## Requirements
@@ -129,6 +131,12 @@ async def main():
129
131
  # Search instruments
130
132
  results = await client.search_instrument("Apple")
131
133
 
134
+ # Persistent instrument cache (avoids re-fetching the full symbol list)
135
+ from xtb_api import InstrumentRegistry
136
+ registry = InstrumentRegistry("~/.xtb_instruments.json")
137
+ matched = await registry.populate(client, ["AAPL.US", "EURUSD"])
138
+ instrument_id = registry.get("AAPL.US") # int | None
139
+
132
140
  # Trading (USE WITH CAUTION!)
133
141
  result = await client.buy("AAPL.US", volume=1, stop_loss=150.0, take_profit=200.0)
134
142
  print(f"Order: {result.order_id}")
@@ -162,6 +170,8 @@ await client.sell("CIG.PL", volume=100, options=TradeOptions(
162
170
  ))
163
171
  ```
164
172
 
173
+ > `TradeResult.price` is populated by polling open positions immediately after fill. If the position cannot be located within the poll window, `price` remains `None`.
174
+
165
175
  ## API Reference
166
176
 
167
177
  ### `XTBClient`
@@ -194,6 +204,18 @@ await client.sell("CIG.PL", volume=100, options=TradeOptions(
194
204
  | `account_server` | No | `"XS-real1"` | gRPC account server |
195
205
  | `auto_reconnect` | No | `True` | Auto-reconnect on disconnect |
196
206
 
207
+ ### `InstrumentRegistry`
208
+
209
+ Persistent symbol → instrument-ID cache, stored as JSON.
210
+
211
+ | Method | Returns | Description |
212
+ |--------|---------|-------------|
213
+ | `InstrumentRegistry(path)` | — | Load (or create) the JSON cache at `path` |
214
+ | `get(symbol)` | `int \| None` | Cached instrument ID for `symbol`, or `None` |
215
+ | `set(symbol, id)` | `None` | Cache one mapping and persist immediately |
216
+ | `populate(client, symbols)` | `dict[str, int]` | Download the full symbol list via `client`, match requested `symbols` (case-insensitive, dot-less fallback), persist, return new matches |
217
+ | `ids` | `dict[str, int]` | Read-only copy of the full cache |
218
+
197
219
  ### WebSocket URLs
198
220
 
199
221
  | Environment | URL |
@@ -212,8 +234,9 @@ ws = client.ws
212
234
  # gRPC client (available after first trade)
213
235
  grpc = client.grpc_client
214
236
 
215
- # Auth manager
237
+ # Auth manager (accessor, or import the public alias)
216
238
  auth = client.auth
239
+ from xtb_api import XTBAuth # public alias for the AuthManager class
217
240
  tgt = await auth.get_tgt()
218
241
  ```
219
242
 
@@ -16,7 +16,9 @@ Python client for the XTB xStation5 trading platform. Dead-simple API that handl
16
16
  - **2FA Support** — Automatic TOTP handling when `totp_secret` is provided
17
17
  - **Real-time Data** — Live quotes, positions, balance via WebSocket push events
18
18
  - **Trading** — Buy/sell market orders with SL/TP via gRPC-web
19
- - **11,888+ Instruments** — Full symbol search with caching
19
+ - **Volume-Validated Orders** — `buy`/`sell` reject `volume < 1` before touching the wire
20
+ - **Persistent Instrument Cache** — `InstrumentRegistry` caches symbol → instrument-ID lookups to disk
21
+ - **Full Symbol Search** — Search and resolve all listed instruments with caching
20
22
  - **Modern Python** — async/await, Pydantic models, strict typing, Python 3.12+
21
23
 
22
24
  ## Requirements
@@ -93,6 +95,12 @@ async def main():
93
95
  # Search instruments
94
96
  results = await client.search_instrument("Apple")
95
97
 
98
+ # Persistent instrument cache (avoids re-fetching the full symbol list)
99
+ from xtb_api import InstrumentRegistry
100
+ registry = InstrumentRegistry("~/.xtb_instruments.json")
101
+ matched = await registry.populate(client, ["AAPL.US", "EURUSD"])
102
+ instrument_id = registry.get("AAPL.US") # int | None
103
+
96
104
  # Trading (USE WITH CAUTION!)
97
105
  result = await client.buy("AAPL.US", volume=1, stop_loss=150.0, take_profit=200.0)
98
106
  print(f"Order: {result.order_id}")
@@ -126,6 +134,8 @@ await client.sell("CIG.PL", volume=100, options=TradeOptions(
126
134
  ))
127
135
  ```
128
136
 
137
+ > `TradeResult.price` is populated by polling open positions immediately after fill. If the position cannot be located within the poll window, `price` remains `None`.
138
+
129
139
  ## API Reference
130
140
 
131
141
  ### `XTBClient`
@@ -158,6 +168,18 @@ await client.sell("CIG.PL", volume=100, options=TradeOptions(
158
168
  | `account_server` | No | `"XS-real1"` | gRPC account server |
159
169
  | `auto_reconnect` | No | `True` | Auto-reconnect on disconnect |
160
170
 
171
+ ### `InstrumentRegistry`
172
+
173
+ Persistent symbol → instrument-ID cache, stored as JSON.
174
+
175
+ | Method | Returns | Description |
176
+ |--------|---------|-------------|
177
+ | `InstrumentRegistry(path)` | — | Load (or create) the JSON cache at `path` |
178
+ | `get(symbol)` | `int \| None` | Cached instrument ID for `symbol`, or `None` |
179
+ | `set(symbol, id)` | `None` | Cache one mapping and persist immediately |
180
+ | `populate(client, symbols)` | `dict[str, int]` | Download the full symbol list via `client`, match requested `symbols` (case-insensitive, dot-less fallback), persist, return new matches |
181
+ | `ids` | `dict[str, int]` | Read-only copy of the full cache |
182
+
161
183
  ### WebSocket URLs
162
184
 
163
185
  | Environment | URL |
@@ -176,8 +198,9 @@ ws = client.ws
176
198
  # gRPC client (available after first trade)
177
199
  grpc = client.grpc_client
178
200
 
179
- # Auth manager
201
+ # Auth manager (accessor, or import the public alias)
180
202
  auth = client.auth
203
+ from xtb_api import XTBAuth # public alias for the AuthManager class
181
204
  tgt = await auth.get_tgt()
182
205
  ```
183
206
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xtb-api-python"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "Python port of unofficial XTB xStation5 API client"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -49,36 +49,41 @@ def parse_balance(
49
49
  )
50
50
 
51
51
 
52
+ def parse_position_trade(trade: dict[str, Any]) -> Position:
53
+ """Convert a single `xcfdtrade` dict (as pushed on the POSITIONS
54
+ subscription) into a `Position`.
55
+
56
+ Callers that receive the wrapped `{value: {xcfdtrade: ...}}` shape
57
+ should use `parse_positions()` instead.
58
+ """
59
+ side_val = int(trade.get("side", 0))
60
+ return Position(
61
+ symbol=str(trade.get("symbol", "")),
62
+ instrument_id=int(trade["idQuote"]) if trade.get("idQuote") is not None else None,
63
+ volume=float(trade.get("volume", 0)),
64
+ current_price=0.0,
65
+ open_price=float(trade.get("openPrice", 0)),
66
+ stop_loss=float(trade["sl"]) if trade.get("sl") and trade["sl"] != 0 else None,
67
+ take_profit=float(trade["tp"]) if trade.get("tp") and trade["tp"] != 0 else None,
68
+ profit_percent=0.0,
69
+ profit_net=0.0,
70
+ swap=float(trade["swap"]) if trade.get("swap") is not None else None,
71
+ side="buy" if side_val == Xs6Side.BUY else "sell",
72
+ order_id=str(trade["positionId"]) if trade.get("positionId") is not None else None,
73
+ commission=float(trade["commission"]) if trade.get("commission") is not None else None,
74
+ margin=float(trade["margin"]) if trade.get("margin") is not None else None,
75
+ open_time=int(trade["openTime"]) if trade.get("openTime") is not None else None,
76
+ )
77
+
78
+
52
79
  def parse_positions(elements: list[dict[str, Any]]) -> list[Position]:
53
80
  """Parse open trading positions from subscription elements."""
54
81
  positions: list[Position] = []
55
-
56
82
  for el in elements:
57
83
  trade = (el or {}).get("value", {}).get("xcfdtrade")
58
84
  if not trade:
59
85
  continue
60
-
61
- side_val = int(trade.get("side", 0))
62
- positions.append(
63
- Position(
64
- symbol=str(trade.get("symbol", "")),
65
- instrument_id=int(trade["idQuote"]) if trade.get("idQuote") is not None else None,
66
- volume=float(trade.get("volume", 0)),
67
- current_price=0.0,
68
- open_price=float(trade.get("openPrice", 0)),
69
- stop_loss=float(trade["sl"]) if trade.get("sl") and trade["sl"] != 0 else None,
70
- take_profit=float(trade["tp"]) if trade.get("tp") and trade["tp"] != 0 else None,
71
- profit_percent=0.0,
72
- profit_net=0.0,
73
- swap=float(trade["swap"]) if trade.get("swap") is not None else None,
74
- side="buy" if side_val == Xs6Side.BUY else "sell",
75
- order_id=str(trade["positionId"]) if trade.get("positionId") is not None else None,
76
- commission=float(trade["commission"]) if trade.get("commission") is not None else None,
77
- margin=float(trade["margin"]) if trade.get("margin") is not None else None,
78
- open_time=int(trade["openTime"]) if trade.get("openTime") is not None else None,
79
- )
80
- )
81
-
86
+ positions.append(parse_position_trade(trade))
82
87
  return positions
83
88
 
84
89
 
@@ -534,13 +534,19 @@ class XTBWebSocketClient:
534
534
  return parse_balance(self._extract_elements(res), account.currency, account.accountNo)
535
535
 
536
536
  async def get_positions(self) -> list[Position]:
537
- """Get all open trading positions."""
537
+ """Get all open trading positions.
538
+
539
+ XTB's xStation5 CoreAPI echoes the position snapshot on the normal
540
+ reqId-correlated response channel (not the push channel, despite the
541
+ `getAndSubscribeElement` keyword). The subscription it sets up also
542
+ emits per-position updates via `_emit("position", ...)`; those are
543
+ orthogonal to the snapshot returned here.
544
+ """
538
545
  res = await self.send(
539
546
  "getPositions",
540
547
  {"getAndSubscribeElement": {"eid": SubscriptionEid.POSITIONS}},
541
548
  timeout_ms=30000,
542
549
  )
543
-
544
550
  return parse_positions(self._extract_elements(res))
545
551
 
546
552
  async def get_orders(self) -> list[PendingOrder]:
File without changes