fxsocket 0.4.0__tar.gz → 0.5.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.
- {fxsocket-0.4.0 → fxsocket-0.5.0}/PKG-INFO +31 -2
- {fxsocket-0.4.0 → fxsocket-0.5.0}/README.md +29 -0
- fxsocket-0.5.0/src/fxsocket/_version.py +1 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/enums.py +7 -1
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/models.py +42 -2
- {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_stream.py +64 -1
- {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_terminal.py +48 -0
- fxsocket-0.4.0/src/fxsocket/_version.py +0 -1
- {fxsocket-0.4.0 → fxsocket-0.5.0}/.github/workflows/ci.yml +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/.github/workflows/publish.yml +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/.gitignore +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/LICENSE +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/manage_accounts.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/stream_quotes.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/terminal_rest.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/pyproject.toml +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/__init__.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/client.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/config.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/errors.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/management.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/client.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/stream.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_errors.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_management.py +0 -0
- {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_private_servers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: fxsocket
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python SDK for the FxSocket API — MT4/MT5 account management, trading, and real-time streaming.
|
|
5
5
|
Project-URL: Homepage, https://fxsocket.com
|
|
6
6
|
Project-URL: Documentation, https://api.fxsocket.com/v1/docs
|
|
@@ -229,6 +229,35 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
229
229
|
print(event.data.bid, event.data.ask)
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
### Trade events
|
|
233
|
+
|
|
234
|
+
A `TradeUpdate` carries the full deal: `commission`, `swap`, `magic` and a
|
|
235
|
+
real `comment` alongside `profit` (bridges MT5 0.12+ / MT4 0.11+; zero on
|
|
236
|
+
older pods). Event-only P&L accounting is `data.net_profit`
|
|
237
|
+
(`profit + commission + swap`).
|
|
238
|
+
|
|
239
|
+
Correlate the `In` and `Out` events of one round-trip through
|
|
240
|
+
`data.position` — on MT5, `Out` deals carry `magic=0` / `comment=""` unless
|
|
241
|
+
the closing request set them (platform behavior, not a bridge gap), so
|
|
242
|
+
position id is the reliable join key. On MT4, `deal` is always 0 and
|
|
243
|
+
`position` equals the order ticket. The same id appears as `position` in
|
|
244
|
+
`order_history()` rows (bridges MT5 0.14+ / MT4 0.13+) and as
|
|
245
|
+
`position_id` in `position_history()`.
|
|
246
|
+
|
|
247
|
+
If the bridge can't fully enrich an event in time it sets
|
|
248
|
+
`data.degraded=True`: identifiers, `symbol`, `type`, `volume` and `price`
|
|
249
|
+
are still trustworthy, but `entry` is `"Unknown"` and the cost fields are
|
|
250
|
+
zeroed — reconcile that deal via `order_history()`.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
async for event in s:
|
|
254
|
+
match event:
|
|
255
|
+
case TradeUpdate() as t if t.data.degraded:
|
|
256
|
+
reconcile_later(t.data.position) # costs/entry unreliable
|
|
257
|
+
case TradeUpdate() as t if t.data.entry == DealEntry.OUT:
|
|
258
|
+
print(t.data.position, "closed, net", t.data.net_profit)
|
|
259
|
+
```
|
|
260
|
+
|
|
232
261
|
## Errors
|
|
233
262
|
|
|
234
263
|
Every failure raises a subclass of `fxsocket.FxSocketError`:
|
|
@@ -202,6 +202,35 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
202
202
|
print(event.data.bid, event.data.ask)
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
### Trade events
|
|
206
|
+
|
|
207
|
+
A `TradeUpdate` carries the full deal: `commission`, `swap`, `magic` and a
|
|
208
|
+
real `comment` alongside `profit` (bridges MT5 0.12+ / MT4 0.11+; zero on
|
|
209
|
+
older pods). Event-only P&L accounting is `data.net_profit`
|
|
210
|
+
(`profit + commission + swap`).
|
|
211
|
+
|
|
212
|
+
Correlate the `In` and `Out` events of one round-trip through
|
|
213
|
+
`data.position` — on MT5, `Out` deals carry `magic=0` / `comment=""` unless
|
|
214
|
+
the closing request set them (platform behavior, not a bridge gap), so
|
|
215
|
+
position id is the reliable join key. On MT4, `deal` is always 0 and
|
|
216
|
+
`position` equals the order ticket. The same id appears as `position` in
|
|
217
|
+
`order_history()` rows (bridges MT5 0.14+ / MT4 0.13+) and as
|
|
218
|
+
`position_id` in `position_history()`.
|
|
219
|
+
|
|
220
|
+
If the bridge can't fully enrich an event in time it sets
|
|
221
|
+
`data.degraded=True`: identifiers, `symbol`, `type`, `volume` and `price`
|
|
222
|
+
are still trustworthy, but `entry` is `"Unknown"` and the cost fields are
|
|
223
|
+
zeroed — reconcile that deal via `order_history()`.
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
async for event in s:
|
|
227
|
+
match event:
|
|
228
|
+
case TradeUpdate() as t if t.data.degraded:
|
|
229
|
+
reconcile_later(t.data.position) # costs/entry unreliable
|
|
230
|
+
case TradeUpdate() as t if t.data.entry == DealEntry.OUT:
|
|
231
|
+
print(t.data.position, "closed, net", t.data.net_profit)
|
|
232
|
+
```
|
|
233
|
+
|
|
205
234
|
## Errors
|
|
206
235
|
|
|
207
236
|
Every failure raises a subclass of `fxsocket.FxSocketError`:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -103,11 +103,17 @@ class OrderKind(str, Enum):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
class DealEntry(str, Enum):
|
|
106
|
-
"""Direction of a deal in trade history / the ``trades`` stream.
|
|
106
|
+
"""Direction of a deal in trade history / the ``trades`` stream.
|
|
107
|
+
|
|
108
|
+
``UNKNOWN`` appears only on degraded ``trades``-stream frames, where the
|
|
109
|
+
bridge could not resolve the deal direction in time — see
|
|
110
|
+
``TradeEventData.degraded``.
|
|
111
|
+
"""
|
|
107
112
|
|
|
108
113
|
IN = "In"
|
|
109
114
|
OUT = "Out"
|
|
110
115
|
IN_OUT = "InOut"
|
|
116
|
+
UNKNOWN = "Unknown"
|
|
111
117
|
|
|
112
118
|
|
|
113
119
|
class HealthStatus(str, Enum):
|
|
@@ -205,10 +205,20 @@ class HistoryTrade(_Camel):
|
|
|
205
205
|
|
|
206
206
|
On MT4 this is one row per closed *order* (no per-deal granularity);
|
|
207
207
|
``order`` aliases the ticket and ``entry`` is constant.
|
|
208
|
+
|
|
209
|
+
``position`` groups the rows of one round-trip: on MT5 it is the deal's
|
|
210
|
+
``DEAL_POSITION_ID`` — the ``In`` and ``Out`` rows share it, and it
|
|
211
|
+
equals the ``trades``-stream events' ``position`` and
|
|
212
|
+
:attr:`PositionTrade.position_id` — so an exit row alone identifies the
|
|
213
|
+
position it closed even though MT5 exits usually carry ``magic=0`` /
|
|
214
|
+
``comment=""``. On MT4 it equals the order ticket. 0 on pods older than
|
|
215
|
+
bridge MT5 0.14 / MT4 0.13. (Netting-account caveat: a reversal
|
|
216
|
+
``InOut`` row reports the position it belongs to *after* processing.)
|
|
208
217
|
"""
|
|
209
218
|
|
|
210
219
|
ticket: int
|
|
211
220
|
order: int
|
|
221
|
+
position: int = 0
|
|
212
222
|
symbol: str
|
|
213
223
|
type: str
|
|
214
224
|
entry: str
|
|
@@ -553,8 +563,29 @@ class HealthChecks(_Camel):
|
|
|
553
563
|
class TradeEventData(_Camel):
|
|
554
564
|
"""A trade transaction pushed on the ``trades`` stream.
|
|
555
565
|
|
|
556
|
-
``
|
|
557
|
-
|
|
566
|
+
``entry`` is the deal direction (compare against
|
|
567
|
+
:class:`fxsocket.DealEntry`; ``"Unknown"`` appears only on degraded
|
|
568
|
+
frames). ``commission`` / ``swap`` / ``magic`` (and a real ``comment`` on
|
|
569
|
+
MT5) arrive on bridges MT5 0.12+ / MT4 0.11+ and default to 0 before
|
|
570
|
+
that; a deal's net P&L is ``profit + commission + swap``
|
|
571
|
+
(:attr:`net_profit`).
|
|
572
|
+
|
|
573
|
+
Platform semantics:
|
|
574
|
+
|
|
575
|
+
* **MT5** — ``Out`` deals carry ``magic=0`` / ``comment=""`` unless the
|
|
576
|
+
closing request set them (a platform property, not a bridge gap).
|
|
577
|
+
Correlate ``In``/``Out`` through ``position``, which is present on
|
|
578
|
+
every event.
|
|
579
|
+
* **MT4** — orders keep their magic/comment for the whole lifecycle, so
|
|
580
|
+
both ``In`` and ``Out`` events carry them; ``deal`` is always 0 and
|
|
581
|
+
``position`` equals the order ticket.
|
|
582
|
+
|
|
583
|
+
``degraded=True`` (bridges MT5 0.13+ / MT4 0.12+; structurally always
|
|
584
|
+
``False`` on MT4) means the bridge could not fully enrich the event in
|
|
585
|
+
time: the identifiers, ``symbol``, ``type``, ``volume`` and ``price`` are
|
|
586
|
+
trustworthy, but ``entry`` is ``"Unknown"`` and ``profit`` /
|
|
587
|
+
``commission`` / ``swap`` / ``magic`` / ``comment`` are zeroed —
|
|
588
|
+
reconcile the deal via ``GET /OrderHistory``.
|
|
558
589
|
"""
|
|
559
590
|
|
|
560
591
|
deal: int
|
|
@@ -566,8 +597,17 @@ class TradeEventData(_Camel):
|
|
|
566
597
|
volume: float
|
|
567
598
|
price: float
|
|
568
599
|
profit: float
|
|
600
|
+
commission: float = 0.0
|
|
601
|
+
swap: float = 0.0
|
|
602
|
+
magic: int = 0
|
|
569
603
|
comment: str
|
|
570
604
|
time: str
|
|
605
|
+
degraded: bool = False
|
|
606
|
+
|
|
607
|
+
@property
|
|
608
|
+
def net_profit(self) -> float:
|
|
609
|
+
"""Deal P&L including costs: ``profit + commission + swap``."""
|
|
610
|
+
return self.profit + self.commission + self.swap
|
|
571
611
|
|
|
572
612
|
|
|
573
613
|
class TerminalStatusData(_Camel):
|
|
@@ -13,6 +13,7 @@ from fxsocket import (
|
|
|
13
13
|
AccountUpdate,
|
|
14
14
|
AsyncStream,
|
|
15
15
|
Bar,
|
|
16
|
+
DealEntry,
|
|
16
17
|
PositionsUpdate,
|
|
17
18
|
Stream,
|
|
18
19
|
StreamWarning,
|
|
@@ -157,12 +158,19 @@ def test_parse_event_all_types() -> None:
|
|
|
157
158
|
"volume": 0.1,
|
|
158
159
|
"price": 1.0,
|
|
159
160
|
"profit": 0.0,
|
|
160
|
-
"
|
|
161
|
+
"commission": -0.04,
|
|
162
|
+
"swap": 0.0,
|
|
163
|
+
"magic": 1000999,
|
|
164
|
+
"comment": "CN_9999_8888",
|
|
161
165
|
"time": "t",
|
|
166
|
+
"degraded": False,
|
|
162
167
|
},
|
|
163
168
|
}
|
|
164
169
|
)
|
|
165
170
|
assert isinstance(trade, TradeUpdate) and trade.data.entry == "In"
|
|
171
|
+
assert trade.data.commission == -0.04 and trade.data.magic == 1000999
|
|
172
|
+
assert trade.data.degraded is False
|
|
173
|
+
assert trade.data.net_profit == pytest.approx(-0.04)
|
|
166
174
|
|
|
167
175
|
term = parse_event(
|
|
168
176
|
{
|
|
@@ -192,6 +200,61 @@ def test_parse_event_all_types() -> None:
|
|
|
192
200
|
assert isinstance(parse_event({"type": "subscriptions", "data": []}), Subscriptions)
|
|
193
201
|
|
|
194
202
|
|
|
203
|
+
def test_parse_trade_pre_012_bridge_defaults() -> None:
|
|
204
|
+
"""Bridges older than MT5 0.12 / MT4 0.11 omit the enrichment fields."""
|
|
205
|
+
trade = parse_event(
|
|
206
|
+
{
|
|
207
|
+
"type": "trade",
|
|
208
|
+
"data": {
|
|
209
|
+
"deal": 9,
|
|
210
|
+
"order": 10,
|
|
211
|
+
"position": 10,
|
|
212
|
+
"symbol": "EURUSD",
|
|
213
|
+
"type": "Buy",
|
|
214
|
+
"entry": "In",
|
|
215
|
+
"volume": 0.1,
|
|
216
|
+
"price": 1.0,
|
|
217
|
+
"profit": 0.0,
|
|
218
|
+
"comment": "",
|
|
219
|
+
"time": "t",
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
assert isinstance(trade, TradeUpdate)
|
|
224
|
+
assert trade.data.commission == 0.0 and trade.data.swap == 0.0
|
|
225
|
+
assert trade.data.magic == 0 and trade.data.degraded is False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_parse_trade_degraded_frame() -> None:
|
|
229
|
+
"""A degraded frame: identifiers/direction real, costs zeroed, entry Unknown."""
|
|
230
|
+
trade = parse_event(
|
|
231
|
+
{
|
|
232
|
+
"type": "trade",
|
|
233
|
+
"data": {
|
|
234
|
+
"deal": 9,
|
|
235
|
+
"order": 10,
|
|
236
|
+
"position": 10,
|
|
237
|
+
"symbol": "EURUSD",
|
|
238
|
+
"type": "Buy",
|
|
239
|
+
"entry": "Unknown",
|
|
240
|
+
"volume": 0.1,
|
|
241
|
+
"price": 1.0,
|
|
242
|
+
"profit": 0.0,
|
|
243
|
+
"commission": 0.0,
|
|
244
|
+
"swap": 0.0,
|
|
245
|
+
"magic": 0,
|
|
246
|
+
"comment": "",
|
|
247
|
+
"time": "2026-08-19T18:53:41.000Z",
|
|
248
|
+
"degraded": True,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
assert isinstance(trade, TradeUpdate)
|
|
253
|
+
assert trade.data.degraded is True
|
|
254
|
+
assert trade.data.entry == DealEntry.UNKNOWN
|
|
255
|
+
assert trade.data.symbol == "EURUSD" and trade.data.position == 10
|
|
256
|
+
|
|
257
|
+
|
|
195
258
|
# --------------------------------------------------------------------------- #
|
|
196
259
|
# Validation (no connection needed — raised before send)
|
|
197
260
|
# --------------------------------------------------------------------------- #
|
|
@@ -693,6 +693,54 @@ def test_server_timezone_parses() -> None:
|
|
|
693
693
|
assert isinstance(tz.server_time, str)
|
|
694
694
|
|
|
695
695
|
|
|
696
|
+
@respx.mock
|
|
697
|
+
def test_order_history_parses_position() -> None:
|
|
698
|
+
# Second row mimics a pod older than bridge MT5 0.14 / MT4 0.13 (no
|
|
699
|
+
# ``position`` yet) — must default to 0, not fail validation.
|
|
700
|
+
respx.get(f"{TERM}/OrderHistory").mock(
|
|
701
|
+
return_value=httpx.Response(
|
|
702
|
+
200,
|
|
703
|
+
json=[
|
|
704
|
+
{
|
|
705
|
+
"ticket": 14541780,
|
|
706
|
+
"order": 13173527,
|
|
707
|
+
"position": 13173524,
|
|
708
|
+
"symbol": "EURUSD",
|
|
709
|
+
"type": "Sell",
|
|
710
|
+
"entry": "Out",
|
|
711
|
+
"volume": 0.01,
|
|
712
|
+
"price": 1.16635,
|
|
713
|
+
"profit": -0.02,
|
|
714
|
+
"commission": -0.04,
|
|
715
|
+
"swap": 0.0,
|
|
716
|
+
"magic": 0,
|
|
717
|
+
"comment": "",
|
|
718
|
+
"time": "2026-08-19T18:53:47.000Z",
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"ticket": 14541777,
|
|
722
|
+
"order": 13173524,
|
|
723
|
+
"symbol": "EURUSD",
|
|
724
|
+
"type": "Buy",
|
|
725
|
+
"entry": "In",
|
|
726
|
+
"volume": 0.01,
|
|
727
|
+
"price": 1.16637,
|
|
728
|
+
"profit": 0.0,
|
|
729
|
+
"commission": -0.04,
|
|
730
|
+
"swap": 0.0,
|
|
731
|
+
"magic": 1000999,
|
|
732
|
+
"comment": "CN_9999_8888",
|
|
733
|
+
"time": "2026-08-19T18:53:41.000Z",
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
with _term() as t:
|
|
739
|
+
rows = t.order_history()
|
|
740
|
+
assert rows[0].position == 13173524
|
|
741
|
+
assert rows[1].position == 0
|
|
742
|
+
|
|
743
|
+
|
|
696
744
|
@respx.mock
|
|
697
745
|
def test_position_history_sends_dates_and_parses() -> None:
|
|
698
746
|
route = respx.get(f"{TERM}/PositionHistory").mock(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|