mt5api 1.0.1__tar.gz → 1.0.3__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-1.0.1 → mt5api-1.0.3}/.agents/skills/local-qa/scripts/qa.sh +5 -0
- mt5api-1.0.3/.github/CODEOWNERS +2 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/workflows/ci.yml +5 -5
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/workflows/claude.yml +2 -2
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/workflows/release.yml +2 -2
- mt5api-1.0.3/AGENTS.md +38 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/PKG-INFO +5 -5
- {mt5api-1.0.1 → mt5api-1.0.3}/README.md +0 -1
- {mt5api-1.0.1 → mt5api-1.0.3}/docs/api/deployment.md +0 -1
- {mt5api-1.0.1 → mt5api-1.0.3}/docs/api/rest-api.md +0 -1
- {mt5api-1.0.1 → mt5api-1.0.3}/docs/index.md +0 -1
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/constants.py +3 -1
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/main.py +10 -20
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/models.py +30 -15
- {mt5api-1.0.1 → mt5api-1.0.3}/pyproject.toml +8 -8
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_auth.py +0 -10
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_health.py +6 -8
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_main.py +14 -90
- {mt5api-1.0.1 → mt5api-1.0.3}/uv.lock +274 -17
- mt5api-1.0.1/.agents/skills/pr-feedback-triage/SKILL.md +0 -211
- mt5api-1.0.1/AGENTS.md +0 -70
- mt5api-1.0.1/tests/test_integration.py +0 -80
- {mt5api-1.0.1 → mt5api-1.0.3}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.agents/skills/mt5api/SKILL.md +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/FUNDING.yml +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/dependabot.yml +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.github/renovate.json +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/.gitignore +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/CLAUDE.md +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/LICENSE +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/docs/api/index.md +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mkdocs.yml +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/__init__.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/__main__.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/auth.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/config.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/dependencies.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/formatters.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/middleware.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/__init__.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/account.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/calc.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/connection.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/health.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/history.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/market.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/symbols.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/mt5api/routers/trading.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/__init__.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/conftest.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/mt5_constants.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/openapi_mt5_constants.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_account.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_calc.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_cli.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_config.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_connection.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_dependencies.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_formatters.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_history.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_lifespan.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_market.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_middleware.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_models.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_symbols.py +0 -0
- {mt5api-1.0.1 → mt5api-1.0.3}/tests/test_trading.py +0 -0
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
set -euox pipefail
|
|
4
4
|
cd "$(git rev-parse --show-toplevel)"
|
|
5
5
|
|
|
6
|
+
COOLDOWN_DAYS=7
|
|
7
|
+
export UV_EXCLUDE_NEWER="${COOLDOWN_DAYS} days"
|
|
8
|
+
export NPM_CONFIG_MIN_RELEASE_AGE="${COOLDOWN_DAYS}"
|
|
9
|
+
export PNPM_CONFIG_MINIMUM_RELEASE_AGE=$((COOLDOWN_DAYS * 24 * 60))
|
|
10
|
+
|
|
6
11
|
# Python
|
|
7
12
|
uv run ruff format .
|
|
8
13
|
uv run ruff check --fix .
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
36
36
|
permissions:
|
|
37
37
|
contents: read
|
|
38
|
-
uses: dceoy/
|
|
38
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-lint-and-scan.yml@main # zizmor: ignore[unpinned-uses]
|
|
39
39
|
with:
|
|
40
40
|
package-path: .
|
|
41
41
|
runs-on: windows-latest
|
|
@@ -46,7 +46,7 @@ jobs:
|
|
|
46
46
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
47
47
|
permissions:
|
|
48
48
|
contents: read
|
|
49
|
-
uses: dceoy/
|
|
49
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-test.yml@main # zizmor: ignore[unpinned-uses]
|
|
50
50
|
with:
|
|
51
51
|
package-path: .
|
|
52
52
|
runs-on: windows-latest
|
|
@@ -56,7 +56,7 @@ jobs:
|
|
|
56
56
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'docs-deploy')
|
|
57
57
|
permissions:
|
|
58
58
|
contents: write
|
|
59
|
-
uses: dceoy/
|
|
59
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-mkdocs-gh-deploy.yml@main # zizmor: ignore[unpinned-uses]
|
|
60
60
|
with:
|
|
61
61
|
package-path: .
|
|
62
62
|
runs-on: ubuntu-slim
|
|
@@ -71,7 +71,7 @@ jobs:
|
|
|
71
71
|
contents: read
|
|
72
72
|
security-events: write
|
|
73
73
|
actions: read
|
|
74
|
-
uses: dceoy/
|
|
74
|
+
uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
|
|
75
75
|
with:
|
|
76
76
|
language: >
|
|
77
77
|
["python"]
|
|
@@ -81,7 +81,7 @@ jobs:
|
|
|
81
81
|
needs:
|
|
82
82
|
- python-lint-and-scan
|
|
83
83
|
- python-test
|
|
84
|
-
uses: dceoy/
|
|
84
|
+
uses: dceoy/gha-for-devops/.github/workflows/dependabot-auto-merge.yml@main # zizmor: ignore[unpinned-uses]
|
|
85
85
|
permissions:
|
|
86
86
|
contents: write
|
|
87
87
|
pull-requests: write
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
issues: write
|
|
36
36
|
id-token: write
|
|
37
37
|
actions: read
|
|
38
|
-
uses: dceoy/
|
|
38
|
+
uses: dceoy/gha-for-devops/.github/workflows/claude-code-review.yml@main # zizmor: ignore[unpinned-uses]
|
|
39
39
|
secrets:
|
|
40
40
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
|
41
41
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -51,7 +51,7 @@ jobs:
|
|
|
51
51
|
issues: write
|
|
52
52
|
id-token: write
|
|
53
53
|
actions: read
|
|
54
|
-
uses: dceoy/
|
|
54
|
+
uses: dceoy/gha-for-devops/.github/workflows/claude-code-bot.yml@main # zizmor: ignore[unpinned-uses]
|
|
55
55
|
secrets:
|
|
56
56
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
|
57
57
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
permissions:
|
|
14
14
|
contents: write
|
|
15
15
|
id-token: write
|
|
16
|
-
uses: dceoy/
|
|
16
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
|
|
17
17
|
with:
|
|
18
18
|
package-path: .
|
|
19
19
|
create-github-release: true
|
|
@@ -39,6 +39,6 @@ jobs:
|
|
|
39
39
|
name: ${{ needs.build-and-release.outputs.distribution-artifact-name }}
|
|
40
40
|
path: dist/
|
|
41
41
|
- name: Publish distribution 📦 to PyPI
|
|
42
|
-
uses: pypa/gh-action-pypi-publish@
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
43
43
|
with:
|
|
44
44
|
verbose: true
|
mt5api-1.0.3/AGENTS.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Repository Guidelines
|
|
2
|
+
|
|
3
|
+
## Build, Test, and Development Commands
|
|
4
|
+
|
|
5
|
+
- `.agents/skills/local-qa/scripts/qa.sh`: run the repository QA workflow after
|
|
6
|
+
any file update. This wraps formatting, linting, type checks, tests, Markdown
|
|
7
|
+
formatting, and workflow/security checks.
|
|
8
|
+
|
|
9
|
+
The API server runtime requires Windows with MetaTrader 5 installed and logged
|
|
10
|
+
in. Non-Windows systems are suitable for editing docs and most mocked tests.
|
|
11
|
+
|
|
12
|
+
## Testing Guidelines
|
|
13
|
+
|
|
14
|
+
Parametrize unit tests with `pytest.mark.parametrize` when the same behavior
|
|
15
|
+
should be verified across multiple input and expected-output cases. Prefer
|
|
16
|
+
mocked MT5/pdmt5 interactions unless a test explicitly requires a Windows MT5
|
|
17
|
+
terminal.
|
|
18
|
+
|
|
19
|
+
## Design Principles
|
|
20
|
+
|
|
21
|
+
Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest direct
|
|
22
|
+
implementation that satisfies the current requirement. Remove duplication when
|
|
23
|
+
it improves clarity or prevents drift, but avoid abstractions that do not serve
|
|
24
|
+
an immediate need. Keep changes small, focused, and easy to review.
|
|
25
|
+
|
|
26
|
+
## Commit & Pull Request Guidelines
|
|
27
|
+
|
|
28
|
+
Keep commits scoped to one logical change. Before committing or opening a PR,
|
|
29
|
+
run `.agents/skills/local-qa/scripts/qa.sh`. Pull requests should describe the
|
|
30
|
+
behavior change, list verification commands, link related issues when
|
|
31
|
+
applicable, and call out any runtime impact for Windows/MT5 users.
|
|
32
|
+
|
|
33
|
+
## Security & Configuration Tips
|
|
34
|
+
|
|
35
|
+
Do not commit credentials, terminal account details, or real API keys. Configure
|
|
36
|
+
runtime values through environment variables such as `MT5API_SECRET_KEY`,
|
|
37
|
+
`MT5API_ROUTER_PREFIX`, `MT5API_HOST`, `MT5API_PORT`, and
|
|
38
|
+
`MT5API_LOG_LEVEL`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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>
|
|
@@ -15,11 +15,12 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
-
Requires-Python: <3.
|
|
18
|
+
Requires-Python: <3.15,>=3.11
|
|
19
19
|
Requires-Dist: fastapi>=0.115.0
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: pdmt5>=1.
|
|
20
|
+
Requires-Dist: pandas>=3.0.3
|
|
21
|
+
Requires-Dist: pdmt5>=1.2.0
|
|
22
22
|
Requires-Dist: pyarrow>=18.0.0
|
|
23
|
+
Requires-Dist: pydantic>=2.13.4
|
|
23
24
|
Requires-Dist: python-multipart>=0.0.31
|
|
24
25
|
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
@@ -85,7 +86,6 @@ graph TB
|
|
|
85
86
|
|
|
86
87
|
## Requirements
|
|
87
88
|
|
|
88
|
-
- Python 3.11+
|
|
89
89
|
- Windows host with MetaTrader 5 terminal installed and logged in
|
|
90
90
|
- Linux and macOS are not supported for the API server runtime, but they work
|
|
91
91
|
for HTTP clients
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Application-wide constants."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
3
5
|
API_TITLE = "MT5 REST API"
|
|
4
6
|
API_DESCRIPTION = (
|
|
5
7
|
"REST API for MetaTrader 5 data access and non-executing terminal utilities. "
|
|
6
8
|
"Provides market data, account information, trading history, calculations, "
|
|
7
9
|
"and safe operational endpoints via HTTP."
|
|
8
10
|
)
|
|
9
|
-
API_VERSION = "
|
|
11
|
+
API_VERSION = version("mt5api")
|
|
10
12
|
API_DOCS_URL = "/docs"
|
|
11
13
|
API_REDOC_URL = "/redoc"
|
|
12
14
|
API_OPENAPI_URL = "/openapi.json"
|
|
@@ -81,17 +81,13 @@ def _strip_auth_from_openapi(openapi_schema: dict[str, Any]) -> None:
|
|
|
81
81
|
"""Remove API key requirements from OpenAPI when auth is disabled."""
|
|
82
82
|
openapi_schema.pop("security", None)
|
|
83
83
|
|
|
84
|
-
components = openapi_schema.get("components")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if not security_schemes:
|
|
90
|
-
components.pop("securitySchemes", None)
|
|
84
|
+
components = openapi_schema.get("components", {})
|
|
85
|
+
security_schemes = components.get("securitySchemes", {})
|
|
86
|
+
security_schemes.pop(API_KEY_SECURITY_SCHEME_NAME, None)
|
|
87
|
+
if not security_schemes:
|
|
88
|
+
components.pop("securitySchemes", None)
|
|
91
89
|
|
|
92
90
|
for methods in openapi_schema.get("paths", {}).values():
|
|
93
|
-
if not isinstance(methods, dict):
|
|
94
|
-
continue
|
|
95
91
|
for operation in methods.values():
|
|
96
92
|
if isinstance(operation, dict):
|
|
97
93
|
operation.pop("security", None)
|
|
@@ -100,16 +96,12 @@ def _strip_auth_from_openapi(openapi_schema: dict[str, Any]) -> None:
|
|
|
100
96
|
def _patch_validation_error_responses(openapi_schema: dict[str, Any]) -> None:
|
|
101
97
|
"""Advertise RFC 7807 Problem Details for request validation failures."""
|
|
102
98
|
components = openapi_schema.setdefault("components", {})
|
|
103
|
-
if not isinstance(components, dict):
|
|
104
|
-
return
|
|
105
|
-
|
|
106
99
|
schemas = components.setdefault("schemas", {})
|
|
107
|
-
|
|
108
|
-
schemas
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
schemas.pop("ValidationError", None)
|
|
100
|
+
schemas["ErrorResponse"] = ErrorResponse.model_json_schema(
|
|
101
|
+
ref_template="#/components/schemas/{model}",
|
|
102
|
+
)
|
|
103
|
+
schemas.pop("HTTPValidationError", None)
|
|
104
|
+
schemas.pop("ValidationError", None)
|
|
113
105
|
|
|
114
106
|
error_response = {
|
|
115
107
|
"description": "Validation Error",
|
|
@@ -121,8 +113,6 @@ def _patch_validation_error_responses(openapi_schema: dict[str, Any]) -> None:
|
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
for methods in openapi_schema.get("paths", {}).values():
|
|
124
|
-
if not isinstance(methods, dict):
|
|
125
|
-
continue
|
|
126
116
|
for operation in methods.values():
|
|
127
117
|
if isinstance(operation, dict) and "422" in operation.get("responses", {}):
|
|
128
118
|
operation["responses"]["422"] = error_response
|
|
@@ -6,7 +6,22 @@ from datetime import datetime # noqa: TC003
|
|
|
6
6
|
from enum import StrEnum
|
|
7
7
|
from typing import Annotated, Any, Self, TypeAlias
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
from pdmt5 import (
|
|
10
|
+
COPY_TICKS_MAP,
|
|
11
|
+
ORDER_TYPE_MAP,
|
|
12
|
+
TIMEFRAME_MAP,
|
|
13
|
+
parse_copy_ticks,
|
|
14
|
+
parse_order_type,
|
|
15
|
+
parse_timeframe,
|
|
16
|
+
)
|
|
17
|
+
from pdmt5.constants import (
|
|
18
|
+
list_copy_ticks_names,
|
|
19
|
+
list_copy_ticks_values,
|
|
20
|
+
list_order_type_names,
|
|
21
|
+
list_order_type_values,
|
|
22
|
+
list_timeframe_names,
|
|
23
|
+
list_timeframe_values,
|
|
24
|
+
)
|
|
10
25
|
from pydantic import (
|
|
11
26
|
BaseModel,
|
|
12
27
|
BeforeValidator,
|
|
@@ -76,17 +91,17 @@ def _build_mt5_constant_json_schema(
|
|
|
76
91
|
|
|
77
92
|
def get_mt5_timeframe_values() -> tuple[int, ...]:
|
|
78
93
|
"""Return all available MT5 timeframe values from pdmt5."""
|
|
79
|
-
return tuple(
|
|
94
|
+
return tuple(list_timeframe_values())
|
|
80
95
|
|
|
81
96
|
|
|
82
97
|
def get_mt5_timeframe_names() -> tuple[str, ...]:
|
|
83
98
|
"""Return all available MT5 timeframe names from pdmt5."""
|
|
84
|
-
return tuple(
|
|
99
|
+
return tuple(list_timeframe_names())
|
|
85
100
|
|
|
86
101
|
|
|
87
102
|
def get_mt5_timeframe_examples() -> list[int]:
|
|
88
103
|
"""Return common MT5 timeframe examples from pdmt5."""
|
|
89
|
-
return [
|
|
104
|
+
return [TIMEFRAME_MAP[name] for name in _TIMEFRAME_EXAMPLE_NAMES]
|
|
90
105
|
|
|
91
106
|
|
|
92
107
|
def get_mt5_timeframe_example_names() -> list[str]:
|
|
@@ -96,17 +111,17 @@ def get_mt5_timeframe_example_names() -> list[str]:
|
|
|
96
111
|
|
|
97
112
|
def get_mt5_copy_ticks_values() -> tuple[int, ...]:
|
|
98
113
|
"""Return all MT5 COPY_TICKS values from pdmt5."""
|
|
99
|
-
return tuple(
|
|
114
|
+
return tuple(list_copy_ticks_values())
|
|
100
115
|
|
|
101
116
|
|
|
102
117
|
def get_mt5_copy_ticks_names() -> tuple[str, ...]:
|
|
103
118
|
"""Return all MT5 COPY_TICKS names from pdmt5."""
|
|
104
|
-
return tuple(
|
|
119
|
+
return tuple(list_copy_ticks_names())
|
|
105
120
|
|
|
106
121
|
|
|
107
122
|
def get_mt5_copy_ticks_examples() -> list[int]:
|
|
108
123
|
"""Return common MT5 COPY_TICKS integer examples."""
|
|
109
|
-
return [
|
|
124
|
+
return [COPY_TICKS_MAP[name] for name in _COPY_TICKS_EXAMPLE_NAMES]
|
|
110
125
|
|
|
111
126
|
|
|
112
127
|
def get_mt5_copy_ticks_example_names() -> list[str]:
|
|
@@ -120,7 +135,7 @@ def _validate_mt5_timeframe(value: object) -> int:
|
|
|
120
135
|
Returns:
|
|
121
136
|
The validated integer timeframe value.
|
|
122
137
|
"""
|
|
123
|
-
return
|
|
138
|
+
return parse_timeframe(value)
|
|
124
139
|
|
|
125
140
|
|
|
126
141
|
def _validate_mt5_copy_ticks(value: object) -> int:
|
|
@@ -129,22 +144,22 @@ def _validate_mt5_copy_ticks(value: object) -> int:
|
|
|
129
144
|
Returns:
|
|
130
145
|
The validated integer COPY_TICKS value.
|
|
131
146
|
"""
|
|
132
|
-
return
|
|
147
|
+
return parse_copy_ticks(value)
|
|
133
148
|
|
|
134
149
|
|
|
135
150
|
def get_mt5_order_type_values() -> tuple[int, ...]:
|
|
136
151
|
"""Return all available MT5 ORDER_TYPE values from pdmt5."""
|
|
137
|
-
return tuple(
|
|
152
|
+
return tuple(list_order_type_values())
|
|
138
153
|
|
|
139
154
|
|
|
140
155
|
def get_mt5_order_type_names() -> tuple[str, ...]:
|
|
141
156
|
"""Return all available MT5 ORDER_TYPE names from pdmt5."""
|
|
142
|
-
return tuple(
|
|
157
|
+
return tuple(list_order_type_names())
|
|
143
158
|
|
|
144
159
|
|
|
145
160
|
def get_mt5_order_type_examples() -> list[int]:
|
|
146
161
|
"""Return common MT5 ORDER_TYPE integer examples."""
|
|
147
|
-
return [
|
|
162
|
+
return [ORDER_TYPE_MAP[name] for name in _ORDER_TYPE_EXAMPLE_NAMES]
|
|
148
163
|
|
|
149
164
|
|
|
150
165
|
def get_mt5_order_type_example_names() -> list[str]:
|
|
@@ -158,7 +173,7 @@ def _validate_mt5_order_type(value: object) -> int:
|
|
|
158
173
|
Returns:
|
|
159
174
|
The validated integer ORDER_TYPE value.
|
|
160
175
|
"""
|
|
161
|
-
return
|
|
176
|
+
return parse_order_type(value)
|
|
162
177
|
|
|
163
178
|
|
|
164
179
|
Mt5Timeframe: TypeAlias = Annotated[
|
|
@@ -404,7 +419,7 @@ class TicksFromRequest(BaseModel):
|
|
|
404
419
|
le=100000,
|
|
405
420
|
)
|
|
406
421
|
flags: Mt5CopyTicks = Field(
|
|
407
|
-
default=
|
|
422
|
+
default=COPY_TICKS_MAP["COPY_TICKS_ALL"],
|
|
408
423
|
description=_COPY_TICKS_DESCRIPTION,
|
|
409
424
|
)
|
|
410
425
|
format: ResponseFormat | None = Field(default=None)
|
|
@@ -417,7 +432,7 @@ class TicksRangeRequest(BaseModel):
|
|
|
417
432
|
date_from: datetime = Field(..., description="Start date (ISO 8601)")
|
|
418
433
|
date_to: datetime = Field(..., description="End date (ISO 8601)")
|
|
419
434
|
flags: Mt5CopyTicks = Field(
|
|
420
|
-
default=
|
|
435
|
+
default=COPY_TICKS_MAP["COPY_TICKS_ALL"],
|
|
421
436
|
description=_COPY_TICKS_DESCRIPTION,
|
|
422
437
|
)
|
|
423
438
|
format: ResponseFormat | None = Field(default=None)
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5api"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.3"
|
|
4
4
|
description = "MetaTrader 5 REST API"
|
|
5
5
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
6
6
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
7
7
|
license = "MIT"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">= 3.11, < 3.
|
|
10
|
+
requires-python = ">= 3.11, < 3.15"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pdmt5 >= 1.
|
|
12
|
+
"pdmt5 >= 1.2.0",
|
|
13
13
|
"fastapi >= 0.115.0",
|
|
14
14
|
"uvicorn[standard] >= 0.32.0",
|
|
15
|
+
"pandas >= 3.0.3",
|
|
16
|
+
"pydantic >= 2.13.4",
|
|
15
17
|
"pyarrow >= 18.0.0",
|
|
16
18
|
"python-multipart >= 0.0.31",
|
|
17
|
-
"httpx >= 0.27.0",
|
|
18
19
|
]
|
|
19
20
|
classifiers = [
|
|
20
21
|
"Development Status :: 4 - Beta",
|
|
@@ -32,9 +33,12 @@ Repository = "https://github.com/dceoy/mt5api.git"
|
|
|
32
33
|
|
|
33
34
|
[tool.uv]
|
|
34
35
|
required-environments = ["platform_system == 'Windows'"]
|
|
36
|
+
exclude-newer = "1 week"
|
|
37
|
+
exclude-newer-package = { pdmt5 = false }
|
|
35
38
|
|
|
36
39
|
[dependency-groups]
|
|
37
40
|
dev = [
|
|
41
|
+
"httpx >= 0.27.0",
|
|
38
42
|
"ruff >= 0.11.0",
|
|
39
43
|
"pyright >= 1.1.407",
|
|
40
44
|
"pytest >= 8.0.0",
|
|
@@ -50,10 +54,6 @@ dev = [
|
|
|
50
54
|
"mkdocstrings[python] >= 0.29.1",
|
|
51
55
|
]
|
|
52
56
|
|
|
53
|
-
[tool.uv.build-backend]
|
|
54
|
-
source-include = ["mt5api/**", "LICENSE"]
|
|
55
|
-
source-exclude = ["tests/**"]
|
|
56
|
-
|
|
57
57
|
[tool.hatch.build.targets.wheel]
|
|
58
58
|
packages = ["mt5api"]
|
|
59
59
|
|
|
@@ -41,16 +41,6 @@ def test_version_endpoint_rejects_invalid_api_key(client: TestClient) -> None:
|
|
|
41
41
|
assert "Invalid API key" in data["detail"]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def test_version_endpoint_accepts_valid_api_key(
|
|
45
|
-
client: TestClient,
|
|
46
|
-
api_headers: dict[str, str],
|
|
47
|
-
) -> None:
|
|
48
|
-
"""Test version endpoint accepts valid API key."""
|
|
49
|
-
response = client.get("/version", headers=api_headers)
|
|
50
|
-
|
|
51
|
-
assert response.status_code == 200
|
|
52
|
-
|
|
53
|
-
|
|
54
44
|
def test_symbols_endpoint_requires_authentication(
|
|
55
45
|
client: TestClient,
|
|
56
46
|
mock_mt5_client: Mock,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from importlib.metadata import version
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -14,6 +15,11 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from fastapi.testclient import TestClient
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def test_api_version_uses_package_metadata() -> None:
|
|
19
|
+
"""Test API version has one authoritative package-metadata source."""
|
|
20
|
+
assert version("mt5api") == API_VERSION
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
def test_health_endpoint_returns_healthy_status(client: TestClient) -> None:
|
|
18
24
|
"""Test health endpoint returns healthy status when MT5 is connected."""
|
|
19
25
|
response = client.get("/health")
|
|
@@ -27,14 +33,6 @@ def test_health_endpoint_returns_healthy_status(client: TestClient) -> None:
|
|
|
27
33
|
assert data["api_version"] == API_VERSION
|
|
28
34
|
|
|
29
35
|
|
|
30
|
-
def test_health_endpoint_no_authentication_required(client: TestClient) -> None:
|
|
31
|
-
"""Test health endpoint works without authentication."""
|
|
32
|
-
# No X-API-Key header
|
|
33
|
-
response = client.get("/health")
|
|
34
|
-
|
|
35
|
-
assert response.status_code == 200
|
|
36
|
-
|
|
37
|
-
|
|
38
36
|
def test_version_endpoint_returns_mt5_version(
|
|
39
37
|
client: TestClient,
|
|
40
38
|
api_headers: dict[str, str],
|
|
@@ -14,42 +14,6 @@ if TYPE_CHECKING:
|
|
|
14
14
|
from pytest_mock import MockerFixture
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def test_strip_auth_from_openapi_handles_non_dict_components() -> None:
|
|
18
|
-
"""OpenAPI auth stripping should tolerate missing component mappings."""
|
|
19
|
-
from mt5api import main # noqa: PLC0415
|
|
20
|
-
|
|
21
|
-
openapi_schema: dict[str, Any] = {
|
|
22
|
-
"security": [{API_KEY_SECURITY_SCHEME_NAME: []}],
|
|
23
|
-
"components": None,
|
|
24
|
-
"paths": {
|
|
25
|
-
"/bad": [],
|
|
26
|
-
"/version": {
|
|
27
|
-
"summary": "Version endpoint",
|
|
28
|
-
"get": {"security": [{API_KEY_SECURITY_SCHEME_NAME: []}]},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
main._strip_auth_from_openapi(openapi_schema) # pyright: ignore[reportPrivateUsage]
|
|
34
|
-
|
|
35
|
-
assert "security" not in openapi_schema
|
|
36
|
-
assert "security" not in openapi_schema["paths"]["/version"]["get"]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_strip_auth_from_openapi_handles_non_dict_security_schemes() -> None:
|
|
40
|
-
"""OpenAPI auth stripping should tolerate non-dict security schemes."""
|
|
41
|
-
from mt5api import main # noqa: PLC0415
|
|
42
|
-
|
|
43
|
-
openapi_schema: dict[str, Any] = {
|
|
44
|
-
"components": {"securitySchemes": []},
|
|
45
|
-
"paths": {},
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main._strip_auth_from_openapi(openapi_schema) # pyright: ignore[reportPrivateUsage]
|
|
49
|
-
|
|
50
|
-
assert openapi_schema["components"]["securitySchemes"] == []
|
|
51
|
-
|
|
52
|
-
|
|
53
17
|
def test_strip_auth_from_openapi_preserves_other_security_schemes() -> None:
|
|
54
18
|
"""OpenAPI auth stripping should keep unrelated security schemes."""
|
|
55
19
|
from mt5api import main # noqa: PLC0415
|
|
@@ -58,12 +22,21 @@ def test_strip_auth_from_openapi_preserves_other_security_schemes() -> None:
|
|
|
58
22
|
"components": {
|
|
59
23
|
"securitySchemes": {API_KEY_SECURITY_SCHEME_NAME: {}, "Other": {}}
|
|
60
24
|
},
|
|
61
|
-
"paths": {
|
|
25
|
+
"paths": {
|
|
26
|
+
"/rates/from": {
|
|
27
|
+
"parameters": [{"name": "symbol", "in": "query"}],
|
|
28
|
+
"get": {"security": [{API_KEY_SECURITY_SCHEME_NAME: []}]},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
62
31
|
}
|
|
63
32
|
|
|
64
33
|
main._strip_auth_from_openapi(openapi_schema) # pyright: ignore[reportPrivateUsage]
|
|
65
34
|
|
|
66
35
|
assert openapi_schema["components"]["securitySchemes"] == {"Other": {}}
|
|
36
|
+
assert "security" not in openapi_schema["paths"]["/rates/from"]["get"]
|
|
37
|
+
assert openapi_schema["paths"]["/rates/from"]["parameters"] == [
|
|
38
|
+
{"name": "symbol", "in": "query"}
|
|
39
|
+
]
|
|
67
40
|
|
|
68
41
|
|
|
69
42
|
def test_patch_validation_error_responses_updates_422_schema() -> None:
|
|
@@ -78,8 +51,8 @@ def test_patch_validation_error_responses_updates_422_schema() -> None:
|
|
|
78
51
|
},
|
|
79
52
|
},
|
|
80
53
|
"paths": {
|
|
81
|
-
"/bad": [],
|
|
82
54
|
"/rates/from": {
|
|
55
|
+
"parameters": [{"name": "symbol", "in": "query"}],
|
|
83
56
|
"get": {
|
|
84
57
|
"responses": {
|
|
85
58
|
"422": {
|
|
@@ -106,58 +79,9 @@ def test_patch_validation_error_responses_updates_422_schema() -> None:
|
|
|
106
79
|
assert "ErrorResponse" in schemas
|
|
107
80
|
assert "HTTPValidationError" not in schemas
|
|
108
81
|
assert "ValidationError" not in schemas
|
|
109
|
-
assert openapi_schema["paths"]["/rates/from"]["
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
"application/json": {
|
|
113
|
-
"schema": {"$ref": "#/components/schemas/ErrorResponse"},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_patch_validation_error_responses_handles_non_dict_components() -> None:
|
|
120
|
-
"""OpenAPI validation patching should tolerate missing component mappings."""
|
|
121
|
-
from mt5api import main # noqa: PLC0415
|
|
122
|
-
|
|
123
|
-
openapi_schema: dict[str, Any] = {"components": None, "paths": {}}
|
|
124
|
-
|
|
125
|
-
main._patch_validation_error_responses(openapi_schema) # pyright: ignore[reportPrivateUsage]
|
|
126
|
-
|
|
127
|
-
assert openapi_schema["components"] is None
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_patch_validation_error_responses_handles_non_dict_schemas() -> None:
|
|
131
|
-
"""OpenAPI validation patching should still update 422 responses."""
|
|
132
|
-
from mt5api import main # noqa: PLC0415
|
|
133
|
-
|
|
134
|
-
openapi_schema: dict[str, Any] = {
|
|
135
|
-
"components": {"schemas": []},
|
|
136
|
-
"paths": {
|
|
137
|
-
"/rates/from": {
|
|
138
|
-
"get": {
|
|
139
|
-
"responses": {
|
|
140
|
-
"422": {
|
|
141
|
-
"description": "Validation Error",
|
|
142
|
-
"content": {
|
|
143
|
-
"application/json": {
|
|
144
|
-
"schema": {
|
|
145
|
-
"$ref": (
|
|
146
|
-
"#/components/schemas/HTTPValidationError"
|
|
147
|
-
),
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
main._patch_validation_error_responses(openapi_schema) # pyright: ignore[reportPrivateUsage]
|
|
159
|
-
|
|
160
|
-
assert openapi_schema["components"]["schemas"] == []
|
|
82
|
+
assert openapi_schema["paths"]["/rates/from"]["parameters"] == [
|
|
83
|
+
{"name": "symbol", "in": "query"}
|
|
84
|
+
]
|
|
161
85
|
assert openapi_schema["paths"]["/rates/from"]["get"]["responses"]["422"] == {
|
|
162
86
|
"description": "Validation Error",
|
|
163
87
|
"content": {
|