fxsocket 0.7.0__tar.gz → 0.8.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 (35) hide show
  1. {fxsocket-0.7.0 → fxsocket-0.8.0}/.github/workflows/ci.yml +2 -2
  2. {fxsocket-0.7.0 → fxsocket-0.8.0}/.github/workflows/publish.yml +5 -5
  3. fxsocket-0.8.0/CHANGELOG.md +39 -0
  4. {fxsocket-0.7.0 → fxsocket-0.8.0}/PKG-INFO +6 -3
  5. {fxsocket-0.7.0 → fxsocket-0.8.0}/README.md +5 -2
  6. fxsocket-0.8.0/src/fxsocket/_version.py +1 -0
  7. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/enums.py +7 -19
  8. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/errors.py +3 -2
  9. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/client.py +8 -23
  10. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/stream.py +5 -11
  11. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_stream.py +6 -4
  12. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_terminal.py +25 -3
  13. fxsocket-0.7.0/src/fxsocket/_version.py +0 -1
  14. {fxsocket-0.7.0 → fxsocket-0.8.0}/.gitignore +0 -0
  15. {fxsocket-0.7.0 → fxsocket-0.8.0}/LICENSE +0 -0
  16. {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/manage_accounts.py +0 -0
  17. {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/multi_account_trade.py +0 -0
  18. {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/stream_quotes.py +0 -0
  19. {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/terminal_rest.py +0 -0
  20. {fxsocket-0.7.0 → fxsocket-0.8.0}/pyproject.toml +0 -0
  21. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/__init__.py +0 -0
  22. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/_http.py +0 -0
  23. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/client.py +0 -0
  24. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/config.py +0 -0
  25. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/management.py +0 -0
  26. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/models.py +0 -0
  27. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/py.typed +0 -0
  28. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/__init__.py +0 -0
  29. {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/trading.py +0 -0
  30. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_errors.py +0 -0
  31. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_management.py +0 -0
  32. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_private_servers.py +0 -0
  33. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_readonly_keys.py +0 -0
  34. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_trading.py +0 -0
  35. {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_wallet.py +0 -0
@@ -13,8 +13,8 @@ jobs:
13
13
  matrix:
14
14
  python-version: ["3.10", "3.11", "3.12", "3.13"]
15
15
  steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-python@v5
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v7
18
18
  with:
19
19
  python-version: ${{ matrix.python-version }}
20
20
  - name: Install
@@ -13,8 +13,8 @@ jobs:
13
13
  build:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-python@v5
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v7
18
18
  with:
19
19
  python-version: "3.12"
20
20
  - name: Build sdist + wheel
@@ -22,7 +22,7 @@ jobs:
22
22
  python -m pip install --upgrade build twine
23
23
  python -m build
24
24
  twine check dist/*
25
- - uses: actions/upload-artifact@v4
25
+ - uses: actions/upload-artifact@v7
26
26
  with:
27
27
  name: dist
28
28
  path: dist/
@@ -36,7 +36,7 @@ jobs:
36
36
  permissions:
37
37
  id-token: write # required for trusted publishing
38
38
  steps:
39
- - uses: actions/download-artifact@v4
39
+ - uses: actions/download-artifact@v8
40
40
  with:
41
41
  name: dist
42
42
  path: dist/
@@ -53,7 +53,7 @@ jobs:
53
53
  permissions:
54
54
  id-token: write # required for trusted publishing
55
55
  steps:
56
- - uses: actions/download-artifact@v4
56
+ - uses: actions/download-artifact@v8
57
57
  with:
58
58
  name: dist
59
59
  path: dist/
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases
6
+ up to 0.7.0 are described in their tagged commits.
7
+
8
+ ## [0.8.0] - 2026-10-26
9
+
10
+ ### Removed
11
+
12
+ - `Timeframe.M2`, `M3`, `H2`, `H6`, `H8` and `H12`, and the `2min`, `3min`,
13
+ `2h`, `6h`, `8h` and `12h` aliases. The SDK treated them as MT5-only, but
14
+ neither terminal API serves them: MT4 and MT5 both accept exactly `M1`, `M5`,
15
+ `M15`, `M30`, `H1`, `H4`, `D1`, `W1` and `MN1`, in REST `price_history` and on
16
+ the `bars` stream. On an MT5 account they passed the SDK and the terminal
17
+ refused them (HTTP 400 from `price_history`, an `error` frame and no bars
18
+ from the stream).
19
+
20
+ They are removed rather than kept and rejected on both platforms, so that
21
+ `Timeframe` lists exactly what the server accepts: iterating it gives the
22
+ usable set, and type checkers flag the removed names. The only code this
23
+ breaks is code that could never succeed.
24
+
25
+ ### Changed
26
+
27
+ - A timeframe outside those nine now raises `ValidationError` on both
28
+ platforms before anything is sent, and the message lists the accepted
29
+ labels. On MT4 the six removed ones used to raise
30
+ `UnsupportedOnPlatformError`, so code that caught that for timeframes should
31
+ catch `ValidationError` instead. Nothing raises `UnsupportedOnPlatformError`
32
+ now; it stays exported.
33
+
34
+ ### Deprecated
35
+
36
+ - `fxsocket.enums.MT5_ONLY_TIMEFRAMES` is always empty and will be removed in
37
+ 1.0.
38
+
39
+ [0.8.0]: https://github.com/fxsocket-com/FxSocket-Python-SDK/releases/tag/v0.8.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fxsocket
3
- Version: 0.7.0
3
+ Version: 0.8.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
@@ -232,8 +232,11 @@ in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
232
232
  it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
233
233
  `None` (the default) keeps the current value.
234
234
 
235
- MT4 and MT5 share one interface. MT5-only timeframes (`M2`, `M3`, `H2`, `H6`,
236
- `H8`, `H12`) raise `UnsupportedOnPlatformError` on MT4 before any request.
235
+ MT4 and MT5 share one interface, timeframes included. Both accept the same nine:
236
+ `M1`, `M5`, `M15`, `M30`, `H1`, `H4`, `D1`, `W1`, `MN1` (or `1min`, `5min`,
237
+ `15min`, `30min`, `1h`, `4h`, `1d`, `1w`, `1month`). MT5's other periods, such
238
+ as `H2` or `H12`, aren't served by the terminal API, so anything else raises
239
+ `ValidationError` before any request.
237
240
 
238
241
  > **MT4 history note:** on MT4, `price_history` with `from_`/`to` bounds (or the
239
242
  > `D1` timeframe) can fail server-side with `CopyRates failed` when the terminal
@@ -205,8 +205,11 @@ in `order_modify`, a literal `stop_loss=0.0` would *remove* your stop-loss, so
205
205
  it's rejected — pass `clear_stop_loss=True` to remove one deliberately, while
206
206
  `None` (the default) keeps the current value.
207
207
 
208
- MT4 and MT5 share one interface. MT5-only timeframes (`M2`, `M3`, `H2`, `H6`,
209
- `H8`, `H12`) raise `UnsupportedOnPlatformError` on MT4 before any request.
208
+ MT4 and MT5 share one interface, timeframes included. Both accept the same nine:
209
+ `M1`, `M5`, `M15`, `M30`, `H1`, `H4`, `D1`, `W1`, `MN1` (or `1min`, `5min`,
210
+ `15min`, `30min`, `1h`, `4h`, `1d`, `1w`, `1month`). MT5's other periods, such
211
+ as `H2` or `H12`, aren't served by the terminal API, so anything else raises
212
+ `ValidationError` before any request.
210
213
 
211
214
  > **MT4 history note:** on MT4, `price_history` with `from_`/`to` bounds (or the
212
215
  > `D1` timeframe) can fail server-side with `CopyRates failed` when the terminal
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -229,35 +229,23 @@ class HealthStatus(str, Enum):
229
229
  class Timeframe(str, Enum):
230
230
  """Candle timeframes.
231
231
 
232
- The MT5-only members (``M2``, ``M3``, ``H2``, ``H6``, ``H8``, ``H12``)
233
- are rejected client-side for MT4 accounts.
232
+ Exactly the nine the terminal API accepts, on MT4 and MT5 alike (REST
233
+ ``price_history`` and the ``bars`` stream). MT5's other native periods
234
+ (``M2``, ``H2``, ``H12`` …) are not exposed by the terminal API, so they
235
+ are not members.
234
236
  """
235
237
 
236
238
  M1 = "M1"
237
- M2 = "M2"
238
- M3 = "M3"
239
239
  M5 = "M5"
240
240
  M15 = "M15"
241
241
  M30 = "M30"
242
242
  H1 = "H1"
243
- H2 = "H2"
244
243
  H4 = "H4"
245
- H6 = "H6"
246
- H8 = "H8"
247
- H12 = "H12"
248
244
  D1 = "D1"
249
245
  W1 = "W1"
250
246
  MN1 = "MN1"
251
247
 
252
248
 
253
- #: Timeframes that only MT5 supports.
254
- MT5_ONLY_TIMEFRAMES = frozenset(
255
- {
256
- Timeframe.M2,
257
- Timeframe.M3,
258
- Timeframe.H2,
259
- Timeframe.H6,
260
- Timeframe.H8,
261
- Timeframe.H12,
262
- }
263
- )
249
+ #: Deprecated, always empty: both platforms accept every :class:`Timeframe`.
250
+ #: Kept so existing imports don't break; will be removed in 1.0.
251
+ MT5_ONLY_TIMEFRAMES: frozenset[Timeframe] = frozenset()
@@ -206,8 +206,9 @@ class TerminalTimeoutError(FxSocketError):
206
206
  class UnsupportedOnPlatformError(FxSocketError):
207
207
  """A requested feature doesn't exist on the account's platform.
208
208
 
209
- Enforced client-side — e.g. stop-limit orders or MT5-only timeframes on
210
- an MT4 account.
209
+ Nothing raises it at the moment: the MT4 and MT5 terminal APIs accept the
210
+ same timeframes and order operations. It stays exported for platform gaps
211
+ that need a client-side guard later.
211
212
  """
212
213
 
213
214
 
@@ -4,11 +4,10 @@ One :class:`TerminalClient` (sync) / :class:`AsyncTerminalClient` (async) is
4
4
  bound to a single account's terminal endpoint (``Account.rest_url``). The two
5
5
  are method-for-method mirrors; only the awaiting differs.
6
6
 
7
- Platform awareness is enforced **client-side** before the request goes out:
8
- MT5-only timeframes (``M2``/``M3``/``H2``/``H6``/``H8``/``H12``) don't exist on
9
- MT4, so the SDK raises :class:`~fxsocket.UnsupportedOnPlatformError` rather than
10
- letting the terminal answer 400. (Order *operations*, including stop-limit, are
11
- accepted by both platforms' terminal APIs, so they are not gated.)
7
+ MT4 and MT5 share one interface with nothing gated by platform: both terminal
8
+ APIs accept the same nine timeframes (see :class:`~fxsocket.Timeframe`) and
9
+ all eight order operations, stop-limit included. A timeframe outside that set
10
+ raises :class:`~fxsocket.ValidationError` before the request goes out.
12
11
 
13
12
  Order inputs are validated client-side to fail fast and, above all, safely:
14
13
  the same constraints the terminal enforces (volume > 0, an entry price for
@@ -30,7 +29,6 @@ import httpx
30
29
 
31
30
  from .._http import AsyncHTTP, SyncHTTP, auth_headers
32
31
  from ..enums import (
33
- MT5_ONLY_TIMEFRAMES,
34
32
  PENDING_OPERATIONS,
35
33
  STOP_LIMIT_OPERATIONS,
36
34
  OrderOperation,
@@ -38,7 +36,6 @@ from ..enums import (
38
36
  Timeframe,
39
37
  )
40
38
  from ..errors import (
41
- UnsupportedOnPlatformError,
42
39
  ValidationError,
43
40
  error_from_response,
44
41
  )
@@ -68,17 +65,11 @@ _OP_BY_NORM = {op.value.lower(): op for op in OrderOperation}
68
65
 
69
66
  _TF_HUMAN = {
70
67
  "1min": Timeframe.M1,
71
- "2min": Timeframe.M2,
72
- "3min": Timeframe.M3,
73
68
  "5min": Timeframe.M5,
74
69
  "15min": Timeframe.M15,
75
70
  "30min": Timeframe.M30,
76
71
  "1h": Timeframe.H1,
77
- "2h": Timeframe.H2,
78
72
  "4h": Timeframe.H4,
79
- "6h": Timeframe.H6,
80
- "8h": Timeframe.H8,
81
- "12h": Timeframe.H12,
82
73
  "1d": Timeframe.D1,
83
74
  "1w": Timeframe.W1,
84
75
  "1month": Timeframe.MN1,
@@ -108,7 +99,10 @@ def coerce_timeframe(value: Timeframe | str) -> Timeframe:
108
99
  pass
109
100
  tf = _TF_HUMAN.get(raw.lower())
110
101
  if tf is None:
111
- raise ValidationError(f"Unknown timeframe: {value!r}")
102
+ supported = ", ".join(t.value for t in Timeframe)
103
+ raise ValidationError(
104
+ f"Unknown timeframe: {value!r}; the terminal API accepts {supported}"
105
+ )
112
106
  return tf
113
107
 
114
108
 
@@ -184,13 +178,6 @@ def _resolve_modify_sl_tp(
184
178
  return sl, tp
185
179
 
186
180
 
187
- def _check_timeframe(tf: Timeframe, platform: Platform) -> None:
188
- if platform is Platform.MT4 and tf in MT5_ONLY_TIMEFRAMES:
189
- raise UnsupportedOnPlatformError(
190
- f"{tf.value} is an MT5-only timeframe; not available on MT4."
191
- )
192
-
193
-
194
181
  def _fmt_time(value: str | datetime | date | None) -> str | None:
195
182
  if value is None:
196
183
  return None
@@ -357,7 +344,6 @@ class TerminalClient:
357
344
  to: str | datetime | date | None = None,
358
345
  ) -> list[Candle]:
359
346
  tf = coerce_timeframe(timeframe)
360
- _check_timeframe(tf, self.platform)
361
347
  params = _clean(
362
348
  {
363
349
  "symbol": symbol,
@@ -638,7 +624,6 @@ class AsyncTerminalClient:
638
624
  to: str | datetime | date | None = None,
639
625
  ) -> list[Candle]:
640
626
  tf = coerce_timeframe(timeframe)
641
- _check_timeframe(tf, self.platform)
642
627
  params = _clean(
643
628
  {
644
629
  "symbol": symbol,
@@ -38,8 +38,8 @@ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
38
38
  from websockets.asyncio.client import connect as _ws_connect
39
39
  from websockets.exceptions import ConnectionClosed
40
40
 
41
- from ..enums import MT5_ONLY_TIMEFRAMES, Platform, Timeframe
42
- from ..errors import StreamError, UnsupportedOnPlatformError, ValidationError
41
+ from ..enums import Platform, Timeframe
42
+ from ..errors import StreamError, ValidationError
43
43
  from ..models import (
44
44
  AccountSummary,
45
45
  Candle,
@@ -224,7 +224,6 @@ def _validate_sub(
224
224
  topic: str,
225
225
  symbol: str | None,
226
226
  timeframe: Timeframe | str | None,
227
- platform: Platform,
228
227
  ) -> str | None:
229
228
  """Validate a subscription; returns the canonical timeframe label (or None)."""
230
229
  if topic not in _TOPICS:
@@ -236,12 +235,7 @@ def _validate_sub(
236
235
  if topic == "bars":
237
236
  if not timeframe:
238
237
  raise ValidationError("topic 'bars' requires a timeframe")
239
- tf = coerce_timeframe(timeframe)
240
- if platform is Platform.MT4 and tf in MT5_ONLY_TIMEFRAMES:
241
- raise UnsupportedOnPlatformError(
242
- f"{tf.value} is an MT5-only timeframe; not available on MT4."
243
- )
244
- return tf.value
238
+ return coerce_timeframe(timeframe).value
245
239
  return None
246
240
 
247
241
 
@@ -319,7 +313,7 @@ class AsyncStream:
319
313
  symbol: str | None = None,
320
314
  timeframe: Timeframe | str | None = None,
321
315
  ) -> None:
322
- tf = _validate_sub(topic, symbol, timeframe, self.platform)
316
+ tf = _validate_sub(topic, symbol, timeframe)
323
317
  self._subs.add((topic, symbol, tf))
324
318
  await self._send(_sub_payload(topic, symbol, tf))
325
319
 
@@ -330,7 +324,7 @@ class AsyncStream:
330
324
  symbol: str | None = None,
331
325
  timeframe: Timeframe | str | None = None,
332
326
  ) -> None:
333
- tf = _validate_sub(topic, symbol, timeframe, self.platform)
327
+ tf = _validate_sub(topic, symbol, timeframe)
334
328
  self._subs.discard((topic, symbol, tf))
335
329
  payload = _sub_payload(topic, symbol, tf)
336
330
  payload["action"] = "unsubscribe"
@@ -21,7 +21,6 @@ from fxsocket import (
21
21
  TerminalUpdate,
22
22
  Tick,
23
23
  TradeUpdate,
24
- UnsupportedOnPlatformError,
25
24
  )
26
25
  from fxsocket.terminal.stream import (
27
26
  StreamErrorEvent,
@@ -268,9 +267,12 @@ async def test_bars_requires_timeframe() -> None:
268
267
  await s.subscribe("bars", symbol="EURUSD")
269
268
 
270
269
 
271
- async def test_bars_mt5_only_timeframe_rejected_on_mt4() -> None:
272
- s = AsyncStream(ws_url="ws://x/ws", api_key="k", platform="mt4")
273
- with pytest.raises(UnsupportedOnPlatformError):
270
+ @pytest.mark.parametrize("platform", ["mt4", "mt5"])
271
+ async def test_bars_unsupported_timeframe_rejected(platform: str) -> None:
272
+ from fxsocket import ValidationError
273
+
274
+ s = AsyncStream(ws_url="ws://x/ws", api_key="k", platform=platform)
275
+ with pytest.raises(ValidationError):
274
276
  await s.subscribe_bars("EURUSD", "H6")
275
277
 
276
278
 
@@ -16,7 +16,6 @@ from fxsocket import (
16
16
  TerminalNotReadyError,
17
17
  TerminalTimeoutError,
18
18
  Timeframe,
19
- UnsupportedOnPlatformError,
20
19
  )
21
20
  from fxsocket.models import Account
22
21
  from fxsocket.terminal.client import (
@@ -102,6 +101,21 @@ def test_coerce_timeframe(value: object, expected: Timeframe) -> None:
102
101
  assert coerce_timeframe(value) is expected # type: ignore[arg-type]
103
102
 
104
103
 
104
+ def test_timeframe_matches_terminal_api() -> None:
105
+ # The nine labels the MT4 and MT5 terminal APIs parse (REST + `bars`).
106
+ assert [tf.value for tf in Timeframe] == [
107
+ "M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1", "MN1"
108
+ ]
109
+
110
+
111
+ @pytest.mark.parametrize("value", ["H6", "M2", "h12", "2h", "3min", "7s"])
112
+ def test_coerce_timeframe_unsupported(value: str) -> None:
113
+ from fxsocket import ValidationError
114
+
115
+ with pytest.raises(ValidationError, match="M1, M5, M15"):
116
+ coerce_timeframe(value)
117
+
118
+
105
119
  # --------------------------------------------------------------------------- #
106
120
  # Parsing — camelCase aliases, str timestamps
107
121
  # --------------------------------------------------------------------------- #
@@ -249,9 +263,17 @@ def test_price_history_sends_canonical_timeframe() -> None:
249
263
  assert "to" not in req.url.params # None omitted
250
264
 
251
265
 
252
- def test_price_history_rejects_mt5_timeframe_on_mt4() -> None:
253
- with _term(platform="mt4") as t, pytest.raises(UnsupportedOnPlatformError):
266
+ @respx.mock
267
+ @pytest.mark.parametrize("platform", ["mt4", "mt5"])
268
+ def test_price_history_rejects_unsupported_timeframe(platform: str) -> None:
269
+ from fxsocket import ValidationError
270
+
271
+ route = respx.get(f"{TERM}/PriceHistory").mock(
272
+ return_value=httpx.Response(200, json=[])
273
+ )
274
+ with _term(platform=platform) as t, pytest.raises(ValidationError):
254
275
  t.price_history("EURUSD", "H6")
276
+ assert not route.called
255
277
 
256
278
 
257
279
  # --------------------------------------------------------------------------- #
@@ -1 +0,0 @@
1
- __version__ = "0.7.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