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.
- {fxsocket-0.3.0 → fxsocket-0.4.0}/PKG-INFO +19 -3
- {fxsocket-0.3.0 → fxsocket-0.4.0}/README.md +18 -2
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/__init__.py +10 -0
- fxsocket-0.4.0/src/fxsocket/_version.py +1 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/models.py +103 -2
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/client.py +56 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_private_servers.py +2 -2
- {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_terminal.py +154 -0
- fxsocket-0.3.0/src/fxsocket/_version.py +0 -1
- {fxsocket-0.3.0 → fxsocket-0.4.0}/.github/workflows/ci.yml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/.github/workflows/publish.yml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/.gitignore +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/LICENSE +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/manage_accounts.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/stream_quotes.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/examples/terminal_rest.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/pyproject.toml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/client.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/config.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/enums.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/errors.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/management.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/src/fxsocket/terminal/stream.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_errors.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.4.0}/tests/test_management.py +0 -0
- {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
|
+
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
|
|
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
|
|
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":
|
|
30
|
-
"ws_url":
|
|
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
|
|
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
|