fxsocket 0.3.0__tar.gz → 0.4.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.3.0 → fxsocket-0.4.0}/PKG-INFO +19 -3
  2. {fxsocket-0.3.0 → fxsocket-0.4.0}/README.md +18 -2
  3. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/__init__.py +10 -0
  4. fxsocket-0.4.0/src/fxsocket/_version.py +1 -0
  5. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/models.py +103 -2
  6. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/client.py +56 -0
  7. {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_private_servers.py +2 -2
  8. {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_terminal.py +154 -0
  9. fxsocket-0.3.0/src/fxsocket/_version.py +0 -1
  10. {fxsocket-0.3.0 → fxsocket-0.4.0}/.github/workflows/ci.yml +0 -0
  11. {fxsocket-0.3.0 → fxsocket-0.4.0}/.github/workflows/publish.yml +0 -0
  12. {fxsocket-0.3.0 → fxsocket-0.4.0}/.gitignore +0 -0
  13. {fxsocket-0.3.0 → fxsocket-0.4.0}/LICENSE +0 -0
  14. {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/manage_accounts.py +0 -0
  15. {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/stream_quotes.py +0 -0
  16. {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/terminal_rest.py +0 -0
  17. {fxsocket-0.3.0 → fxsocket-0.4.0}/pyproject.toml +0 -0
  18. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/_http.py +0 -0
  19. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/client.py +0 -0
  20. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/config.py +0 -0
  21. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/enums.py +0 -0
  22. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/errors.py +0 -0
  23. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/management.py +0 -0
  24. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/py.typed +0 -0
  25. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/__init__.py +0 -0
  26. {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/stream.py +0 -0
  27. {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_errors.py +0 -0
  28. {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_management.py +0 -0
  29. {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fxsocket
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -42,8 +42,9 @@ interfaces.
42
42
  - **Account management** — link, list, fetch, and disconnect MT4/MT5 accounts.
43
43
  - **Private servers** — list your dedicated hosting servers and manage the
44
44
  accounts on them.
45
- - **Trading** — market & pending orders, modify, close, plus margin/profit calculators.
46
- - **Market data** — quotes, symbol specifications, OHLC history, account state & info.
45
+ - **Trading** — market & pending orders, modify, close, close-all, plus margin/profit calculators.
46
+ - **Market data** — quotes, symbol specifications (incl. commission rules & trading
47
+ sessions), OHLC history, account state & info.
47
48
  - **Live streaming** — ticks, bars, account, positions, trades, and terminal status
48
49
  over WebSocket, with automatic reconnect + subscription replay.
49
50
  - **Sync *and* async** — `Client` / `AsyncClient`, method-for-method mirrors.
@@ -157,6 +158,21 @@ if res.is_effective: # applied now, or already in effect
157
158
  ...
158
159
  ```
159
160
 
161
+ There's also a panic button. `close_all()` closes every open position in one
162
+ trade-EA pass — optionally filtered by `symbol` and/or `magic` (`magic=0`
163
+ matches manually-opened orders), and `delete_pending=True` also deletes
164
+ matching pending orders. It returns a `CloseAllSummary` with per-ticket
165
+ results. On a 504 the pass *continues inside the terminal* — check
166
+ `opened_orders()` before acting again rather than re-sending:
167
+
168
+ ```python
169
+ summary = term.close_all(symbol="EURUSD", delete_pending=True)
170
+ if summary.failed:
171
+ for r in summary.results:
172
+ if not r.success:
173
+ print(r.ticket, r.retcode, r.retcode_description)
174
+ ```
175
+
160
176
  Inputs are validated client-side before they're sent. One guard worth knowing:
161
177
  in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
162
178
  it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
@@ -15,8 +15,9 @@ interfaces.
15
15
  - **Account management** — link, list, fetch, and disconnect MT4/MT5 accounts.
16
16
  - **Private servers** — list your dedicated hosting servers and manage the
17
17
  accounts on them.
18
- - **Trading** — market & pending orders, modify, close, plus margin/profit calculators.
19
- - **Market data** — quotes, symbol specifications, OHLC history, account state & info.
18
+ - **Trading** — market & pending orders, modify, close, close-all, plus margin/profit calculators.
19
+ - **Market data** — quotes, symbol specifications (incl. commission rules & trading
20
+ sessions), OHLC history, account state & info.
20
21
  - **Live streaming** — ticks, bars, account, positions, trades, and terminal status
21
22
  over WebSocket, with automatic reconnect + subscription replay.
22
23
  - **Sync *and* async** — `Client` / `AsyncClient`, method-for-method mirrors.
@@ -130,6 +131,21 @@ if res.is_effective: # applied now, or already in effect
130
131
  ...
131
132
  ```
132
133
 
134
+ There's also a panic button. `close_all()` closes every open position in one
135
+ trade-EA pass — optionally filtered by `symbol` and/or `magic` (`magic=0`
136
+ matches manually-opened orders), and `delete_pending=True` also deletes
137
+ matching pending orders. It returns a `CloseAllSummary` with per-ticket
138
+ results. On a 504 the pass *continues inside the terminal* — check
139
+ `opened_orders()` before acting again rather than re-sending:
140
+
141
+ ```python
142
+ summary = term.close_all(symbol="EURUSD", delete_pending=True)
143
+ if summary.failed:
144
+ for r in summary.results:
145
+ if not r.success:
146
+ print(r.ticket, r.retcode, r.retcode_description)
147
+ ```
148
+
133
149
  Inputs are validated client-side before they're sent. One guard worth knowing:
134
150
  in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
135
151
  it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
@@ -42,6 +42,10 @@ from .models import (
42
42
  AccountInfo,
43
43
  AccountSummary,
44
44
  Candle,
45
+ CloseAllResult,
46
+ CloseAllSummary,
47
+ CommissionRule,
48
+ CommissionTier,
45
49
  Health,
46
50
  HealthChecks,
47
51
  HistoryTrade,
@@ -57,6 +61,7 @@ from .models import (
57
61
  SymbolInfo,
58
62
  TerminalStatusData,
59
63
  TradeEventData,
64
+ TradingSession,
60
65
  )
61
66
  from .terminal import (
62
67
  AccountUpdate,
@@ -112,8 +117,13 @@ __all__ = [
112
117
  "ServerTimezone",
113
118
  "Quote",
114
119
  "SymbolInfo",
120
+ "CommissionRule",
121
+ "CommissionTier",
122
+ "TradingSession",
115
123
  "Candle",
116
124
  "OrderResult",
125
+ "CloseAllResult",
126
+ "CloseAllSummary",
117
127
  "MarginCalc",
118
128
  "ProfitCalc",
119
129
  "Health",
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  from datetime import datetime
23
23
 
24
- from pydantic import BaseModel, ConfigDict
24
+ from pydantic import BaseModel, ConfigDict, Field
25
25
  from pydantic.alias_generators import to_camel
26
26
 
27
27
  from .enums import OrderOutcome, Platform, TradingStatus
@@ -271,8 +271,65 @@ class Quote(_Camel):
271
271
  volume: int
272
272
 
273
273
 
274
+ class CommissionTier(_Camel):
275
+ """One tier of a commission rule — a value and the volume/turnover range
276
+ it applies to.
277
+
278
+ Enum-like fields (``mode``, ``volume_type``) carry the raw MQL5 constant
279
+ names (e.g. ``SYMBOL_COMMISSION_MODE_MONEY``) so nothing is lost in
280
+ translation. ``range_to == 0`` means unbounded; ``min_value`` /
281
+ ``max_value`` cap the charged amount (0 = no cap).
282
+ """
283
+
284
+ mode: str
285
+ volume_type: str
286
+ value: float
287
+ min_value: float
288
+ max_value: float
289
+ range_from: float
290
+ range_to: float
291
+ currency: str
292
+
293
+
294
+ class CommissionRule(_Camel):
295
+ """One broker commission rule for a symbol, as configured server-side
296
+ (MT5 ``SymbolInfoCommissions``).
297
+
298
+ A symbol can carry several rules; each has its own tiers. The mode fields
299
+ carry the MQL5 ``ENUM_SYMBOL_COMMISSION_*`` constant names verbatim.
300
+ """
301
+
302
+ currency: str
303
+ range_mode: str
304
+ charge_mode: str
305
+ entry_mode: str
306
+ direction_mode: str
307
+ profit_mode: str
308
+ tiers: list[CommissionTier] = []
309
+
310
+
311
+ class TradingSession(_Camel):
312
+ """One trading-session window of a symbol, in *broker server time*.
313
+
314
+ ``day`` uses the MQL ``ENUM_DAY_OF_WEEK`` constant names (``SUNDAY`` …
315
+ ``SATURDAY``); times are ``HH:MM`` where ``24:00`` means end of day, so a
316
+ 24-hour market shows ``00:00``–``24:00``.
317
+ """
318
+
319
+ day: str
320
+ from_: str = Field(alias="from")
321
+ to: str
322
+
323
+
274
324
  class SymbolInfo(_Camel):
275
- """Contract specification for a symbol (``GET /SymbolInfo``)."""
325
+ """Contract specification for a symbol (``GET /SymbolInfo``).
326
+
327
+ ``commissions`` are the broker's commission rules straight from the
328
+ server's symbol specification; ``sessions`` are the per-weekday trading
329
+ windows in broker server time. Both default to empty on pods older than
330
+ bridge 0.10 (and ``commissions`` also when the broker publishes none or
331
+ the terminal predates the API — build 6060+).
332
+ """
276
333
 
277
334
  symbol: str
278
335
  description: str
@@ -295,6 +352,8 @@ class SymbolInfo(_Camel):
295
352
  currency_base: str
296
353
  currency_profit: str
297
354
  currency_margin: str
355
+ commissions: list[CommissionRule] = []
356
+ sessions: list[TradingSession] = []
298
357
 
299
358
 
300
359
  class Candle(_Camel):
@@ -360,6 +419,39 @@ class OrderResult(_Camel):
360
419
  return self.success or self.is_no_change
361
420
 
362
421
 
422
+ class CloseAllResult(_Camel):
423
+ """One per-ticket outcome of a ``/CloseAll`` pass.
424
+
425
+ ``kind`` is ``"position"`` (closed) or ``"pending"`` (deleted).
426
+ """
427
+
428
+ ticket: int
429
+ kind: str
430
+ success: bool
431
+ retcode: int
432
+ retcode_description: str
433
+
434
+ @property
435
+ def is_pending(self) -> bool:
436
+ """True when this row is a deleted pending order (vs. a closed position)."""
437
+ return self.kind.lower() == "pending"
438
+
439
+
440
+ class CloseAllSummary(_Camel):
441
+ """Reply of ``POST /CloseAll`` — every matched position (and pending
442
+ order, when ``delete_pending`` was set) with its close/delete outcome.
443
+
444
+ ``requested`` is how many orders the filters matched and were attempted;
445
+ ``closed`` how many attempts the broker accepted; ``failed`` how many it
446
+ rejected — inspect ``results`` for the per-ticket retcodes.
447
+ """
448
+
449
+ requested: int
450
+ closed: int
451
+ failed: int
452
+ results: list[CloseAllResult] = []
453
+
454
+
363
455
  class MarginCalc(_Camel):
364
456
  """Required margin for a hypothetical order (``GET /OrderCalcMargin``)."""
365
457
 
@@ -411,8 +503,17 @@ class AccountHealth(_Camel):
411
503
 
412
504
 
413
505
  class BridgeHealth(_Camel):
506
+ """Bridge section of ``/status``.
507
+
508
+ ``trade_ea_heartbeat_age_ms`` is how long ago the trade EA last made
509
+ dispatcher progress (``-1`` = never registered, or a pod older than
510
+ bridge 0.10). A large age while ``trade_ea_ready`` is still ``True``
511
+ means the EA is blocked in a long dealer call or dead — worth alerting on.
512
+ """
513
+
414
514
  version: str = ""
415
515
  trade_ea_ready: bool = False
516
+ trade_ea_heartbeat_age_ms: int = -1
416
517
  symbols_synced: bool = False
417
518
 
418
519
 
@@ -46,6 +46,7 @@ from ..models import (
46
46
  AccountInfo,
47
47
  AccountSummary,
48
48
  Candle,
49
+ CloseAllSummary,
49
50
  Health,
50
51
  HealthChecks,
51
52
  HistoryTrade,
@@ -234,6 +235,20 @@ def _order_send_body(
234
235
  )
235
236
 
236
237
 
238
+ def _close_all_body(
239
+ *, symbol: str | None, magic: int | None, delete_pending: bool
240
+ ) -> dict[str, Any]:
241
+ # magic=0 is a real filter (manually-opened orders) — only None is omitted.
242
+ # An empty dict is a valid body: it means "close everything".
243
+ return _clean(
244
+ {
245
+ "symbol": symbol,
246
+ "magic": magic,
247
+ "deletePending": True if delete_pending else None,
248
+ }
249
+ )
250
+
251
+
237
252
  def _order_modify_body(
238
253
  *,
239
254
  ticket: int,
@@ -481,6 +496,32 @@ class TerminalClient:
481
496
  self._http.request("POST", "/OrderClose", json=body)
482
497
  )
483
498
 
499
+ def close_all(
500
+ self,
501
+ *,
502
+ symbol: str | None = None,
503
+ magic: int | None = None,
504
+ delete_pending: bool = False,
505
+ ) -> CloseAllSummary:
506
+ """Close every open position in one trade-EA pass (``POST /CloseAll``).
507
+
508
+ Optionally filtered by ``symbol`` and/or ``magic`` (``magic=0``
509
+ matches manually-opened orders; ``None`` means no filter);
510
+ ``delete_pending=True`` also deletes matching pending orders. The
511
+ reply carries a per-ticket result for every attempted close/delete.
512
+
513
+ Positions opened while the pass is running are not covered, and on a
514
+ 504 (:class:`~fxsocket.TerminalTimeoutError`) the pass *continues to
515
+ completion inside the terminal* — check :meth:`opened_orders` before
516
+ acting again rather than re-sending.
517
+ """
518
+ body = _close_all_body(
519
+ symbol=symbol, magic=magic, delete_pending=delete_pending
520
+ )
521
+ return CloseAllSummary.model_validate(
522
+ self._http.request("POST", "/CloseAll", json=body)
523
+ )
524
+
484
525
  # -- health ------------------------------------------------------------ #
485
526
 
486
527
  def status(self) -> Health:
@@ -732,6 +773,21 @@ class AsyncTerminalClient:
732
773
  await self._http.request("POST", "/OrderClose", json=body)
733
774
  )
734
775
 
776
+ async def close_all(
777
+ self,
778
+ *,
779
+ symbol: str | None = None,
780
+ magic: int | None = None,
781
+ delete_pending: bool = False,
782
+ ) -> CloseAllSummary:
783
+ """Async mirror of :meth:`TerminalClient.close_all`."""
784
+ body = _close_all_body(
785
+ symbol=symbol, magic=magic, delete_pending=delete_pending
786
+ )
787
+ return CloseAllSummary.model_validate(
788
+ await self._http.request("POST", "/CloseAll", json=body)
789
+ )
790
+
735
791
  async def status(self) -> Health:
736
792
  return Health.model_validate(await self._http.request("GET", "/status"))
737
793
 
@@ -26,8 +26,8 @@ SERVER_ACCOUNT = {
26
26
  "server": "ICMarkets-Demo",
27
27
  "login": 7001,
28
28
  "status": "ready",
29
- "rest_url": f"https://159.223.244.125/22222222-2222-2222-2222-222222222222",
30
- "ws_url": f"wss://159.223.244.125/22222222-2222-2222-2222-222222222222/ws",
29
+ "rest_url": "https://159.223.244.125/22222222-2222-2222-2222-222222222222",
30
+ "ws_url": "wss://159.223.244.125/22222222-2222-2222-2222-222222222222/ws",
31
31
  "trade_ea_symbol": "",
32
32
  "created_at": "2026-07-16T08:00:00Z",
33
33
  }
@@ -212,6 +212,22 @@ def test_status_nested_health() -> None:
212
212
  assert h.terminal.ping_ms == 108
213
213
  assert h.account.logged_in is True
214
214
  assert h.bridge.trade_ea_ready is True
215
+ # Pre-0.10 bridges omit the heartbeat — defaults to -1 (never/unknown).
216
+ assert h.bridge.trade_ea_heartbeat_age_ms == -1
217
+
218
+
219
+ def test_bridge_health_heartbeat_parses() -> None:
220
+ from fxsocket.models import BridgeHealth
221
+
222
+ b = BridgeHealth.model_validate(
223
+ {
224
+ "version": "0.10.0",
225
+ "tradeEaReady": True,
226
+ "tradeEaHeartbeatAgeMs": 8,
227
+ "symbolsSynced": True,
228
+ }
229
+ )
230
+ assert b.trade_ea_heartbeat_age_ms == 8
215
231
 
216
232
 
217
233
  # --------------------------------------------------------------------------- #
@@ -430,6 +446,76 @@ def test_order_modify_omits_unset_fields() -> None:
430
446
  }
431
447
 
432
448
 
449
+ @respx.mock
450
+ def test_close_all_sends_filters_and_parses() -> None:
451
+ route = respx.post(f"{TERM}/CloseAll").mock(
452
+ return_value=httpx.Response(
453
+ 200,
454
+ json={
455
+ "requested": 2,
456
+ "closed": 1,
457
+ "failed": 1,
458
+ "results": [
459
+ {
460
+ "ticket": 100,
461
+ "kind": "position",
462
+ "success": True,
463
+ "retcode": 10009,
464
+ "retcodeDescription": "Done",
465
+ },
466
+ {
467
+ "ticket": 101,
468
+ "kind": "pending",
469
+ "success": False,
470
+ "retcode": 10006,
471
+ "retcodeDescription": "Rejected",
472
+ },
473
+ ],
474
+ },
475
+ )
476
+ )
477
+ with _term() as t:
478
+ summary = t.close_all(symbol="EURUSD", magic=0, delete_pending=True)
479
+ # magic=0 is a real filter (manual orders) and must be sent, not dropped.
480
+ assert json.loads(route.calls.last.request.content) == {
481
+ "symbol": "EURUSD",
482
+ "magic": 0,
483
+ "deletePending": True,
484
+ }
485
+ assert (summary.requested, summary.closed, summary.failed) == (2, 1, 1)
486
+ assert [r.is_pending for r in summary.results] == [False, True]
487
+ assert summary.results[1].retcode_description == "Rejected"
488
+
489
+
490
+ @respx.mock
491
+ def test_close_all_no_filters_sends_empty_json_body() -> None:
492
+ # /CloseAll requires a JSON body; with no filters it must be exactly {}.
493
+ route = respx.post(f"{TERM}/CloseAll").mock(
494
+ return_value=httpx.Response(
495
+ 200, json={"requested": 0, "closed": 0, "failed": 0, "results": []}
496
+ )
497
+ )
498
+ with _term() as t:
499
+ summary = t.close_all()
500
+ req = route.calls.last.request
501
+ assert json.loads(req.content) == {}
502
+ assert req.headers["content-type"] == "application/json"
503
+ assert summary.requested == 0 and summary.results == []
504
+
505
+
506
+ @respx.mock
507
+ def test_close_all_504_maps_to_timeout() -> None:
508
+ # The pass continues inside the terminal after a 504 — the SDK must
509
+ # surface the timeout (never retry), so callers re-check /OpenedOrders.
510
+ respx.post(f"{TERM}/CloseAll").mock(
511
+ return_value=httpx.Response(
512
+ 504, json={"error": "MRPC_TIMEOUT", "message": "timed out", "command_id": 3}
513
+ )
514
+ )
515
+ with _term() as t, pytest.raises(TerminalTimeoutError):
516
+ t.close_all()
517
+
518
+
433
519
  # --------------------------------------------------------------------------- #
434
520
  # Health probes — 503 carries a body, not an error
435
521
  # --------------------------------------------------------------------------- #
@@ -552,6 +638,46 @@ def test_symbol_info_parses_camelcase() -> None:
552
638
  assert si.tick_size == 0.00001
553
639
  assert si.volume_min == 0.01
554
640
  assert si.currency_base == "EUR"
641
+ # Pods older than bridge 0.10 omit commissions/sessions — default empty.
642
+ assert si.commissions == [] and si.sessions == []
643
+
644
+
645
+ def test_symbol_info_commissions_and_sessions_parse() -> None:
646
+ from fxsocket import CommissionRule, TradingSession
647
+
648
+ rule = CommissionRule.model_validate(
649
+ {
650
+ "currency": "USD",
651
+ "rangeMode": "SYMBOL_COMMISSION_RANGE_VOLUME",
652
+ "chargeMode": "SYMBOL_COMMISSION_CHARGE_INSTANT",
653
+ "entryMode": "SYMBOL_COMMISSION_ENTRY_INOUT",
654
+ "directionMode": "SYMBOL_COMMISSION_DIRECTION_BOTH",
655
+ "profitMode": "SYMBOL_COMMISSION_PROFIT_ALL",
656
+ "tiers": [
657
+ {
658
+ "mode": "SYMBOL_COMMISSION_MONEY_DEPOSIT",
659
+ "volumeType": "SYMBOL_COMMISSION_VOLUME_TYPE_VOLUME",
660
+ "value": 3.5,
661
+ "minValue": 0.0,
662
+ "maxValue": 0.0,
663
+ "rangeFrom": 0.0,
664
+ "rangeTo": 1000000.0,
665
+ "currency": "USD",
666
+ }
667
+ ],
668
+ }
669
+ )
670
+ assert rule.range_mode == "SYMBOL_COMMISSION_RANGE_VOLUME"
671
+ assert rule.tiers[0].value == 3.5
672
+ assert rule.tiers[0].range_to == 1000000.0
673
+
674
+ # `from` is a Python keyword — the wire field maps to `from_`.
675
+ session = TradingSession.model_validate(
676
+ {"day": "FRIDAY", "from": "00:01", "to": "23:57"}
677
+ )
678
+ assert session.day == "FRIDAY"
679
+ assert session.from_ == "00:01"
680
+ assert session.to == "23:57"
555
681
 
556
682
 
557
683
  @respx.mock
@@ -721,3 +847,31 @@ async def test_async_order_send_body_and_validation() -> None:
721
847
  "volume": 0.2,
722
848
  "expertId": 7,
723
849
  }
850
+
851
+
852
+ @respx.mock
853
+ async def test_async_close_all() -> None:
854
+ route = respx.post(f"{TERM}/CloseAll").mock(
855
+ return_value=httpx.Response(
856
+ 200,
857
+ json={
858
+ "requested": 1,
859
+ "closed": 1,
860
+ "failed": 0,
861
+ "results": [
862
+ {
863
+ "ticket": 100,
864
+ "kind": "position",
865
+ "success": True,
866
+ "retcode": 10009,
867
+ "retcodeDescription": "Done",
868
+ }
869
+ ],
870
+ },
871
+ )
872
+ )
873
+ async with AsyncClient(api_key="fxs_live_k") as fx:
874
+ term = fx.terminal(_account())
875
+ summary = await term.close_all(symbol="EURUSD")
876
+ assert json.loads(route.calls.last.request.content) == {"symbol": "EURUSD"}
877
+ assert summary.closed == 1 and summary.results[0].success
@@ -1 +0,0 @@
1
- __version__ = "0.3.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