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/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
+ )