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.
- {fxsocket-0.7.0 → fxsocket-0.8.0}/.github/workflows/ci.yml +2 -2
- {fxsocket-0.7.0 → fxsocket-0.8.0}/.github/workflows/publish.yml +5 -5
- fxsocket-0.8.0/CHANGELOG.md +39 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/PKG-INFO +6 -3
- {fxsocket-0.7.0 → fxsocket-0.8.0}/README.md +5 -2
- fxsocket-0.8.0/src/fxsocket/_version.py +1 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/enums.py +7 -19
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/errors.py +3 -2
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/client.py +8 -23
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/stream.py +5 -11
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_stream.py +6 -4
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_terminal.py +25 -3
- fxsocket-0.7.0/src/fxsocket/_version.py +0 -1
- {fxsocket-0.7.0 → fxsocket-0.8.0}/.gitignore +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/LICENSE +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/manage_accounts.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/multi_account_trade.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/stream_quotes.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/examples/terminal_rest.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/pyproject.toml +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/__init__.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/client.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/config.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/management.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/models.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/src/fxsocket/trading.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_errors.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_management.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_private_servers.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_readonly_keys.py +0 -0
- {fxsocket-0.7.0 → fxsocket-0.8.0}/tests/test_trading.py +0 -0
- {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@
|
|
17
|
-
- uses: actions/setup-python@
|
|
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@
|
|
17
|
-
- uses: actions/setup-python@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
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.
|
|
236
|
-
`
|
|
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.
|
|
209
|
-
`
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
#:
|
|
254
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
from ..errors import StreamError,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
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
|