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.
Files changed (29) hide show
  1. {fxsocket-0.4.0 → fxsocket-0.5.0}/PKG-INFO +31 -2
  2. {fxsocket-0.4.0 → fxsocket-0.5.0}/README.md +29 -0
  3. fxsocket-0.5.0/src/fxsocket/_version.py +1 -0
  4. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/enums.py +7 -1
  5. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/models.py +42 -2
  6. {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_stream.py +64 -1
  7. {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_terminal.py +48 -0
  8. fxsocket-0.4.0/src/fxsocket/_version.py +0 -1
  9. {fxsocket-0.4.0 → fxsocket-0.5.0}/.github/workflows/ci.yml +0 -0
  10. {fxsocket-0.4.0 → fxsocket-0.5.0}/.github/workflows/publish.yml +0 -0
  11. {fxsocket-0.4.0 → fxsocket-0.5.0}/.gitignore +0 -0
  12. {fxsocket-0.4.0 → fxsocket-0.5.0}/LICENSE +0 -0
  13. {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/manage_accounts.py +0 -0
  14. {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/stream_quotes.py +0 -0
  15. {fxsocket-0.4.0 → fxsocket-0.5.0}/examples/terminal_rest.py +0 -0
  16. {fxsocket-0.4.0 → fxsocket-0.5.0}/pyproject.toml +0 -0
  17. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/__init__.py +0 -0
  18. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/_http.py +0 -0
  19. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/client.py +0 -0
  20. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/config.py +0 -0
  21. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/errors.py +0 -0
  22. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/management.py +0 -0
  23. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/py.typed +0 -0
  24. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/__init__.py +0 -0
  25. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/client.py +0 -0
  26. {fxsocket-0.4.0 → fxsocket-0.5.0}/src/fxsocket/terminal/stream.py +0 -0
  27. {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_errors.py +0 -0
  28. {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_management.py +0 -0
  29. {fxsocket-0.4.0 → fxsocket-0.5.0}/tests/test_private_servers.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: fxsocket
3
- Version: 0.4.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
- ``deal`` / ``position`` are 0 on MT4 (no per-deal model); ``entry`` is the
557
- deal direction (compare against :class:`fxsocket.DealEntry`).
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
- "comment": "",
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