mt5api 0.1.0__tar.gz → 0.2.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.
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/local-qa/SKILL.md +1 -1
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/local-qa/scripts/qa.sh +1 -1
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/mt5api/SKILL.md +73 -2
- {mt5api-0.1.0 → mt5api-0.2.0}/.github/workflows/ci.yml +1 -22
- mt5api-0.2.0/.github/workflows/release.yml +44 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/PKG-INFO +28 -2
- {mt5api-0.1.0 → mt5api-0.2.0}/README.md +27 -1
- {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/rest-api.md +19 -3
- mt5api-0.2.0/mt5api/dependencies.py +236 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/main.py +13 -30
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/models.py +56 -0
- mt5api-0.2.0/mt5api/routers/__init__.py +16 -0
- mt5api-0.2.0/mt5api/routers/connection.py +72 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/pyproject.toml +1 -1
- mt5api-0.2.0/tests/test_connection.py +467 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_main.py +22 -27
- {mt5api-0.1.0 → mt5api-0.2.0}/uv.lock +59 -59
- mt5api-0.1.0/mt5api/dependencies.py +0 -132
- mt5api-0.1.0/mt5api/routers/__init__.py +0 -7
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-analyze/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-checklist/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-clarify/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-constitution/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-implement/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-plan/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-specify/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-tasks/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-taskstoissues/SKILL.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/agents/codex.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/agents/copilot.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.analyze.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.checklist.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.clarify.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.constitution.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.implement.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.plan.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.specify.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.tasks.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.taskstoissues.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.claude/settings.json +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.github/FUNDING.yml +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.github/dependabot.yml +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.github/renovate.json +0 -0
- /mt5api-0.1.0/.github/workflows/claude-code.yml → /mt5api-0.2.0/.github/workflows/claude.yml +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.gitignore +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/memory/constitution.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/common.sh +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/setup-plan.sh +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/agent-file-template.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/checklist-template.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/plan-template.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/spec-template.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/tasks-template.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/AGENTS.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/CLAUDE.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/LICENSE +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/deployment.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/index.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/docs/index.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mkdocs.yml +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/__init__.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/__main__.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/auth.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/config.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/constants.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/formatters.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/middleware.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/account.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/calc.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/health.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/history.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/market.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/symbols.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/trading.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/contracts/openapi.yaml +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/data-model.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/plan.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/quickstart.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/research.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/tasks.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/042-mt5-core-client/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/043-mt5-dataframe-client/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/044-mt5-trading-client/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/045-mt5-utils/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/046-mt5-api-service/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/047-api-auth-rate-limit/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/048-api-response-format/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/049-api-routing-models/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/050-api-runtime-deploy/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/051-api-error-logging/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/specs/052-api-dependencies/spec.md +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/__init__.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/conftest.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/mt5_constants.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_account.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_auth.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_calc.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_cli.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_config.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_dependencies.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_formatters.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_health.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_history.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_integration.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_lifespan.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_market.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_middleware.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_models.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_symbols.py +0 -0
- {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_trading.py +0 -0
|
@@ -3,8 +3,8 @@ name: mt5api
|
|
|
3
3
|
description: >-
|
|
4
4
|
Query the MT5 API for account info, terminal status, health checks, symbol
|
|
5
5
|
data, market data (OHLCV rates, ticks, market depth), open positions, pending
|
|
6
|
-
orders,
|
|
7
|
-
endpoint.
|
|
6
|
+
orders, trade history, and reconnecting the MT5 terminal with new
|
|
7
|
+
credentials. Use when the user wants to interact with any mt5api endpoint.
|
|
8
8
|
allowed-tools: Bash
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -30,6 +30,16 @@ if [ -n "${MT5API_SECRET_KEY:-}" ]; then
|
|
|
30
30
|
fi
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Login credentials for `/connection/login` are read from the environment so
|
|
34
|
+
the password is never hard-coded in a command:
|
|
35
|
+
|
|
36
|
+
| Variable | Description | Required for login |
|
|
37
|
+
| ----------------- | ------------------------------------------ | ------------------ |
|
|
38
|
+
| `MT5API_LOGIN` | Trading account login (integer) | yes |
|
|
39
|
+
| `MT5API_PASSWORD` | Trading account password | yes |
|
|
40
|
+
| `MT5API_SERVER` | Trading server name (e.g., `Broker-Demo`) | yes |
|
|
41
|
+
| `MT5API_TIMEOUT` | Connection timeout in milliseconds (`> 0`) | no |
|
|
42
|
+
|
|
33
43
|
## Response Formats
|
|
34
44
|
|
|
35
45
|
All endpoints (except `/health`) return JSON by default. Request Parquet with
|
|
@@ -44,6 +54,11 @@ All endpoints (except `/health`) return JSON by default. Request Parquet with
|
|
|
44
54
|
symbol or skipping DOM data.
|
|
45
55
|
- Empty arrays from `/positions`, `/orders`, `/history/orders`, or
|
|
46
56
|
`/history/deals` are valid results.
|
|
57
|
+
- `/connection/login` reconnects the shared MT5 client to a different account.
|
|
58
|
+
It shuts down the current connection and releases any active market-book
|
|
59
|
+
subscriptions before logging in. Never echo the password back to the user
|
|
60
|
+
and do not log it; the response only confirms `login`, `server`, `timeout`,
|
|
61
|
+
and `connected`.
|
|
47
62
|
|
|
48
63
|
---
|
|
49
64
|
|
|
@@ -77,6 +92,59 @@ curl -s "${AUTH_HEADER[@]}" \
|
|
|
77
92
|
|
|
78
93
|
---
|
|
79
94
|
|
|
95
|
+
## Connection
|
|
96
|
+
|
|
97
|
+
### Reconnect to MT5
|
|
98
|
+
|
|
99
|
+
Shut down the current MT5 client and reconnect with new credentials. Active
|
|
100
|
+
market-book subscriptions are released first. The call is serialized so
|
|
101
|
+
concurrent reconnect attempts do not race. The password is sent only in the
|
|
102
|
+
request body and is never echoed in the response.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Build JSON body from env vars; include timeout only when set
|
|
106
|
+
LOGIN_BODY=$(python3 -c "
|
|
107
|
+
import json, os, sys
|
|
108
|
+
body = {
|
|
109
|
+
'login': int(os.environ['MT5API_LOGIN']),
|
|
110
|
+
'password': os.environ['MT5API_PASSWORD'],
|
|
111
|
+
'server': os.environ['MT5API_SERVER'],
|
|
112
|
+
}
|
|
113
|
+
t = os.environ.get('MT5API_TIMEOUT')
|
|
114
|
+
if t:
|
|
115
|
+
body['timeout'] = int(t)
|
|
116
|
+
print(json.dumps(body))
|
|
117
|
+
")
|
|
118
|
+
curl -s -X POST "${AUTH_HEADER[@]}" \
|
|
119
|
+
-H 'Content-Type: application/json' \
|
|
120
|
+
-d "${LOGIN_BODY}" \
|
|
121
|
+
"${MT5API_URL}/connection/login" | python -m json.tool
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Request body:
|
|
125
|
+
|
|
126
|
+
| Field | Type | Required | Description |
|
|
127
|
+
| -------- | ------ | -------- | ------------------------------------------ |
|
|
128
|
+
| login | int | yes | Trading account login (positive integer) |
|
|
129
|
+
| password | string | yes | Trading account password (never echoed) |
|
|
130
|
+
| server | string | yes | Trading server name (e.g., `Broker-Demo`) |
|
|
131
|
+
| timeout | int | no | Connection timeout in milliseconds (`> 0`) |
|
|
132
|
+
|
|
133
|
+
Successful response (`200`):
|
|
134
|
+
|
|
135
|
+
| Field | Type | Description |
|
|
136
|
+
| --------- | ------ | -------------------------------------------- |
|
|
137
|
+
| login | int | Login that was used to connect |
|
|
138
|
+
| server | string | Trading server that was connected to |
|
|
139
|
+
| timeout | int? | Timeout in milliseconds if one was specified |
|
|
140
|
+
| connected | bool | `true` when the new connection succeeded |
|
|
141
|
+
|
|
142
|
+
On failure, MT5 errors surface as `503 Service Unavailable` with an RFC 7807
|
|
143
|
+
problem-details body. Never include the supplied password in any summary or
|
|
144
|
+
diagnostic you return to the user.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
80
148
|
## Account & Terminal
|
|
81
149
|
|
|
82
150
|
### Account Info
|
|
@@ -323,3 +391,6 @@ Either `(date_from AND date_to)` or `(ticket OR position)` must be provided.
|
|
|
323
391
|
running or reachable.
|
|
324
392
|
9. For historical queries, remind the user that either a date range or a
|
|
325
393
|
ticket/position filter is required.
|
|
394
|
+
10. For `/connection/login`, always send the password in the POST body and
|
|
395
|
+
never repeat it in any reply or log message. If the user asks you to
|
|
396
|
+
reconnect, confirm the target `login`/`server` before sending the request.
|
|
@@ -19,7 +19,6 @@ on:
|
|
|
19
19
|
options:
|
|
20
20
|
- lint-and-test
|
|
21
21
|
- docs-deploy
|
|
22
|
-
- release
|
|
23
22
|
description: Choose the workflow to run
|
|
24
23
|
default: lint-and-test
|
|
25
24
|
permissions:
|
|
@@ -54,10 +53,7 @@ jobs:
|
|
|
54
53
|
python-docs-deploy:
|
|
55
54
|
if: >
|
|
56
55
|
github.event_name == 'push'
|
|
57
|
-
|| (
|
|
58
|
-
github.event_name == 'workflow_dispatch'
|
|
59
|
-
&& (inputs.workflow == 'docs-deploy' || inputs.workflow == 'release')
|
|
60
|
-
)
|
|
56
|
+
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'docs-deploy')
|
|
61
57
|
permissions:
|
|
62
58
|
contents: write
|
|
63
59
|
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-mkdocs-gh-deploy.yml@main # zizmor: ignore[unpinned-uses]
|
|
@@ -66,23 +62,6 @@ jobs:
|
|
|
66
62
|
runs-on: ubuntu-slim
|
|
67
63
|
secrets:
|
|
68
64
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
69
|
-
python-package-release:
|
|
70
|
-
if: >
|
|
71
|
-
github.event_name == 'push'
|
|
72
|
-
|| (
|
|
73
|
-
github.event_name == 'workflow_dispatch'
|
|
74
|
-
&& (inputs.workflow == 'release' || inputs.workflow == 'lint-and-test')
|
|
75
|
-
)
|
|
76
|
-
permissions:
|
|
77
|
-
contents: write
|
|
78
|
-
id-token: write
|
|
79
|
-
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
|
|
80
|
-
with:
|
|
81
|
-
package-path: .
|
|
82
|
-
create-releases: ${{ github.event_name == 'workflow_dispatch' && inputs.workflow == 'release' }}
|
|
83
|
-
secrets:
|
|
84
|
-
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
85
|
-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
86
65
|
dependabot-auto-merge:
|
|
87
66
|
if: >
|
|
88
67
|
github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Release
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
defaults:
|
|
8
|
+
run:
|
|
9
|
+
shell: bash -euo pipefail {0}
|
|
10
|
+
working-directory: .
|
|
11
|
+
jobs:
|
|
12
|
+
build-and-release:
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
id-token: write
|
|
16
|
+
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
|
|
17
|
+
with:
|
|
18
|
+
package-path: .
|
|
19
|
+
create-github-release: true
|
|
20
|
+
publish-to-pypi: false
|
|
21
|
+
secrets:
|
|
22
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
publish-to-pypi:
|
|
24
|
+
name: Publish the Python 🐍 distribution 📦 to PyPI
|
|
25
|
+
if: >
|
|
26
|
+
startsWith(github.ref, 'refs/tags/')
|
|
27
|
+
needs:
|
|
28
|
+
- build-and-release
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment:
|
|
31
|
+
name: pypi
|
|
32
|
+
url: https://pypi.org/p/${{ needs.build-and-release.outputs.project-name }}
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
35
|
+
steps:
|
|
36
|
+
- name: Download all the dists
|
|
37
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
38
|
+
with:
|
|
39
|
+
name: ${{ needs.build-and-release.outputs.distribution-artifact-name }}
|
|
40
|
+
path: dist/
|
|
41
|
+
- name: Publish distribution 📦 to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
43
|
+
with:
|
|
44
|
+
verbose: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: MetaTrader 5 REST API
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5api.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -89,6 +89,20 @@ graph TB
|
|
|
89
89
|
|
|
90
90
|
Install and run the API on the Windows machine where MetaTrader 5 is installed.
|
|
91
91
|
|
|
92
|
+
Install the latest release from PyPI:
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
pip install mt5api
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or, when managing the project with `uv`, add it as a dependency:
|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
uv add mt5api
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Alternatively, install from source:
|
|
105
|
+
|
|
92
106
|
```powershell
|
|
93
107
|
git clone https://github.com/dceoy/mt5api.git
|
|
94
108
|
cd mt5api
|
|
@@ -97,12 +111,24 @@ uv sync
|
|
|
97
111
|
|
|
98
112
|
## Running the API on Windows
|
|
99
113
|
|
|
114
|
+
After installing from PyPI:
|
|
115
|
+
|
|
100
116
|
```powershell
|
|
101
117
|
$env:MT5API_SECRET_KEY = "your-secret-api-key" # Optional: omit to disable auth
|
|
102
118
|
$env:MT5API_ROUTER_PREFIX = "/api/v1" # Optional: omit for root-level routes
|
|
103
|
-
|
|
119
|
+
python -m mt5api
|
|
104
120
|
```
|
|
105
121
|
|
|
122
|
+
`python -m mt5api` reads `MT5API_HOST`, `MT5API_PORT`, and `MT5API_LOG_LEVEL`
|
|
123
|
+
from the environment. You can also invoke `uvicorn` directly:
|
|
124
|
+
|
|
125
|
+
```powershell
|
|
126
|
+
uvicorn mt5api.main:app --host 0.0.0.0 --port 8000
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If you cloned the source tree, prepend `uv run` to either command (for example,
|
|
130
|
+
`uv run uvicorn mt5api.main:app --host 0.0.0.0 --port 8000`).
|
|
131
|
+
|
|
106
132
|
Docs:
|
|
107
133
|
|
|
108
134
|
- Swagger UI: `http://localhost:8000/docs`
|
|
@@ -60,6 +60,20 @@ graph TB
|
|
|
60
60
|
|
|
61
61
|
Install and run the API on the Windows machine where MetaTrader 5 is installed.
|
|
62
62
|
|
|
63
|
+
Install the latest release from PyPI:
|
|
64
|
+
|
|
65
|
+
```console
|
|
66
|
+
pip install mt5api
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or, when managing the project with `uv`, add it as a dependency:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
uv add mt5api
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Alternatively, install from source:
|
|
76
|
+
|
|
63
77
|
```powershell
|
|
64
78
|
git clone https://github.com/dceoy/mt5api.git
|
|
65
79
|
cd mt5api
|
|
@@ -68,12 +82,24 @@ uv sync
|
|
|
68
82
|
|
|
69
83
|
## Running the API on Windows
|
|
70
84
|
|
|
85
|
+
After installing from PyPI:
|
|
86
|
+
|
|
71
87
|
```powershell
|
|
72
88
|
$env:MT5API_SECRET_KEY = "your-secret-api-key" # Optional: omit to disable auth
|
|
73
89
|
$env:MT5API_ROUTER_PREFIX = "/api/v1" # Optional: omit for root-level routes
|
|
74
|
-
|
|
90
|
+
python -m mt5api
|
|
75
91
|
```
|
|
76
92
|
|
|
93
|
+
`python -m mt5api` reads `MT5API_HOST`, `MT5API_PORT`, and `MT5API_LOG_LEVEL`
|
|
94
|
+
from the environment. You can also invoke `uvicorn` directly:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
uvicorn mt5api.main:app --host 0.0.0.0 --port 8000
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If you cloned the source tree, prepend `uv run` to either command (for example,
|
|
101
|
+
`uv run uvicorn mt5api.main:app --host 0.0.0.0 --port 8000`).
|
|
102
|
+
|
|
77
103
|
Docs:
|
|
78
104
|
|
|
79
105
|
- Swagger UI: `http://localhost:8000/docs`
|
|
@@ -116,9 +116,9 @@ If `MT5API_ROUTER_PREFIX` is configured, prepend it to each API route below.
|
|
|
116
116
|
- `GET /rates/range` (`symbol`, `timeframe`, `date_from`, `date_to`, `format`)
|
|
117
117
|
- `GET /ticks/from` (`symbol`, `date_from`, `count`, `flags`, `format`)
|
|
118
118
|
- `GET /ticks/range` (`symbol`, `date_from`, `date_to`, `flags`, `format`)
|
|
119
|
-
- `GET /market-book/{symbol}` (`format`) — Market depth (DOM)
|
|
120
|
-
- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events
|
|
121
|
-
- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events
|
|
119
|
+
- `GET /market-book/{symbol}` (`format`) — Market depth (DOM) _(experimental)_
|
|
120
|
+
- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events _(experimental)_
|
|
121
|
+
- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events _(experimental)_
|
|
122
122
|
|
|
123
123
|
### Calculations
|
|
124
124
|
|
|
@@ -162,6 +162,14 @@ structure](https://www.mql5.com/en/docs/constants/structures/mqltraderequest),
|
|
|
162
162
|
with typed validation for core fields such as `action`, `symbol`, `volume`,
|
|
163
163
|
`type`, and `price`.
|
|
164
164
|
|
|
165
|
+
### Connection
|
|
166
|
+
|
|
167
|
+
- `POST /connection/login` (body: `{"login": ..., "password": "...",
|
|
168
|
+
"server": "...", "timeout": ...}`) — Reconnect the MT5 terminal with the
|
|
169
|
+
supplied credentials. Any active market-book subscriptions are released and
|
|
170
|
+
the previous MT5 client is shut down before the new connection is
|
|
171
|
+
established. The supplied password is never echoed in responses or logs.
|
|
172
|
+
|
|
165
173
|
## Response Formatter Utilities
|
|
166
174
|
|
|
167
175
|
If you are extending the API with custom endpoints, use the formatter helpers
|
|
@@ -291,6 +299,14 @@ curl -X POST -H "X-API-Key: your-secret-api-key" -H "Content-Type: application/j
|
|
|
291
299
|
"http://windows-host:8000/order/check"
|
|
292
300
|
```
|
|
293
301
|
|
|
302
|
+
### Reconnect to MT5
|
|
303
|
+
|
|
304
|
+
```console
|
|
305
|
+
curl -X POST -H "X-API-Key: your-secret-api-key" -H "Content-Type: application/json" \
|
|
306
|
+
-d '{"login": 12345678, "password": "s3cret", "server": "MetaQuotes-Demo", "timeout": 60000}' \
|
|
307
|
+
"http://windows-host:8000/connection/login"
|
|
308
|
+
```
|
|
309
|
+
|
|
294
310
|
## Error Responses
|
|
295
311
|
|
|
296
312
|
Errors follow RFC 7807 Problem Details:
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""FastAPI dependency injection for MT5 client and format negotiation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, TypeVar
|
|
8
|
+
|
|
9
|
+
from fastapi import Header, Query, Request
|
|
10
|
+
from pdmt5.dataframe import Mt5Config, Mt5DataClient
|
|
11
|
+
|
|
12
|
+
from .constants import (
|
|
13
|
+
ACTIVE_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
|
|
14
|
+
MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY,
|
|
15
|
+
)
|
|
16
|
+
from .models import ResponseFormat
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Type variable for return types
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
# Global singleton instance
|
|
29
|
+
_mt5_client: Mt5DataClient | None = None
|
|
30
|
+
|
|
31
|
+
# Serializes singleton replacement across concurrent reconnect requests.
|
|
32
|
+
_mt5_client_lock = asyncio.Lock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_mt5_client_lock() -> asyncio.Lock:
|
|
36
|
+
"""Return the lock guarding MT5 client singleton replacement."""
|
|
37
|
+
return _mt5_client_lock
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_mt5_client() -> Mt5DataClient:
|
|
41
|
+
"""Get or create Mt5DataClient singleton instance.
|
|
42
|
+
|
|
43
|
+
This dependency provides a single shared MT5 client instance across all
|
|
44
|
+
requests to avoid multiple terminal connections.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Mt5DataClient: Singleton MT5 client instance.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: If MT5 client cannot be initialized.
|
|
51
|
+
"""
|
|
52
|
+
global _mt5_client # noqa: PLW0603
|
|
53
|
+
|
|
54
|
+
if _mt5_client is None:
|
|
55
|
+
config = Mt5Config()
|
|
56
|
+
_mt5_client = Mt5DataClient(config=config)
|
|
57
|
+
try:
|
|
58
|
+
_mt5_client.initialize_and_login_mt5()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
_mt5_client = None
|
|
61
|
+
error_message = f"Failed to initialize MT5 client: {e!s}"
|
|
62
|
+
raise RuntimeError(error_message) from e
|
|
63
|
+
|
|
64
|
+
return _mt5_client
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def shutdown_mt5_client() -> None:
|
|
68
|
+
"""Shutdown and cleanup MT5 client singleton.
|
|
69
|
+
|
|
70
|
+
This should be called during application shutdown to properly
|
|
71
|
+
close the MT5 connection.
|
|
72
|
+
"""
|
|
73
|
+
global _mt5_client # noqa: PLW0603
|
|
74
|
+
|
|
75
|
+
if _mt5_client is not None:
|
|
76
|
+
_mt5_client.shutdown()
|
|
77
|
+
_mt5_client = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def replace_mt5_client(config: Mt5Config) -> Mt5DataClient:
|
|
81
|
+
"""Swap the MT5 client singleton with a new connection.
|
|
82
|
+
|
|
83
|
+
Constructs and initializes the new ``Mt5DataClient`` BEFORE touching the
|
|
84
|
+
singleton so a failed reconnect leaves the existing client in place
|
|
85
|
+
instead of disconnecting the operator from a working terminal. The new
|
|
86
|
+
client is installed atomically; the old client (if any) is shut down on
|
|
87
|
+
a best-effort basis after the swap. Callers MUST hold
|
|
88
|
+
``get_mt5_client_lock`` so concurrent reconnect requests do not race.
|
|
89
|
+
|
|
90
|
+
The raised ``RuntimeError`` deliberately omits the underlying exception
|
|
91
|
+
text from its message because third-party ``pdmt5``/MetaTrader5
|
|
92
|
+
exceptions may include the connection config (and thus the password) in
|
|
93
|
+
their string form. The original exception is chained via ``raise from``
|
|
94
|
+
and logged server-side so diagnostics are preserved.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
config: Configuration for the new MT5 connection.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The newly initialized MT5 data client.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
RuntimeError: If the new MT5 client cannot be constructed or
|
|
104
|
+
initialized. The previously installed client is preserved.
|
|
105
|
+
"""
|
|
106
|
+
global _mt5_client # noqa: PLW0603
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
new_client = Mt5DataClient(config=config)
|
|
110
|
+
await asyncio.to_thread(new_client.initialize_and_login_mt5)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.exception("Failed to initialize MT5 client")
|
|
113
|
+
error_message = "Failed to initialize MT5 client"
|
|
114
|
+
raise RuntimeError(error_message) from e
|
|
115
|
+
|
|
116
|
+
old_client = _mt5_client
|
|
117
|
+
_mt5_client = new_client
|
|
118
|
+
|
|
119
|
+
if old_client is not None:
|
|
120
|
+
try:
|
|
121
|
+
await asyncio.to_thread(old_client.shutdown)
|
|
122
|
+
except Exception:
|
|
123
|
+
logger.exception("Failed to shutdown previous MT5 client")
|
|
124
|
+
|
|
125
|
+
return new_client
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def release_market_book_subscriptions(app: FastAPI) -> None:
|
|
129
|
+
"""Release tracked market-book subscriptions on the application.
|
|
130
|
+
|
|
131
|
+
Iterates the application's tracked subscription set, calls
|
|
132
|
+
``market_book_release`` for each symbol using the cleanup client, then
|
|
133
|
+
clears the tracking state. Individual release failures are logged and do
|
|
134
|
+
not stop processing.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
app: FastAPI application whose state holds the subscription set.
|
|
138
|
+
"""
|
|
139
|
+
subscriptions = getattr(
|
|
140
|
+
app.state,
|
|
141
|
+
ACTIVE_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
|
|
142
|
+
None,
|
|
143
|
+
)
|
|
144
|
+
if not isinstance(subscriptions, set) or not subscriptions:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
mt5_client = getattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
148
|
+
if mt5_client is None:
|
|
149
|
+
logger.warning("Active market-book subscriptions found without cleanup client")
|
|
150
|
+
subscriptions.clear()
|
|
151
|
+
setattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
def release_subscriptions() -> None:
|
|
155
|
+
for symbol in tuple(subscriptions):
|
|
156
|
+
try:
|
|
157
|
+
mt5_client.market_book_release(symbol=symbol)
|
|
158
|
+
except Exception:
|
|
159
|
+
logger.exception("Failed to release market book for %s", symbol)
|
|
160
|
+
|
|
161
|
+
await asyncio.to_thread(release_subscriptions)
|
|
162
|
+
|
|
163
|
+
subscriptions.clear()
|
|
164
|
+
setattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def run_in_threadpool(
|
|
168
|
+
func: Callable[..., T],
|
|
169
|
+
*args: Any, # noqa: ANN401
|
|
170
|
+
**kwargs: Any, # noqa: ANN401
|
|
171
|
+
) -> T:
|
|
172
|
+
"""Run synchronous MT5 function in thread pool.
|
|
173
|
+
|
|
174
|
+
MT5 API calls are synchronous and blocking. This wrapper runs them
|
|
175
|
+
in a thread pool to avoid blocking the async event loop.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
func: Synchronous function to run.
|
|
179
|
+
*args: Positional arguments for the function.
|
|
180
|
+
**kwargs: Keyword arguments for the function.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The function's return value.
|
|
184
|
+
"""
|
|
185
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_response_format(
|
|
189
|
+
accept: Annotated[str | None, Header()] = None,
|
|
190
|
+
format_param: Annotated[ResponseFormat | None, Query(alias="format")] = None,
|
|
191
|
+
) -> ResponseFormat:
|
|
192
|
+
"""Determine response format from Accept header or query parameter.
|
|
193
|
+
|
|
194
|
+
Priority:
|
|
195
|
+
1. Query parameter (?format=json or ?format=parquet)
|
|
196
|
+
2. Accept header (application/json or application/parquet)
|
|
197
|
+
3. Default to JSON
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
accept: Accept header from request.
|
|
201
|
+
format_param: Format query parameter.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
ResponseFormat: Negotiated response format.
|
|
205
|
+
"""
|
|
206
|
+
# Query parameter takes priority
|
|
207
|
+
if format_param is not None:
|
|
208
|
+
return format_param
|
|
209
|
+
|
|
210
|
+
# Check Accept header
|
|
211
|
+
if accept:
|
|
212
|
+
accept_lower = accept.lower()
|
|
213
|
+
if "application/parquet" in accept_lower:
|
|
214
|
+
return ResponseFormat.PARQUET
|
|
215
|
+
if "application/json" in accept_lower:
|
|
216
|
+
return ResponseFormat.JSON
|
|
217
|
+
|
|
218
|
+
# Default to JSON
|
|
219
|
+
return ResponseFormat.JSON
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_request_info(request: Request) -> dict[str, Any]:
|
|
223
|
+
"""Extract request information for logging.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
request: FastAPI request object.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Dictionary with request details.
|
|
230
|
+
"""
|
|
231
|
+
return {
|
|
232
|
+
"method": request.method,
|
|
233
|
+
"url": str(request.url),
|
|
234
|
+
"client": request.client.host if request.client else None,
|
|
235
|
+
"user_agent": request.headers.get("user-agent"),
|
|
236
|
+
}
|
|
@@ -29,9 +29,18 @@ from .constants import (
|
|
|
29
29
|
MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY,
|
|
30
30
|
MAX_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
|
|
31
31
|
)
|
|
32
|
-
from .dependencies import
|
|
32
|
+
from .dependencies import release_market_book_subscriptions, shutdown_mt5_client
|
|
33
33
|
from .middleware import add_middleware
|
|
34
|
-
from .routers import
|
|
34
|
+
from .routers import (
|
|
35
|
+
account,
|
|
36
|
+
calc,
|
|
37
|
+
connection,
|
|
38
|
+
health,
|
|
39
|
+
history,
|
|
40
|
+
market,
|
|
41
|
+
symbols,
|
|
42
|
+
trading,
|
|
43
|
+
)
|
|
35
44
|
|
|
36
45
|
if TYPE_CHECKING:
|
|
37
46
|
from collections.abc import AsyncGenerator
|
|
@@ -122,33 +131,6 @@ _configure_logging()
|
|
|
122
131
|
logger = logging.getLogger(__name__)
|
|
123
132
|
|
|
124
133
|
|
|
125
|
-
async def _release_market_book_subscriptions(app: FastAPI) -> None:
|
|
126
|
-
"""Release active market-book subscriptions before shutting down MT5."""
|
|
127
|
-
subscriptions = getattr(
|
|
128
|
-
app.state,
|
|
129
|
-
ACTIVE_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
|
|
130
|
-
None,
|
|
131
|
-
)
|
|
132
|
-
if not isinstance(subscriptions, set) or not subscriptions:
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
mt5_client = getattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
136
|
-
if mt5_client is None:
|
|
137
|
-
logger.warning("Active market-book subscriptions found without cleanup client")
|
|
138
|
-
subscriptions.clear()
|
|
139
|
-
setattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
for symbol in tuple(sorted(subscriptions)):
|
|
143
|
-
try:
|
|
144
|
-
await run_in_threadpool(mt5_client.market_book_release, symbol=symbol)
|
|
145
|
-
except Exception:
|
|
146
|
-
logger.exception("Failed to release market book for %s", symbol)
|
|
147
|
-
|
|
148
|
-
subscriptions.clear()
|
|
149
|
-
setattr(app.state, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, None)
|
|
150
|
-
|
|
151
|
-
|
|
152
134
|
@asynccontextmanager
|
|
153
135
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
154
136
|
"""Manage application lifespan (startup and shutdown).
|
|
@@ -178,7 +160,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
178
160
|
# Shutdown
|
|
179
161
|
logger.info("Shutting down MT5 REST API...")
|
|
180
162
|
try:
|
|
181
|
-
await
|
|
163
|
+
await release_market_book_subscriptions(app)
|
|
182
164
|
finally:
|
|
183
165
|
shutdown_mt5_client()
|
|
184
166
|
logger.info("MT5 connection closed")
|
|
@@ -208,5 +190,6 @@ app.include_router(account.router, prefix=router_prefix)
|
|
|
208
190
|
app.include_router(history.router, prefix=router_prefix)
|
|
209
191
|
app.include_router(calc.router, prefix=router_prefix)
|
|
210
192
|
app.include_router(trading.router, prefix=router_prefix)
|
|
193
|
+
app.include_router(connection.router, prefix=router_prefix)
|
|
211
194
|
|
|
212
195
|
logger.info("MT5 REST API initialized")
|