xtb-api-python 0.5.2__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/__init__.py +70 -0
- xtb_api/__main__.py +154 -0
- xtb_api/auth/__init__.py +5 -0
- xtb_api/auth/auth_manager.py +321 -0
- xtb_api/auth/browser_auth.py +316 -0
- xtb_api/auth/cas_client.py +543 -0
- xtb_api/client.py +444 -0
- xtb_api/exceptions.py +56 -0
- xtb_api/grpc/__init__.py +25 -0
- xtb_api/grpc/client.py +329 -0
- xtb_api/grpc/proto.py +239 -0
- xtb_api/grpc/types.py +14 -0
- xtb_api/instruments.py +132 -0
- xtb_api/py.typed +0 -0
- xtb_api/types/__init__.py +6 -0
- xtb_api/types/enums.py +92 -0
- xtb_api/types/instrument.py +45 -0
- xtb_api/types/trading.py +139 -0
- xtb_api/types/websocket.py +164 -0
- xtb_api/utils.py +62 -0
- xtb_api/ws/__init__.py +3 -0
- xtb_api/ws/parsers.py +161 -0
- xtb_api/ws/ws_client.py +905 -0
- xtb_api_python-0.5.2.dist-info/METADATA +257 -0
- xtb_api_python-0.5.2.dist-info/RECORD +28 -0
- xtb_api_python-0.5.2.dist-info/WHEEL +4 -0
- xtb_api_python-0.5.2.dist-info/entry_points.txt +2 -0
- xtb_api_python-0.5.2.dist-info/licenses/LICENSE +21 -0
xtb_api/ws/parsers.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Pure parser functions for XTB WebSocket API responses.
|
|
2
|
+
|
|
3
|
+
Each function takes raw elements (list of dicts from the CoreAPI subscription
|
|
4
|
+
response) and returns typed Pydantic models. No I/O, no side effects —
|
|
5
|
+
easy to unit-test with fixture data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from xtb_api.types.enums import Xs6Side
|
|
13
|
+
from xtb_api.types.instrument import InstrumentSearchResult, Quote
|
|
14
|
+
from xtb_api.types.trading import AccountBalance, PendingOrder, Position
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_balance(
|
|
18
|
+
elements: list[dict[str, Any]],
|
|
19
|
+
currency: str,
|
|
20
|
+
account_number: int,
|
|
21
|
+
) -> AccountBalance:
|
|
22
|
+
"""Parse balance from subscription elements.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
elements: Raw elements from getAndSubscribeElement response.
|
|
26
|
+
currency: Account currency code.
|
|
27
|
+
account_number: Account number.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Parsed AccountBalance (zeros if data is missing).
|
|
31
|
+
"""
|
|
32
|
+
if elements:
|
|
33
|
+
balance_data = (elements[0] or {}).get("value", {}).get("xtotalbalance")
|
|
34
|
+
if balance_data:
|
|
35
|
+
return AccountBalance(
|
|
36
|
+
balance=float(balance_data.get("balance", 0)),
|
|
37
|
+
equity=float(balance_data.get("equity", 0)),
|
|
38
|
+
free_margin=float(balance_data.get("freeMargin", 0)),
|
|
39
|
+
currency=currency,
|
|
40
|
+
account_number=account_number,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return AccountBalance(
|
|
44
|
+
balance=0.0,
|
|
45
|
+
equity=0.0,
|
|
46
|
+
free_margin=0.0,
|
|
47
|
+
currency=currency,
|
|
48
|
+
account_number=account_number,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_positions(elements: list[dict[str, Any]]) -> list[Position]:
|
|
53
|
+
"""Parse open trading positions from subscription elements."""
|
|
54
|
+
positions: list[Position] = []
|
|
55
|
+
|
|
56
|
+
for el in elements:
|
|
57
|
+
trade = (el or {}).get("value", {}).get("xcfdtrade")
|
|
58
|
+
if not trade:
|
|
59
|
+
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
|
+
|
|
82
|
+
return positions
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_orders(elements: list[dict[str, Any]]) -> list[PendingOrder]:
|
|
86
|
+
"""Parse pending (limit/stop) orders from subscription elements."""
|
|
87
|
+
orders: list[PendingOrder] = []
|
|
88
|
+
|
|
89
|
+
for el in elements:
|
|
90
|
+
trade = (el or {}).get("value", {}).get("xcfdtrade")
|
|
91
|
+
if not trade:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
side_val = int(trade.get("side", 0))
|
|
95
|
+
orders.append(
|
|
96
|
+
PendingOrder(
|
|
97
|
+
symbol=str(trade.get("symbol", "")),
|
|
98
|
+
instrument_id=int(trade["idQuote"]) if trade.get("idQuote") is not None else None,
|
|
99
|
+
volume=float(trade.get("volume", 0)),
|
|
100
|
+
price=float(trade.get("openPrice", 0)),
|
|
101
|
+
stop_loss=float(trade["sl"]) if trade.get("sl") and trade["sl"] != 0 else None,
|
|
102
|
+
take_profit=float(trade["tp"]) if trade.get("tp") and trade["tp"] != 0 else None,
|
|
103
|
+
side="buy" if side_val == Xs6Side.BUY else "sell",
|
|
104
|
+
order_id=str(trade["positionId"]) if trade.get("positionId") is not None else None,
|
|
105
|
+
order_type=str(trade.get("orderType", "")),
|
|
106
|
+
expiration=int(trade["expiration"]) if trade.get("expiration") is not None else None,
|
|
107
|
+
open_time=int(trade["openTime"]) if trade.get("openTime") is not None else None,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return orders
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_instruments(elements: list[dict[str, Any]]) -> list[InstrumentSearchResult]:
|
|
115
|
+
"""Parse instrument symbols from subscription elements."""
|
|
116
|
+
symbols: list[InstrumentSearchResult] = []
|
|
117
|
+
|
|
118
|
+
for el in elements:
|
|
119
|
+
sym = (el or {}).get("value", {}).get("xcfdsymbol")
|
|
120
|
+
if not sym:
|
|
121
|
+
continue
|
|
122
|
+
symbols.append(
|
|
123
|
+
InstrumentSearchResult(
|
|
124
|
+
symbol=str(sym.get("name", "")),
|
|
125
|
+
instrument_id=int(sym.get("instrumentId", sym.get("quoteId", 0))),
|
|
126
|
+
name=str(sym.get("description", sym.get("name", ""))),
|
|
127
|
+
description=str(sym.get("description", "")),
|
|
128
|
+
asset_class=str(sym.get("idAssetClass", "")),
|
|
129
|
+
symbol_key=f"{sym.get('idAssetClass')}_{sym.get('name')}_{sym.get('groupId', sym.get('quoteId'))}",
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return symbols
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_quote(elements: list[dict[str, Any]], symbol: str) -> Quote | None:
|
|
137
|
+
"""Parse a quote (bid/ask) from subscription elements.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
elements: Raw elements from tick subscription response.
|
|
141
|
+
symbol: Fallback symbol name if not present in data.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Parsed Quote, or None if no tick data found.
|
|
145
|
+
"""
|
|
146
|
+
if not elements:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
tick = (elements[0] or {}).get("value", {}).get("xcfdtick")
|
|
150
|
+
if not tick:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
return Quote(
|
|
154
|
+
symbol=str(tick.get("symbol", symbol)),
|
|
155
|
+
ask=float(tick.get("ask", 0)),
|
|
156
|
+
bid=float(tick.get("bid", 0)),
|
|
157
|
+
spread=float(tick.get("ask", 0)) - float(tick.get("bid", 0)),
|
|
158
|
+
high=float(tick["high"]) if tick.get("high") is not None else None,
|
|
159
|
+
low=float(tick["low"]) if tick.get("low") is not None else None,
|
|
160
|
+
time=int(tick["timestamp"]) if tick.get("timestamp") is not None else None,
|
|
161
|
+
)
|