open-api-mt5 0.7.2__tar.gz → 0.7.4__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 (38) hide show
  1. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/PKG-INFO +1 -1
  2. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/main.py +1 -1
  3. open_api_mt5-0.7.4/app/services/connection_service.py +128 -0
  4. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/PKG-INFO +1 -1
  5. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/pyproject.toml +1 -1
  6. open_api_mt5-0.7.4/tests/test_connection_service.py +157 -0
  7. open_api_mt5-0.7.2/app/services/connection_service.py +0 -41
  8. open_api_mt5-0.7.2/tests/test_connection_service.py +0 -50
  9. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/README.md +0 -0
  10. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/__init__.py +0 -0
  11. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/cli.py +0 -0
  12. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/__init__.py +0 -0
  13. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/account_service.py +0 -0
  14. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/bar_service.py +0 -0
  15. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/calendar_service.py +0 -0
  16. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/config_service.py +0 -0
  17. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/exposure_service.py +0 -0
  18. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/market_data_service.py +0 -0
  19. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/news_close_store.py +0 -0
  20. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/registry_service.py +0 -0
  21. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/serialization.py +0 -0
  22. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/stream_service.py +0 -0
  23. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/symbol_service.py +0 -0
  24. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/ticket_service.py +0 -0
  25. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/trade_service.py +0 -0
  26. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/SOURCES.txt +0 -0
  27. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/dependency_links.txt +0 -0
  28. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/entry_points.txt +0 -0
  29. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/requires.txt +0 -0
  30. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/top_level.txt +0 -0
  31. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/setup.cfg +0 -0
  32. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_bar_service.py +0 -0
  33. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_bars_api.py +0 -0
  34. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_calendar_api.py +0 -0
  35. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_calendar_service.py +0 -0
  36. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_config_service.py +0 -0
  37. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_trade_api.py +0 -0
  38. {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_trade_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-api-mt5
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: REST and WebSocket API project for MetaTrader 5
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -432,7 +432,7 @@ async def lifespan(_: FastAPI):
432
432
  await asyncio.gather(monitor_task, calendar_task, news_close_task)
433
433
  except asyncio.CancelledError:
434
434
  pass
435
- mt5.shutdown()
435
+ shutdown_connection()
436
436
 
437
437
  app = FastAPI(
438
438
  title="MT5 API",
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import PurePath, PureWindowsPath
4
+ import subprocess
5
+ from typing import Any
6
+
7
+ import MetaTrader5 as mt5
8
+
9
+
10
+ _launched_terminal_process: subprocess.Popen[Any] | None = None
11
+
12
+
13
+ def _path_parts(value: str) -> PurePath:
14
+ if "\\" in value or ":" in value:
15
+ return PureWindowsPath(value)
16
+ return PurePath(value)
17
+
18
+
19
+ def _normalize_terminal_dir(value: str | None) -> str | None:
20
+ if not value:
21
+ return None
22
+
23
+ cleaned = value.strip().strip('"')
24
+ if not cleaned:
25
+ return None
26
+
27
+ parsed = _path_parts(cleaned)
28
+ if parsed.name.lower() in {"terminal.exe", "terminal64.exe"}:
29
+ parsed = parsed.parent
30
+ return str(parsed).replace("/", "\\").rstrip("\\").casefold()
31
+
32
+
33
+ def _connected_to_requested_terminal(path: str) -> bool:
34
+ terminal_info = mt5.terminal_info()
35
+ connected_path = _normalize_terminal_dir(getattr(terminal_info, "path", None))
36
+ requested_path = _normalize_terminal_dir(path)
37
+ return connected_path is None or requested_path is None or connected_path == requested_path
38
+
39
+
40
+ def _launch_terminal(path: str, portable: bool) -> tuple[bool, str | None]:
41
+ global _launched_terminal_process
42
+
43
+ args = [path]
44
+ if portable:
45
+ args.append("/portable")
46
+
47
+ try:
48
+ _launched_terminal_process = subprocess.Popen(args)
49
+ except OSError as exc:
50
+ return False, str(exc)
51
+ return True, None
52
+
53
+
54
+ def _close_launched_terminal() -> None:
55
+ global _launched_terminal_process
56
+
57
+ process = _launched_terminal_process
58
+ _launched_terminal_process = None
59
+ if process is None:
60
+ return
61
+
62
+ poll = getattr(process, "poll", None)
63
+ if callable(poll) and poll() is not None:
64
+ return
65
+
66
+ terminate = getattr(process, "terminate", None)
67
+ if callable(terminate):
68
+ terminate()
69
+
70
+ wait = getattr(process, "wait", None)
71
+ if not callable(wait):
72
+ return
73
+
74
+ try:
75
+ wait(timeout=5)
76
+ except subprocess.TimeoutExpired:
77
+ kill = getattr(process, "kill", None)
78
+ if callable(kill):
79
+ kill()
80
+ wait(timeout=5)
81
+
82
+
83
+ def initialize_connection(
84
+ username: str | int | None,
85
+ password: str | None,
86
+ server: str | None,
87
+ path: str | None = None,
88
+ portable: bool | None = None,
89
+ ) -> tuple[bool, str]:
90
+ init_kwargs: dict[str, Any] = {}
91
+ if path:
92
+ init_kwargs["portable"] = True if portable is None else portable
93
+ elif portable is not None:
94
+ init_kwargs["portable"] = portable
95
+ if username not in (None, ""):
96
+ try:
97
+ init_kwargs["login"] = int(username)
98
+ except ValueError:
99
+ return False, "failed: username/login must be a number"
100
+ if password:
101
+ init_kwargs["password"] = password
102
+ if server:
103
+ init_kwargs["server"] = server
104
+
105
+ if path:
106
+ mt5.shutdown()
107
+ _close_launched_terminal()
108
+ launched, launch_error = _launch_terminal(path, bool(init_kwargs["portable"]))
109
+ if not launched:
110
+ return False, f"failed to launch terminal '{path}': {launch_error}"
111
+
112
+ initialized = mt5.initialize(path, **init_kwargs) if path else mt5.initialize(**init_kwargs)
113
+ if initialized:
114
+ if path and not _connected_to_requested_terminal(path):
115
+ connected_path = getattr(mt5.terminal_info(), "path", "unknown")
116
+ mt5.shutdown()
117
+ _close_launched_terminal()
118
+ return False, f"failed: connected to terminal at '{connected_path}' instead of requested path '{path}'"
119
+ return True, "connected"
120
+ return False, f"failed: {mt5.last_error()}"
121
+
122
+
123
+ def shutdown_connection() -> tuple[bool, str]:
124
+ mt5.shutdown()
125
+ _close_launched_terminal()
126
+ if mt5.terminal_info() is None:
127
+ return True, "disconnected"
128
+ return False, "failed: terminal still connected"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-api-mt5
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: REST and WebSocket API project for MetaTrader 5
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "open-api-mt5"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  description = "REST and WebSocket API project for MetaTrader 5"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+
7
+ from app.services import connection_service
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def reset_launched_terminal_process():
12
+ original = connection_service._launched_terminal_process
13
+ connection_service._launched_terminal_process = None
14
+ yield
15
+ connection_service._launched_terminal_process = original
16
+
17
+
18
+ class FakeProcess:
19
+ def __init__(self) -> None:
20
+ self.terminated = False
21
+ self.killed = False
22
+ self.waited = False
23
+ self.returncode = None
24
+
25
+ def poll(self):
26
+ return self.returncode
27
+
28
+ def terminate(self):
29
+ self.terminated = True
30
+ self.returncode = 0
31
+
32
+ def kill(self):
33
+ self.killed = True
34
+ self.returncode = -9
35
+
36
+ def wait(self, timeout=None):
37
+ self.waited = True
38
+ return self.returncode
39
+
40
+
41
+ def _capture_terminal_launch(monkeypatch):
42
+ launched = []
43
+ monkeypatch.setattr(connection_service.subprocess, "Popen", lambda args: launched.append(args) or FakeProcess())
44
+ return launched
45
+
46
+
47
+ def test_initialize_connection_uses_portable_mode_with_terminal_path(monkeypatch):
48
+ captured = {"args": None, "kwargs": None}
49
+ launched = _capture_terminal_launch(monkeypatch)
50
+
51
+ def initialize(*args, **kwargs):
52
+ captured["args"] = args
53
+ captured["kwargs"] = kwargs
54
+ return True
55
+
56
+ monkeypatch.setattr(connection_service.mt5, "initialize", initialize, raising=False)
57
+ monkeypatch.setattr(
58
+ connection_service.mt5,
59
+ "terminal_info",
60
+ lambda: SimpleNamespace(path=r"C:\MT5-A"),
61
+ raising=False,
62
+ )
63
+
64
+ assert connection_service.initialize_connection(
65
+ username="123",
66
+ password="secret",
67
+ server="Broker-Server",
68
+ path=r"C:\MT5-A\terminal64.exe",
69
+ ) == (True, "connected")
70
+ assert launched == [[r"C:\MT5-A\terminal64.exe", "/portable"]]
71
+ assert captured["args"] == (r"C:\MT5-A\terminal64.exe",)
72
+ assert captured["kwargs"] == {
73
+ "portable": True,
74
+ "login": 123,
75
+ "password": "secret",
76
+ "server": "Broker-Server",
77
+ }
78
+
79
+
80
+ def test_initialize_connection_allows_disabling_portable_mode(monkeypatch):
81
+ captured = {"args": None, "kwargs": None}
82
+ launched = _capture_terminal_launch(monkeypatch)
83
+
84
+ def initialize(*args, **kwargs):
85
+ captured["args"] = args
86
+ captured["kwargs"] = kwargs
87
+ return True
88
+
89
+ monkeypatch.setattr(
90
+ connection_service.mt5,
91
+ "initialize",
92
+ initialize,
93
+ raising=False,
94
+ )
95
+ monkeypatch.setattr(
96
+ connection_service.mt5,
97
+ "terminal_info",
98
+ lambda: SimpleNamespace(path=r"C:\MT5-A"),
99
+ raising=False,
100
+ )
101
+
102
+ assert connection_service.initialize_connection(
103
+ username="123",
104
+ password=None,
105
+ server=None,
106
+ path=r"C:\MT5-A\terminal64.exe",
107
+ portable=False,
108
+ ) == (True, "connected")
109
+ assert launched == [[r"C:\MT5-A\terminal64.exe"]]
110
+ assert captured["args"] == (r"C:\MT5-A\terminal64.exe",)
111
+ assert captured["kwargs"] == {
112
+ "portable": False,
113
+ "login": 123,
114
+ }
115
+
116
+
117
+ def test_initialize_connection_rejects_wrong_running_terminal(monkeypatch):
118
+ shutdown_called = False
119
+ _capture_terminal_launch(monkeypatch)
120
+
121
+ monkeypatch.setattr(connection_service.mt5, "initialize", lambda *_args, **_kwargs: True, raising=False)
122
+ monkeypatch.setattr(
123
+ connection_service.mt5,
124
+ "terminal_info",
125
+ lambda: SimpleNamespace(path=r"C:\MT5-B"),
126
+ raising=False,
127
+ )
128
+
129
+ def shutdown():
130
+ nonlocal shutdown_called
131
+ shutdown_called = True
132
+
133
+ monkeypatch.setattr(connection_service.mt5, "shutdown", shutdown, raising=False)
134
+
135
+ ok, message = connection_service.initialize_connection(
136
+ username="123",
137
+ password=None,
138
+ server=None,
139
+ path=r"C:\MT5-A\terminal64.exe",
140
+ )
141
+
142
+ assert ok is False
143
+ assert "instead of requested path" in message
144
+ assert shutdown_called is True
145
+
146
+
147
+ def test_shutdown_connection_closes_launched_terminal(monkeypatch):
148
+ process = FakeProcess()
149
+ monkeypatch.setattr(connection_service, "_launched_terminal_process", process)
150
+ monkeypatch.setattr(connection_service.mt5, "shutdown", lambda: None, raising=False)
151
+ monkeypatch.setattr(connection_service.mt5, "terminal_info", lambda: None, raising=False)
152
+
153
+ assert connection_service.shutdown_connection() == (True, "disconnected")
154
+ assert process.terminated is True
155
+ assert process.waited is True
156
+ assert process.killed is False
157
+ assert connection_service._launched_terminal_process is None
@@ -1,41 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- import MetaTrader5 as mt5
6
-
7
-
8
- def initialize_connection(
9
- username: str | int | None,
10
- password: str | None,
11
- server: str | None,
12
- path: str | None = None,
13
- portable: bool | None = None,
14
- ) -> tuple[bool, str]:
15
- init_kwargs: dict[str, Any] = {}
16
- if path:
17
- init_kwargs["path"] = path
18
- init_kwargs["portable"] = True if portable is None else portable
19
- elif portable is not None:
20
- init_kwargs["portable"] = portable
21
- if username not in (None, ""):
22
- try:
23
- init_kwargs["login"] = int(username)
24
- except ValueError:
25
- return False, "failed: username/login must be a number"
26
- if password:
27
- init_kwargs["password"] = password
28
- if server:
29
- init_kwargs["server"] = server
30
-
31
- initialized = mt5.initialize(**init_kwargs)
32
- if initialized:
33
- return True, "connected"
34
- return False, f"failed: {mt5.last_error()}"
35
-
36
-
37
- def shutdown_connection() -> tuple[bool, str]:
38
- mt5.shutdown()
39
- if mt5.terminal_info() is None:
40
- return True, "disconnected"
41
- return False, "failed: terminal still connected"
@@ -1,50 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from app.services import connection_service
4
-
5
-
6
- def test_initialize_connection_uses_portable_mode_with_terminal_path(monkeypatch):
7
- captured = {}
8
-
9
- def initialize(**kwargs):
10
- captured.update(kwargs)
11
- return True
12
-
13
- monkeypatch.setattr(connection_service.mt5, "initialize", initialize, raising=False)
14
-
15
- assert connection_service.initialize_connection(
16
- username="123",
17
- password="secret",
18
- server="Broker-Server",
19
- path=r"C:\MT5-A\terminal64.exe",
20
- ) == (True, "connected")
21
- assert captured == {
22
- "path": r"C:\MT5-A\terminal64.exe",
23
- "portable": True,
24
- "login": 123,
25
- "password": "secret",
26
- "server": "Broker-Server",
27
- }
28
-
29
-
30
- def test_initialize_connection_allows_disabling_portable_mode(monkeypatch):
31
- captured = {}
32
- monkeypatch.setattr(
33
- connection_service.mt5,
34
- "initialize",
35
- lambda **kwargs: captured.update(kwargs) or True,
36
- raising=False,
37
- )
38
-
39
- assert connection_service.initialize_connection(
40
- username="123",
41
- password=None,
42
- server=None,
43
- path=r"C:\MT5-A\terminal64.exe",
44
- portable=False,
45
- ) == (True, "connected")
46
- assert captured == {
47
- "path": r"C:\MT5-A\terminal64.exe",
48
- "portable": False,
49
- "login": 123,
50
- }
File without changes
File without changes
File without changes