fxsocket 0.3.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.3.0 → fxsocket-0.5.0}/PKG-INFO +49 -4
- {fxsocket-0.3.0 → fxsocket-0.5.0}/README.md +47 -2
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/__init__.py +10 -0
- fxsocket-0.5.0/src/fxsocket/_version.py +1 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/enums.py +7 -1
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/models.py +145 -4
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/terminal/client.py +56 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/tests/test_private_servers.py +2 -2
- {fxsocket-0.3.0 → fxsocket-0.5.0}/tests/test_stream.py +64 -1
- {fxsocket-0.3.0 → fxsocket-0.5.0}/tests/test_terminal.py +202 -0
- fxsocket-0.3.0/src/fxsocket/_version.py +0 -1
- {fxsocket-0.3.0 → fxsocket-0.5.0}/.github/workflows/ci.yml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/.github/workflows/publish.yml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/.gitignore +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/LICENSE +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/examples/manage_accounts.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/examples/stream_quotes.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/examples/terminal_rest.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/pyproject.toml +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/client.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/config.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/errors.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/management.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/src/fxsocket/terminal/stream.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/tests/test_errors.py +0 -0
- {fxsocket-0.3.0 → fxsocket-0.5.0}/tests/test_management.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
|
|
@@ -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
|
|
@@ -213,6 +229,35 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
213
229
|
print(event.data.bid, event.data.ask)
|
|
214
230
|
```
|
|
215
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
|
+
|
|
216
261
|
## Errors
|
|
217
262
|
|
|
218
263
|
Every failure raises a subclass of `fxsocket.FxSocketError`:
|
|
@@ -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
|
|
@@ -186,6 +202,35 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
186
202
|
print(event.data.bid, event.data.ask)
|
|
187
203
|
```
|
|
188
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
|
+
|
|
189
234
|
## Errors
|
|
190
235
|
|
|
191
236
|
Every failure raises a subclass of `fxsocket.FxSocketError`:
|
|
@@ -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.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):
|
|
@@ -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
|
|
@@ -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
|
|
@@ -271,8 +281,65 @@ class Quote(_Camel):
|
|
|
271
281
|
volume: int
|
|
272
282
|
|
|
273
283
|
|
|
284
|
+
class CommissionTier(_Camel):
|
|
285
|
+
"""One tier of a commission rule — a value and the volume/turnover range
|
|
286
|
+
it applies to.
|
|
287
|
+
|
|
288
|
+
Enum-like fields (``mode``, ``volume_type``) carry the raw MQL5 constant
|
|
289
|
+
names (e.g. ``SYMBOL_COMMISSION_MODE_MONEY``) so nothing is lost in
|
|
290
|
+
translation. ``range_to == 0`` means unbounded; ``min_value`` /
|
|
291
|
+
``max_value`` cap the charged amount (0 = no cap).
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
mode: str
|
|
295
|
+
volume_type: str
|
|
296
|
+
value: float
|
|
297
|
+
min_value: float
|
|
298
|
+
max_value: float
|
|
299
|
+
range_from: float
|
|
300
|
+
range_to: float
|
|
301
|
+
currency: str
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class CommissionRule(_Camel):
|
|
305
|
+
"""One broker commission rule for a symbol, as configured server-side
|
|
306
|
+
(MT5 ``SymbolInfoCommissions``).
|
|
307
|
+
|
|
308
|
+
A symbol can carry several rules; each has its own tiers. The mode fields
|
|
309
|
+
carry the MQL5 ``ENUM_SYMBOL_COMMISSION_*`` constant names verbatim.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
currency: str
|
|
313
|
+
range_mode: str
|
|
314
|
+
charge_mode: str
|
|
315
|
+
entry_mode: str
|
|
316
|
+
direction_mode: str
|
|
317
|
+
profit_mode: str
|
|
318
|
+
tiers: list[CommissionTier] = []
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TradingSession(_Camel):
|
|
322
|
+
"""One trading-session window of a symbol, in *broker server time*.
|
|
323
|
+
|
|
324
|
+
``day`` uses the MQL ``ENUM_DAY_OF_WEEK`` constant names (``SUNDAY`` …
|
|
325
|
+
``SATURDAY``); times are ``HH:MM`` where ``24:00`` means end of day, so a
|
|
326
|
+
24-hour market shows ``00:00``–``24:00``.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
day: str
|
|
330
|
+
from_: str = Field(alias="from")
|
|
331
|
+
to: str
|
|
332
|
+
|
|
333
|
+
|
|
274
334
|
class SymbolInfo(_Camel):
|
|
275
|
-
"""Contract specification for a symbol (``GET /SymbolInfo``).
|
|
335
|
+
"""Contract specification for a symbol (``GET /SymbolInfo``).
|
|
336
|
+
|
|
337
|
+
``commissions`` are the broker's commission rules straight from the
|
|
338
|
+
server's symbol specification; ``sessions`` are the per-weekday trading
|
|
339
|
+
windows in broker server time. Both default to empty on pods older than
|
|
340
|
+
bridge 0.10 (and ``commissions`` also when the broker publishes none or
|
|
341
|
+
the terminal predates the API — build 6060+).
|
|
342
|
+
"""
|
|
276
343
|
|
|
277
344
|
symbol: str
|
|
278
345
|
description: str
|
|
@@ -295,6 +362,8 @@ class SymbolInfo(_Camel):
|
|
|
295
362
|
currency_base: str
|
|
296
363
|
currency_profit: str
|
|
297
364
|
currency_margin: str
|
|
365
|
+
commissions: list[CommissionRule] = []
|
|
366
|
+
sessions: list[TradingSession] = []
|
|
298
367
|
|
|
299
368
|
|
|
300
369
|
class Candle(_Camel):
|
|
@@ -360,6 +429,39 @@ class OrderResult(_Camel):
|
|
|
360
429
|
return self.success or self.is_no_change
|
|
361
430
|
|
|
362
431
|
|
|
432
|
+
class CloseAllResult(_Camel):
|
|
433
|
+
"""One per-ticket outcome of a ``/CloseAll`` pass.
|
|
434
|
+
|
|
435
|
+
``kind`` is ``"position"`` (closed) or ``"pending"`` (deleted).
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
ticket: int
|
|
439
|
+
kind: str
|
|
440
|
+
success: bool
|
|
441
|
+
retcode: int
|
|
442
|
+
retcode_description: str
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def is_pending(self) -> bool:
|
|
446
|
+
"""True when this row is a deleted pending order (vs. a closed position)."""
|
|
447
|
+
return self.kind.lower() == "pending"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class CloseAllSummary(_Camel):
|
|
451
|
+
"""Reply of ``POST /CloseAll`` — every matched position (and pending
|
|
452
|
+
order, when ``delete_pending`` was set) with its close/delete outcome.
|
|
453
|
+
|
|
454
|
+
``requested`` is how many orders the filters matched and were attempted;
|
|
455
|
+
``closed`` how many attempts the broker accepted; ``failed`` how many it
|
|
456
|
+
rejected — inspect ``results`` for the per-ticket retcodes.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
requested: int
|
|
460
|
+
closed: int
|
|
461
|
+
failed: int
|
|
462
|
+
results: list[CloseAllResult] = []
|
|
463
|
+
|
|
464
|
+
|
|
363
465
|
class MarginCalc(_Camel):
|
|
364
466
|
"""Required margin for a hypothetical order (``GET /OrderCalcMargin``)."""
|
|
365
467
|
|
|
@@ -411,8 +513,17 @@ class AccountHealth(_Camel):
|
|
|
411
513
|
|
|
412
514
|
|
|
413
515
|
class BridgeHealth(_Camel):
|
|
516
|
+
"""Bridge section of ``/status``.
|
|
517
|
+
|
|
518
|
+
``trade_ea_heartbeat_age_ms`` is how long ago the trade EA last made
|
|
519
|
+
dispatcher progress (``-1`` = never registered, or a pod older than
|
|
520
|
+
bridge 0.10). A large age while ``trade_ea_ready`` is still ``True``
|
|
521
|
+
means the EA is blocked in a long dealer call or dead — worth alerting on.
|
|
522
|
+
"""
|
|
523
|
+
|
|
414
524
|
version: str = ""
|
|
415
525
|
trade_ea_ready: bool = False
|
|
526
|
+
trade_ea_heartbeat_age_ms: int = -1
|
|
416
527
|
symbols_synced: bool = False
|
|
417
528
|
|
|
418
529
|
|
|
@@ -452,8 +563,29 @@ class HealthChecks(_Camel):
|
|
|
452
563
|
class TradeEventData(_Camel):
|
|
453
564
|
"""A trade transaction pushed on the ``trades`` stream.
|
|
454
565
|
|
|
455
|
-
``
|
|
456
|
-
|
|
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``.
|
|
457
589
|
"""
|
|
458
590
|
|
|
459
591
|
deal: int
|
|
@@ -465,8 +597,17 @@ class TradeEventData(_Camel):
|
|
|
465
597
|
volume: float
|
|
466
598
|
price: float
|
|
467
599
|
profit: float
|
|
600
|
+
commission: float = 0.0
|
|
601
|
+
swap: float = 0.0
|
|
602
|
+
magic: int = 0
|
|
468
603
|
comment: str
|
|
469
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
|
|
470
611
|
|
|
471
612
|
|
|
472
613
|
class TerminalStatusData(_Camel):
|
|
@@ -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
|
}
|
|
@@ -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
|
# --------------------------------------------------------------------------- #
|
|
@@ -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
|
|
@@ -567,6 +693,54 @@ def test_server_timezone_parses() -> None:
|
|
|
567
693
|
assert isinstance(tz.server_time, str)
|
|
568
694
|
|
|
569
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
|
+
|
|
570
744
|
@respx.mock
|
|
571
745
|
def test_position_history_sends_dates_and_parses() -> None:
|
|
572
746
|
route = respx.get(f"{TERM}/PositionHistory").mock(
|
|
@@ -721,3 +895,31 @@ async def test_async_order_send_body_and_validation() -> None:
|
|
|
721
895
|
"volume": 0.2,
|
|
722
896
|
"expertId": 7,
|
|
723
897
|
}
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@respx.mock
|
|
901
|
+
async def test_async_close_all() -> None:
|
|
902
|
+
route = respx.post(f"{TERM}/CloseAll").mock(
|
|
903
|
+
return_value=httpx.Response(
|
|
904
|
+
200,
|
|
905
|
+
json={
|
|
906
|
+
"requested": 1,
|
|
907
|
+
"closed": 1,
|
|
908
|
+
"failed": 0,
|
|
909
|
+
"results": [
|
|
910
|
+
{
|
|
911
|
+
"ticket": 100,
|
|
912
|
+
"kind": "position",
|
|
913
|
+
"success": True,
|
|
914
|
+
"retcode": 10009,
|
|
915
|
+
"retcodeDescription": "Done",
|
|
916
|
+
}
|
|
917
|
+
],
|
|
918
|
+
},
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
async with AsyncClient(api_key="fxs_live_k") as fx:
|
|
922
|
+
term = fx.terminal(_account())
|
|
923
|
+
summary = await term.close_all(symbol="EURUSD")
|
|
924
|
+
assert json.loads(route.calls.last.request.content) == {"symbol": "EURUSD"}
|
|
925
|
+
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
|