xtb-api-python 0.5.2__py3-none-any.whl → 0.5.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xtb_api/ws/parsers.py +28 -23
- xtb_api/ws/ws_client.py +75 -9
- {xtb_api_python-0.5.2.dist-info → xtb_api_python-0.5.3.dist-info}/METADATA +26 -3
- {xtb_api_python-0.5.2.dist-info → xtb_api_python-0.5.3.dist-info}/RECORD +7 -7
- {xtb_api_python-0.5.2.dist-info → xtb_api_python-0.5.3.dist-info}/WHEEL +0 -0
- {xtb_api_python-0.5.2.dist-info → xtb_api_python-0.5.3.dist-info}/entry_points.txt +0 -0
- {xtb_api_python-0.5.2.dist-info → xtb_api_python-0.5.3.dist-info}/licenses/LICENSE +0 -0
xtb_api/ws/parsers.py
CHANGED
|
@@ -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
|
|
xtb_api/ws/ws_client.py
CHANGED
|
@@ -52,7 +52,7 @@ from xtb_api.ws.parsers import (
|
|
|
52
52
|
parse_balance,
|
|
53
53
|
parse_instruments,
|
|
54
54
|
parse_orders,
|
|
55
|
-
|
|
55
|
+
parse_position_trade,
|
|
56
56
|
parse_quote,
|
|
57
57
|
)
|
|
58
58
|
|
|
@@ -533,15 +533,81 @@ class XTBWebSocketClient:
|
|
|
533
533
|
|
|
534
534
|
return parse_balance(self._extract_elements(res), account.currency, account.accountNo)
|
|
535
535
|
|
|
536
|
-
async def get_positions(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
536
|
+
async def get_positions(
|
|
537
|
+
self,
|
|
538
|
+
*,
|
|
539
|
+
max_wait_ms: int = 5000,
|
|
540
|
+
quiet_ms: int = 500,
|
|
541
|
+
) -> list[Position]:
|
|
542
|
+
"""Get all open trading positions.
|
|
543
|
+
|
|
544
|
+
XTB's xStation5 CoreAPI returns position data via the push/events
|
|
545
|
+
channel (`status=1` with `eid=POSITIONS`), NOT via the normal
|
|
546
|
+
`reqId`-echoed response channel. So we subscribe, then collect the
|
|
547
|
+
burst of push events that follow, rather than awaiting a direct reply.
|
|
548
|
+
|
|
549
|
+
The collection window closes when either:
|
|
550
|
+
* `max_wait_ms` has elapsed since the subscription fired, OR
|
|
551
|
+
* `quiet_ms` has passed with no new position push (whichever first).
|
|
552
|
+
|
|
553
|
+
Deduplicates by `positionId` so a retriggered snapshot + update sequence
|
|
554
|
+
won't produce duplicates.
|
|
555
|
+
"""
|
|
556
|
+
if not self.is_connected or not self._ws:
|
|
557
|
+
raise XTBConnectionError("Not connected")
|
|
558
|
+
|
|
559
|
+
collected: dict[str, Position] = {}
|
|
560
|
+
loop = asyncio.get_running_loop()
|
|
561
|
+
last_push_ts = loop.time()
|
|
562
|
+
|
|
563
|
+
def on_position(trade: dict[str, Any]) -> None:
|
|
564
|
+
nonlocal last_push_ts
|
|
565
|
+
try:
|
|
566
|
+
pos = parse_position_trade(trade)
|
|
567
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
568
|
+
logger.warning("Failed to parse position push: %s", exc)
|
|
569
|
+
return
|
|
570
|
+
# Dedup by positionId; fall back to object id if missing.
|
|
571
|
+
key = pos.order_id or str(id(trade))
|
|
572
|
+
collected[key] = pos
|
|
573
|
+
last_push_ts = loop.time()
|
|
574
|
+
|
|
575
|
+
self.on("position", on_position)
|
|
576
|
+
try:
|
|
577
|
+
# Fire the subscribe; do NOT await a reqId response (XTB doesn't
|
|
578
|
+
# echo one for POSITIONS). The handler above captures the pushes.
|
|
579
|
+
req_id = self._next_req_id("getPositions")
|
|
580
|
+
request = {
|
|
581
|
+
"reqId": req_id,
|
|
582
|
+
"command": [
|
|
583
|
+
{
|
|
584
|
+
"CoreAPI": {
|
|
585
|
+
"endpoint": self._config.endpoint,
|
|
586
|
+
"accountId": self.account_id,
|
|
587
|
+
"getAndSubscribeElement": {"eid": SubscriptionEid.POSITIONS},
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
],
|
|
591
|
+
}
|
|
592
|
+
await self._ws.send(json.dumps(request))
|
|
543
593
|
|
|
544
|
-
|
|
594
|
+
start = loop.time()
|
|
595
|
+
max_wait_s = max_wait_ms / 1000.0
|
|
596
|
+
quiet_s = quiet_ms / 1000.0
|
|
597
|
+
while True:
|
|
598
|
+
await asyncio.sleep(0.05)
|
|
599
|
+
elapsed = loop.time() - start
|
|
600
|
+
if elapsed >= max_wait_s:
|
|
601
|
+
break
|
|
602
|
+
# Quiet-period exit only after we have at least one position —
|
|
603
|
+
# otherwise a slow server that hasn't sent anything yet would
|
|
604
|
+
# trigger the quiet-exit prematurely.
|
|
605
|
+
if collected and (loop.time() - last_push_ts) >= quiet_s:
|
|
606
|
+
break
|
|
607
|
+
finally:
|
|
608
|
+
self.off("position", on_position)
|
|
609
|
+
|
|
610
|
+
return list(collected.values())
|
|
545
611
|
|
|
546
612
|
async def get_orders(self) -> list[PendingOrder]:
|
|
547
613
|
"""Get all pending (limit/stop) orders."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtb-api-python
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
-
- **
|
|
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
|
|
|
@@ -19,10 +19,10 @@ xtb_api/types/instrument.py,sha256=uxGB3LrE1IgP3UeEMu9c50Mvqmplr3DmgUbLwjg4i14,9
|
|
|
19
19
|
xtb_api/types/trading.py,sha256=fGLGga-kMX7vDvE848H7lUnw-kHpTcqq6a8P25RDu2k,3033
|
|
20
20
|
xtb_api/types/websocket.py,sha256=66vZSC6hdlglO5EEqPSciBbtBVunGV0L-0pLiZwVrQo,3705
|
|
21
21
|
xtb_api/ws/__init__.py,sha256=6QNzj6rXiGWa9vU-o_IvVTqU7nDqPaYDJnTQhX-1pXY,106
|
|
22
|
-
xtb_api/ws/parsers.py,sha256=
|
|
23
|
-
xtb_api/ws/ws_client.py,sha256=
|
|
24
|
-
xtb_api_python-0.5.
|
|
25
|
-
xtb_api_python-0.5.
|
|
26
|
-
xtb_api_python-0.5.
|
|
27
|
-
xtb_api_python-0.5.
|
|
28
|
-
xtb_api_python-0.5.
|
|
22
|
+
xtb_api/ws/parsers.py,sha256=ipTkURuz-WF8ZgYOgWDSNBbUp1eYaNQScrQN6rUufDI,6397
|
|
23
|
+
xtb_api/ws/ws_client.py,sha256=qpZ3tiEgf2BIEvouK1xdGhrAvFw6_AwYzbREP5bHtlY,36768
|
|
24
|
+
xtb_api_python-0.5.3.dist-info/METADATA,sha256=PpVsMp3Z6JgKPMyRd4XBANA-ivBWIAYn79uzEqkIP0E,9918
|
|
25
|
+
xtb_api_python-0.5.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
26
|
+
xtb_api_python-0.5.3.dist-info/entry_points.txt,sha256=HhB6G6gQU-c43Y0YowXSqXXOTCd1CK8fvy5guFdzy0s,50
|
|
27
|
+
xtb_api_python-0.5.3.dist-info/licenses/LICENSE,sha256=fEnrQRlNl8P7GRU9YAaLjbNRfQ0qe0At5LRkCmM4j0I,1073
|
|
28
|
+
xtb_api_python-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|