fxsocket 0.1__tar.gz → 0.2__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.1 → fxsocket-0.2}/PKG-INFO +18 -1
- {fxsocket-0.1 → fxsocket-0.2}/README.md +17 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/__init__.py +2 -0
- fxsocket-0.2/src/fxsocket/_version.py +1 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/enums.py +15 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/models.py +27 -2
- {fxsocket-0.1 → fxsocket-0.2}/tests/test_terminal.py +42 -0
- fxsocket-0.1/src/fxsocket/_version.py +0 -1
- {fxsocket-0.1 → fxsocket-0.2}/.github/workflows/ci.yml +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/.github/workflows/publish.yml +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/.gitignore +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/LICENSE +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/examples/manage_accounts.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/examples/stream_quotes.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/examples/terminal_rest.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/pyproject.toml +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/client.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/config.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/errors.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/management.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/client.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/stream.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/tests/test_errors.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/tests/test_management.py +0 -0
- {fxsocket-0.1 → fxsocket-0.2}/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.2
|
|
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
|
|
@@ -138,6 +138,23 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
138
138
|
term.order_close(result.order)
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
Every order call returns an `OrderResult`. A `200` only means the terminal
|
|
142
|
+
answered — check the body: `success` is true for `retcode` `10009` (done) or
|
|
143
|
+
`10008` (placed), and `outcome` classifies the result as `applied` /
|
|
144
|
+
`no_change` / `partial` / `rejected` (compare against `OrderOutcome`).
|
|
145
|
+
|
|
146
|
+
`no_change` (retcode `10025`) is a benign, idempotent no-op — the requested
|
|
147
|
+
SL/TP/price already match — so it's safe to treat as applied even though
|
|
148
|
+
`success` is `False`. For idempotent SL/TP management (e.g. re-sending after a
|
|
149
|
+
lost confirmation), send absolute values and gate on `result.is_effective`
|
|
150
|
+
(true for both `applied` and `no_change`):
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
res = term.order_modify(ticket, stop_loss=1.0850)
|
|
154
|
+
if res.is_effective: # applied now, or already in effect
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
141
158
|
Inputs are validated client-side before they're sent. One guard worth knowing:
|
|
142
159
|
in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
|
|
143
160
|
it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
|
|
@@ -111,6 +111,23 @@ with Client(api_key="fxs_live_…") as fx:
|
|
|
111
111
|
term.order_close(result.order)
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
Every order call returns an `OrderResult`. A `200` only means the terminal
|
|
115
|
+
answered — check the body: `success` is true for `retcode` `10009` (done) or
|
|
116
|
+
`10008` (placed), and `outcome` classifies the result as `applied` /
|
|
117
|
+
`no_change` / `partial` / `rejected` (compare against `OrderOutcome`).
|
|
118
|
+
|
|
119
|
+
`no_change` (retcode `10025`) is a benign, idempotent no-op — the requested
|
|
120
|
+
SL/TP/price already match — so it's safe to treat as applied even though
|
|
121
|
+
`success` is `False`. For idempotent SL/TP management (e.g. re-sending after a
|
|
122
|
+
lost confirmation), send absolute values and gate on `result.is_effective`
|
|
123
|
+
(true for both `applied` and `no_change`):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
res = term.order_modify(ticket, stop_loss=1.0850)
|
|
127
|
+
if res.is_effective: # applied now, or already in effect
|
|
128
|
+
...
|
|
129
|
+
```
|
|
130
|
+
|
|
114
131
|
Inputs are validated client-side before they're sent. One guard worth knowing:
|
|
115
132
|
in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
|
|
116
133
|
it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
|
|
@@ -14,6 +14,7 @@ from .enums import (
|
|
|
14
14
|
HealthStatus,
|
|
15
15
|
OrderKind,
|
|
16
16
|
OrderOperation,
|
|
17
|
+
OrderOutcome,
|
|
17
18
|
Platform,
|
|
18
19
|
Timeframe,
|
|
19
20
|
TradingStatus,
|
|
@@ -113,6 +114,7 @@ __all__ = [
|
|
|
113
114
|
"Platform",
|
|
114
115
|
"TradingStatus",
|
|
115
116
|
"OrderOperation",
|
|
117
|
+
"OrderOutcome",
|
|
116
118
|
"OrderKind",
|
|
117
119
|
"DealEntry",
|
|
118
120
|
"HealthStatus",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2"
|
|
@@ -61,6 +61,21 @@ PENDING_OPERATIONS = frozenset(
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class OrderOutcome(str, Enum):
|
|
65
|
+
"""Semantic classification of a trade result (``OrderResult.outcome``).
|
|
66
|
+
|
|
67
|
+
``success`` only tells you applied-or-not; ``outcome`` additionally
|
|
68
|
+
separates a benign no-op and a partial fill from a genuine rejection, so
|
|
69
|
+
clients don't have to hardcode retcode tables. Empty on bridges older than
|
|
70
|
+
MT5 0.6.1 / MT4 0.5.1.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
APPLIED = "applied" #: retcode 10009 (done) / 10008 (placed)
|
|
74
|
+
NO_CHANGE = "no_change" #: retcode 10025 — requested state already in effect
|
|
75
|
+
PARTIAL = "partial" #: retcode 10010 (done partially)
|
|
76
|
+
REJECTED = "rejected" #: anything else — inspect ``retcode`` / ``comment``
|
|
77
|
+
|
|
78
|
+
|
|
64
79
|
class OrderKind(str, Enum):
|
|
65
80
|
"""Whether an opened row is a live position or a resting pending order."""
|
|
66
81
|
|
|
@@ -24,7 +24,7 @@ from datetime import datetime
|
|
|
24
24
|
from pydantic import BaseModel, ConfigDict
|
|
25
25
|
from pydantic.alias_generators import to_camel
|
|
26
26
|
|
|
27
|
-
from .enums import Platform, TradingStatus
|
|
27
|
+
from .enums import OrderOutcome, Platform, TradingStatus
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class _Camel(BaseModel):
|
|
@@ -257,12 +257,23 @@ class Candle(_Camel):
|
|
|
257
257
|
class OrderResult(_Camel):
|
|
258
258
|
"""Result of an order send / modify / close.
|
|
259
259
|
|
|
260
|
-
``success`` is true when ``retcode`` is DONE (10009) or PLACED (
|
|
260
|
+
``success`` is true when ``retcode`` is DONE (10009) or PLACED (10008).
|
|
261
|
+
|
|
262
|
+
``outcome`` classifies the result further — ``"applied"`` /
|
|
263
|
+
``"no_change"`` / ``"partial"`` / ``"rejected"`` (compare against
|
|
264
|
+
:class:`fxsocket.OrderOutcome`). ``"no_change"`` (retcode 10025) is a
|
|
265
|
+
benign idempotent no-op — the requested SL/TP/price already match the
|
|
266
|
+
current values — so it is safe to treat as applied even though ``success``
|
|
267
|
+
is ``False``. Use :attr:`is_effective` when you only care that the
|
|
268
|
+
requested state is in effect (the idempotent-retry case). ``outcome`` is
|
|
269
|
+
empty on bridges older than MT5 0.6.1 / MT4 0.5.1.
|
|
270
|
+
|
|
261
271
|
``deal`` is the executed deal ticket (0 for pending placement, and always
|
|
262
272
|
0 on MT4); ``order`` is the resulting position / pending-order ticket.
|
|
263
273
|
"""
|
|
264
274
|
|
|
265
275
|
success: bool
|
|
276
|
+
outcome: str = ""
|
|
266
277
|
retcode: int
|
|
267
278
|
retcode_description: str
|
|
268
279
|
deal: int
|
|
@@ -273,6 +284,20 @@ class OrderResult(_Camel):
|
|
|
273
284
|
ask: float
|
|
274
285
|
comment: str
|
|
275
286
|
|
|
287
|
+
@property
|
|
288
|
+
def is_no_change(self) -> bool:
|
|
289
|
+
"""True for a benign no-op (retcode 10025 / ``outcome == "no_change"``):
|
|
290
|
+
the requested SL/TP/price already match the current values."""
|
|
291
|
+
return self.retcode == 10025 or self.outcome == OrderOutcome.NO_CHANGE
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def is_effective(self) -> bool:
|
|
295
|
+
"""True when the requested state is in effect — either ``success``
|
|
296
|
+
(applied) or a no-op (:attr:`is_no_change`). Use this for idempotent
|
|
297
|
+
SL/TP management, where re-sending an identical modify returns 10025
|
|
298
|
+
with ``success=False``."""
|
|
299
|
+
return self.success or self.is_no_change
|
|
300
|
+
|
|
276
301
|
|
|
277
302
|
class MarginCalc(_Camel):
|
|
278
303
|
"""Required margin for a hypothetical order (``GET /OrderCalcMargin``)."""
|
|
@@ -299,6 +299,48 @@ def test_order_send_allows_stop_limit_on_mt4() -> None:
|
|
|
299
299
|
assert sent["stopLimitPrice"] == 1.09
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
@respx.mock
|
|
303
|
+
def test_order_modify_no_change_is_effective() -> None:
|
|
304
|
+
# An idempotent re-send (SL/TP already match) returns retcode 10025 with
|
|
305
|
+
# success=False; outcome distinguishes the benign no-op from a rejection,
|
|
306
|
+
# and is_effective treats it as "requested state is in effect".
|
|
307
|
+
respx.post(f"{TERM}/OrderModify").mock(
|
|
308
|
+
return_value=httpx.Response(
|
|
309
|
+
200,
|
|
310
|
+
json={
|
|
311
|
+
"success": False,
|
|
312
|
+
"outcome": "no_change",
|
|
313
|
+
"retcode": 10025,
|
|
314
|
+
"retcodeDescription": "No changes",
|
|
315
|
+
"deal": 0,
|
|
316
|
+
"order": 100,
|
|
317
|
+
"volume": 0.1,
|
|
318
|
+
"price": 0.0,
|
|
319
|
+
"bid": 1.0849,
|
|
320
|
+
"ask": 1.0851,
|
|
321
|
+
"comment": "No changes",
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
with _term() as t:
|
|
326
|
+
res = t.order_modify(100, stop_loss=1.07)
|
|
327
|
+
assert res.success is False
|
|
328
|
+
assert res.outcome == "no_change"
|
|
329
|
+
assert res.is_no_change is True
|
|
330
|
+
assert res.is_effective is True
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_order_result_outcome_absent_defaults_empty() -> None:
|
|
334
|
+
# Bridges older than MT5 0.6.1 / MT4 0.5.1 omit `outcome` — it must default
|
|
335
|
+
# to "" (backward-compatible), not fail validation.
|
|
336
|
+
from fxsocket import OrderResult
|
|
337
|
+
|
|
338
|
+
res = OrderResult.model_validate(_ORDER_OK)
|
|
339
|
+
assert res.outcome == ""
|
|
340
|
+
assert res.success is True
|
|
341
|
+
assert res.is_effective is True
|
|
342
|
+
|
|
343
|
+
|
|
302
344
|
def test_order_send_rejects_nonpositive_volume() -> None:
|
|
303
345
|
from fxsocket import ValidationError
|
|
304
346
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1"
|
|
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
|