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.
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/PKG-INFO +1 -1
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/main.py +1 -1
- open_api_mt5-0.7.4/app/services/connection_service.py +128 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/PKG-INFO +1 -1
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/pyproject.toml +1 -1
- open_api_mt5-0.7.4/tests/test_connection_service.py +157 -0
- open_api_mt5-0.7.2/app/services/connection_service.py +0 -41
- open_api_mt5-0.7.2/tests/test_connection_service.py +0 -50
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/README.md +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/__init__.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/cli.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/__init__.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/account_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/bar_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/calendar_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/config_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/exposure_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/market_data_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/news_close_store.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/registry_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/serialization.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/stream_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/symbol_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/ticket_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/app/services/trade_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/SOURCES.txt +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/dependency_links.txt +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/entry_points.txt +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/requires.txt +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/open_api_mt5.egg-info/top_level.txt +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/setup.cfg +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_bar_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_bars_api.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_calendar_api.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_calendar_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_config_service.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_trade_api.py +0 -0
- {open_api_mt5-0.7.2 → open_api_mt5-0.7.4}/tests/test_trade_service.py +0 -0
|
@@ -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"
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|