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.
Files changed (114) hide show
  1. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/local-qa/SKILL.md +1 -1
  2. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/local-qa/scripts/qa.sh +1 -1
  3. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/mt5api/SKILL.md +73 -2
  4. {mt5api-0.1.0 → mt5api-0.2.0}/.github/workflows/ci.yml +1 -22
  5. mt5api-0.2.0/.github/workflows/release.yml +44 -0
  6. {mt5api-0.1.0 → mt5api-0.2.0}/PKG-INFO +28 -2
  7. {mt5api-0.1.0 → mt5api-0.2.0}/README.md +27 -1
  8. {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/rest-api.md +19 -3
  9. mt5api-0.2.0/mt5api/dependencies.py +236 -0
  10. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/main.py +13 -30
  11. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/models.py +56 -0
  12. mt5api-0.2.0/mt5api/routers/__init__.py +16 -0
  13. mt5api-0.2.0/mt5api/routers/connection.py +72 -0
  14. {mt5api-0.1.0 → mt5api-0.2.0}/pyproject.toml +1 -1
  15. mt5api-0.2.0/tests/test_connection.py +467 -0
  16. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_main.py +22 -27
  17. {mt5api-0.1.0 → mt5api-0.2.0}/uv.lock +59 -59
  18. mt5api-0.1.0/mt5api/dependencies.py +0 -132
  19. mt5api-0.1.0/mt5api/routers/__init__.py +0 -7
  20. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-analyze/SKILL.md +0 -0
  21. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-checklist/SKILL.md +0 -0
  22. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-clarify/SKILL.md +0 -0
  23. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-constitution/SKILL.md +0 -0
  24. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-implement/SKILL.md +0 -0
  25. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-plan/SKILL.md +0 -0
  26. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-specify/SKILL.md +0 -0
  27. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-tasks/SKILL.md +0 -0
  28. {mt5api-0.1.0 → mt5api-0.2.0}/.agents/skills/speckit-taskstoissues/SKILL.md +0 -0
  29. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/agents/codex.md +0 -0
  30. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/agents/copilot.md +0 -0
  31. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.analyze.md +0 -0
  32. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.checklist.md +0 -0
  33. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.clarify.md +0 -0
  34. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.constitution.md +0 -0
  35. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.implement.md +0 -0
  36. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.plan.md +0 -0
  37. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.specify.md +0 -0
  38. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.tasks.md +0 -0
  39. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/commands/speckit.taskstoissues.md +0 -0
  40. {mt5api-0.1.0 → mt5api-0.2.0}/.claude/settings.json +0 -0
  41. {mt5api-0.1.0 → mt5api-0.2.0}/.github/FUNDING.yml +0 -0
  42. {mt5api-0.1.0 → mt5api-0.2.0}/.github/dependabot.yml +0 -0
  43. {mt5api-0.1.0 → mt5api-0.2.0}/.github/renovate.json +0 -0
  44. /mt5api-0.1.0/.github/workflows/claude-code.yml → /mt5api-0.2.0/.github/workflows/claude.yml +0 -0
  45. {mt5api-0.1.0 → mt5api-0.2.0}/.gitignore +0 -0
  46. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/memory/constitution.md +0 -0
  47. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  48. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/common.sh +0 -0
  49. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/create-new-feature.sh +0 -0
  50. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/setup-plan.sh +0 -0
  51. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/scripts/bash/update-agent-context.sh +0 -0
  52. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/agent-file-template.md +0 -0
  53. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/checklist-template.md +0 -0
  54. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/plan-template.md +0 -0
  55. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/spec-template.md +0 -0
  56. {mt5api-0.1.0 → mt5api-0.2.0}/.specify/templates/tasks-template.md +0 -0
  57. {mt5api-0.1.0 → mt5api-0.2.0}/AGENTS.md +0 -0
  58. {mt5api-0.1.0 → mt5api-0.2.0}/CLAUDE.md +0 -0
  59. {mt5api-0.1.0 → mt5api-0.2.0}/LICENSE +0 -0
  60. {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/deployment.md +0 -0
  61. {mt5api-0.1.0 → mt5api-0.2.0}/docs/api/index.md +0 -0
  62. {mt5api-0.1.0 → mt5api-0.2.0}/docs/index.md +0 -0
  63. {mt5api-0.1.0 → mt5api-0.2.0}/mkdocs.yml +0 -0
  64. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/__init__.py +0 -0
  65. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/__main__.py +0 -0
  66. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/auth.py +0 -0
  67. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/config.py +0 -0
  68. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/constants.py +0 -0
  69. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/formatters.py +0 -0
  70. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/middleware.py +0 -0
  71. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/account.py +0 -0
  72. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/calc.py +0 -0
  73. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/health.py +0 -0
  74. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/history.py +0 -0
  75. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/market.py +0 -0
  76. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/symbols.py +0 -0
  77. {mt5api-0.1.0 → mt5api-0.2.0}/mt5api/routers/trading.py +0 -0
  78. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/contracts/openapi.yaml +0 -0
  79. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/data-model.md +0 -0
  80. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/plan.md +0 -0
  81. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/quickstart.md +0 -0
  82. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/research.md +0 -0
  83. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/spec.md +0 -0
  84. {mt5api-0.1.0 → mt5api-0.2.0}/specs/041-mt5-rest-api/tasks.md +0 -0
  85. {mt5api-0.1.0 → mt5api-0.2.0}/specs/042-mt5-core-client/spec.md +0 -0
  86. {mt5api-0.1.0 → mt5api-0.2.0}/specs/043-mt5-dataframe-client/spec.md +0 -0
  87. {mt5api-0.1.0 → mt5api-0.2.0}/specs/044-mt5-trading-client/spec.md +0 -0
  88. {mt5api-0.1.0 → mt5api-0.2.0}/specs/045-mt5-utils/spec.md +0 -0
  89. {mt5api-0.1.0 → mt5api-0.2.0}/specs/046-mt5-api-service/spec.md +0 -0
  90. {mt5api-0.1.0 → mt5api-0.2.0}/specs/047-api-auth-rate-limit/spec.md +0 -0
  91. {mt5api-0.1.0 → mt5api-0.2.0}/specs/048-api-response-format/spec.md +0 -0
  92. {mt5api-0.1.0 → mt5api-0.2.0}/specs/049-api-routing-models/spec.md +0 -0
  93. {mt5api-0.1.0 → mt5api-0.2.0}/specs/050-api-runtime-deploy/spec.md +0 -0
  94. {mt5api-0.1.0 → mt5api-0.2.0}/specs/051-api-error-logging/spec.md +0 -0
  95. {mt5api-0.1.0 → mt5api-0.2.0}/specs/052-api-dependencies/spec.md +0 -0
  96. {mt5api-0.1.0 → mt5api-0.2.0}/tests/__init__.py +0 -0
  97. {mt5api-0.1.0 → mt5api-0.2.0}/tests/conftest.py +0 -0
  98. {mt5api-0.1.0 → mt5api-0.2.0}/tests/mt5_constants.py +0 -0
  99. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_account.py +0 -0
  100. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_auth.py +0 -0
  101. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_calc.py +0 -0
  102. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_cli.py +0 -0
  103. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_config.py +0 -0
  104. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_dependencies.py +0 -0
  105. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_formatters.py +0 -0
  106. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_health.py +0 -0
  107. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_history.py +0 -0
  108. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_integration.py +0 -0
  109. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_lifespan.py +0 -0
  110. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_market.py +0 -0
  111. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_middleware.py +0 -0
  112. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_models.py +0 -0
  113. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_symbols.py +0 -0
  114. {mt5api-0.1.0 → mt5api-0.2.0}/tests/test_trading.py +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: local-qa
3
3
  description: Run local QA including formatting, linting, and testing for the repository. Use whenever any file has been updated.
4
- disable-model-invocation: true
4
+ disable-model-invocation: false
5
5
  ---
6
6
 
7
7
  # Local QA (format, lint, and test)
@@ -10,7 +10,7 @@ uv run pyright .
10
10
  uv run pytest
11
11
 
12
12
  # Markdown
13
- npx prettier --write './**/*.md'
13
+ npx -y prettier --write './**/*.md'
14
14
 
15
15
  # GitHub Actions
16
16
  case "${OSTYPE}" in
@@ -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, and trade history. Use when the user wants to interact with any mt5api
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.1.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
- uv run uvicorn mt5api.main:app --host 0.0.0.0 --port 8000
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
- uv run uvicorn mt5api.main:app --host 0.0.0.0 --port 8000
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) *(experimental)*
120
- - `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events *(experimental)*
121
- - `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events *(experimental)*
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 run_in_threadpool, shutdown_mt5_client
32
+ from .dependencies import release_market_book_subscriptions, shutdown_mt5_client
33
33
  from .middleware import add_middleware
34
- from .routers import account, calc, health, history, market, symbols, trading
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 _release_market_book_subscriptions(app)
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")