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.
Files changed (28) hide show
  1. {fxsocket-0.1 → fxsocket-0.2}/PKG-INFO +18 -1
  2. {fxsocket-0.1 → fxsocket-0.2}/README.md +17 -0
  3. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/__init__.py +2 -0
  4. fxsocket-0.2/src/fxsocket/_version.py +1 -0
  5. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/enums.py +15 -0
  6. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/models.py +27 -2
  7. {fxsocket-0.1 → fxsocket-0.2}/tests/test_terminal.py +42 -0
  8. fxsocket-0.1/src/fxsocket/_version.py +0 -1
  9. {fxsocket-0.1 → fxsocket-0.2}/.github/workflows/ci.yml +0 -0
  10. {fxsocket-0.1 → fxsocket-0.2}/.github/workflows/publish.yml +0 -0
  11. {fxsocket-0.1 → fxsocket-0.2}/.gitignore +0 -0
  12. {fxsocket-0.1 → fxsocket-0.2}/LICENSE +0 -0
  13. {fxsocket-0.1 → fxsocket-0.2}/examples/manage_accounts.py +0 -0
  14. {fxsocket-0.1 → fxsocket-0.2}/examples/stream_quotes.py +0 -0
  15. {fxsocket-0.1 → fxsocket-0.2}/examples/terminal_rest.py +0 -0
  16. {fxsocket-0.1 → fxsocket-0.2}/pyproject.toml +0 -0
  17. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/_http.py +0 -0
  18. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/client.py +0 -0
  19. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/config.py +0 -0
  20. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/errors.py +0 -0
  21. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/management.py +0 -0
  22. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/py.typed +0 -0
  23. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/__init__.py +0 -0
  24. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/client.py +0 -0
  25. {fxsocket-0.1 → fxsocket-0.2}/src/fxsocket/terminal/stream.py +0 -0
  26. {fxsocket-0.1 → fxsocket-0.2}/tests/test_errors.py +0 -0
  27. {fxsocket-0.1 → fxsocket-0.2}/tests/test_management.py +0 -0
  28. {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.1
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 (10010).
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