mt5api 1.0.0__tar.gz → 1.0.1__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.0 → mt5api-1.0.1}/.agents/skills/pr-feedback-triage/SKILL.md +25 -15
- {mt5api-1.0.0 → mt5api-1.0.1}/PKG-INFO +16 -9
- {mt5api-1.0.0 → mt5api-1.0.1}/README.md +14 -7
- {mt5api-1.0.0 → mt5api-1.0.1}/docs/api/rest-api.md +2 -2
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/__main__.py +3 -19
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/config.py +49 -6
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/constants.py +1 -1
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/dependencies.py +7 -14
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/main.py +35 -2
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/middleware.py +82 -2
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/connection.py +3 -3
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/health.py +4 -4
- {mt5api-1.0.0 → mt5api-1.0.1}/pyproject.toml +2 -2
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/conftest.py +2 -2
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_auth.py +5 -5
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_cli.py +20 -23
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_config.py +78 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_connection.py +60 -33
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_health.py +63 -3
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_main.py +102 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_market.py +9 -3
- mt5api-1.0.1/tests/test_middleware.py +221 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_trading.py +3 -1
- {mt5api-1.0.0 → mt5api-1.0.1}/uv.lock +6 -6
- mt5api-1.0.0/tests/test_middleware.py +0 -147
- {mt5api-1.0.0 → mt5api-1.0.1}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.agents/skills/mt5api/SKILL.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/FUNDING.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/dependabot.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/renovate.json +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/workflows/ci.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/workflows/claude.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.github/workflows/release.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/.gitignore +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/AGENTS.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/CLAUDE.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/LICENSE +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/docs/api/deployment.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/docs/api/index.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/docs/index.md +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mkdocs.yml +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/__init__.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/auth.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/formatters.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/models.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/__init__.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/account.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/calc.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/history.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/market.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/symbols.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/mt5api/routers/trading.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/__init__.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/mt5_constants.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/openapi_mt5_constants.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_account.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_calc.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_dependencies.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_formatters.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_history.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_integration.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_lifespan.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_models.py +0 -0
- {mt5api-1.0.0 → mt5api-1.0.1}/tests/test_symbols.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pr-feedback-triage
|
|
3
3
|
description: Triage pull request review comments into fixes, replies, clarification requests, or open follow-ups while respecting safe execution modes.
|
|
4
|
+
allowed-tools: Bash(git:*), Bash(gh:*), mcp__github__*, Read, Grep, Glob, Edit, MultiEdit, Write
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# PR Feedback Triage
|
|
@@ -43,9 +44,10 @@ When a mode disables an action, skip that destructive or externally visible acti
|
|
|
43
44
|
|
|
44
45
|
Gather the complete feedback set before editing:
|
|
45
46
|
|
|
46
|
-
- Fetch unresolved review threads, requested-change reviews,
|
|
47
|
-
- Use
|
|
48
|
-
- For bot reviewers that post both summary comments and inline comments,
|
|
47
|
+
- Fetch unresolved review threads, requested-change reviews, inline comments, copied comments, and PR-level summary comments.
|
|
48
|
+
- Use whichever authenticated GitHub-capable interface is available and reliable. This skill explicitly permits both `gh` and GitHub MCP tools; paginate results and do not inspect only the first page of threads or comments.
|
|
49
|
+
- For bot reviewers that post both summary comments and inline comments, prefer inline comments for actionable triage. Incorporate summary findings only when they contain distinct severity, rationale, or fix instructions not already captured from inline comments.
|
|
50
|
+
- Summary comments may be excluded from triage when they do not add distinct actionable context.
|
|
49
51
|
- Preserve every thread/comment identifier needed to reply or resolve later.
|
|
50
52
|
- Compare each comment with the current diff and file contents because review lines can become outdated.
|
|
51
53
|
|
|
@@ -56,7 +58,7 @@ Build one triage record per distinct finding:
|
|
|
56
58
|
- Prefer exact review-thread identity when available.
|
|
57
59
|
- For duplicate bot findings appearing in both summary and inline comments, merge by exact issue title first, then by file path plus line range as a fallback.
|
|
58
60
|
- Prefer inline comments for location and current code context.
|
|
59
|
-
-
|
|
61
|
+
- Use summary comments only for distinct severity, category, rationale, or detailed agent prompts that are not already available from inline comments.
|
|
60
62
|
- Preserve the reviewer’s exact issue title and original wording where practical. Do not rename findings in a way that would make replies hard to map back to comments.
|
|
61
63
|
- Preserve the reviewer’s original ordering unless the user asks for priority reordering. Many review bots already order findings by severity.
|
|
62
64
|
|
|
@@ -70,13 +72,20 @@ Keep a thread open only when it still needs reviewer, maintainer, or product inp
|
|
|
70
72
|
|
|
71
73
|
When resolving a thread, add a concise reply first only if it provides useful context, such as what changed, why no code change was needed, why a finding was intentionally deferred, or why the original comment is now outdated. Do not add noisy replies for self-evident fixes unless project norms require them.
|
|
72
74
|
|
|
75
|
+
## Platform Comment Style
|
|
76
|
+
|
|
77
|
+
- Keep every posted reply or comment brief: one sentence by default, two short sentences only when necessary.
|
|
78
|
+
- Do not post PR-level summary or status comments by default. Omit them when they only restate completed fixes, resolved threads, or verification already visible in commits/checks.
|
|
79
|
+
- Avoid templates, long bullet lists, exhaustive status logs, and duplicated explanations in platform comments.
|
|
80
|
+
- For simple fixes, already-addressed findings, or outdated findings, prefer `resolve_only` over adding a reply.
|
|
81
|
+
|
|
73
82
|
## Platform Action Contract
|
|
74
83
|
|
|
75
|
-
Do not treat triage as complete until every
|
|
84
|
+
Do not treat triage as complete until every incorporated source ID reaches an explicit terminal state:
|
|
76
85
|
|
|
77
86
|
- `resolved`: a platform resolve action succeeded, or a re-check shows the thread is already resolved.
|
|
78
87
|
- `replied_left_open`: a reply or question was posted and the thread is intentionally left unresolved.
|
|
79
|
-
- `not_resolvable`: the source is a PR-level summary comment or copied comment that has no platform-level resolve action;
|
|
88
|
+
- `not_resolvable`: the source is a PR-level summary comment or copied comment that has no platform-level resolve action; post a brief reply only when useful.
|
|
80
89
|
- `skipped_by_mode`: `dry_run`, `no_push`, or `no_reply` prevented the external action.
|
|
81
90
|
- `failed_action`: a reply or resolve action was attempted and failed; include the attempted action and failure in the final summary.
|
|
82
91
|
|
|
@@ -85,13 +94,13 @@ In normal mode, build and execute a platform action queue after fixes are verifi
|
|
|
85
94
|
- `reply_then_resolve`: use for handled threads where the reviewer needs context before resolution.
|
|
86
95
|
- `resolve_only`: use for self-evident fixes and already-addressed or outdated threads where an extra reply would add noise.
|
|
87
96
|
- `reply_leave_open`: use only for clarification requests, blocked work, or intentionally open follow-ups.
|
|
88
|
-
- `reply_only`: use for PR-level comments or summaries that cannot be resolved
|
|
97
|
+
- `reply_only`: use for PR-level comments or summaries that cannot be resolved, only when a short reply adds value.
|
|
89
98
|
|
|
90
99
|
For duplicate findings, execute the terminal action for every source thread ID, not only the primary triage record. If one finding is represented by three unresolved inline threads, all three must be resolved or explicitly left open.
|
|
91
100
|
|
|
92
101
|
## GitHub Action Guidance
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
Use whichever authenticated GitHub-capable interface is available and reliable, preferably `gh` or GitHub MCP. Prefer interfaces that expose review-thread resolution state. For GitHub inline review threads, use the thread node ID and the GraphQL `resolveReviewThread` mutation, or an equivalent GitHub MCP resolve-thread tool, rather than assuming that a reply resolves the conversation.
|
|
95
104
|
|
|
96
105
|
A reliable pattern is:
|
|
97
106
|
|
|
@@ -150,9 +159,9 @@ flowchart TD
|
|
|
150
159
|
## Compact Workflow
|
|
151
160
|
|
|
152
161
|
1. **Collect all relevant feedback**
|
|
153
|
-
- Identify the PR and gather unresolved review threads, requested-change reviews,
|
|
162
|
+
- Identify the PR and gather unresolved review threads, requested-change reviews, inline comments, copied comments, and PR-level summaries.
|
|
154
163
|
- Paginate all platform calls and keep comment/thread IDs for later replies and resolution.
|
|
155
|
-
- For bot reviews,
|
|
164
|
+
- For bot reviews, prioritize inline comments and incorporate summary findings only when they add distinct actionable context.
|
|
156
165
|
|
|
157
166
|
2. **Classify each triage record**
|
|
158
167
|
- **Fix**: Valid requested change; make the smallest focused edit when not in `dry_run`.
|
|
@@ -168,7 +177,7 @@ flowchart TD
|
|
|
168
177
|
- In `dry_run`, stop at triage, proposed fixes, suggested replies, and verification plan.
|
|
169
178
|
- In `no_push`, local edits are allowed, but do not push or resolve threads whose fix is only local. Reply or resolve non-code, already-addressed, or outdated threads only when the action does not depend on unpushed work and `no_reply` is not set.
|
|
170
179
|
- In `no_reply`, do not post replies or resolve threads; report suggested replies/actions instead.
|
|
171
|
-
- In normal mode, commit and push changed code when appropriate, then execute the platform action queue for every
|
|
180
|
+
- In normal mode, commit and push changed code when appropriate, then execute the platform action queue for every incorporated source ID.
|
|
172
181
|
|
|
173
182
|
4. **Verify before claiming completion**
|
|
174
183
|
- For fixes, run appropriate checks or explain why they could not run.
|
|
@@ -178,15 +187,16 @@ flowchart TD
|
|
|
178
187
|
- If a resolve or reply operation fails, retry once when safe; then report `failed_action` with the affected source ID and reason.
|
|
179
188
|
|
|
180
189
|
5. **Finish**
|
|
181
|
-
- Normal mode: commit/push changes when appropriate, post useful replies
|
|
190
|
+
- Normal mode: commit/push changes when appropriate, post only useful short replies/comments, omit PR-level summaries when they add no value, resolve all handled threads by default, and reconcile the final unresolved set.
|
|
182
191
|
- Safe modes: report the local state and the exact replies/resolution actions a human could take.
|
|
183
192
|
|
|
184
193
|
## Reply Guidance
|
|
185
194
|
|
|
186
|
-
- Keep inline replies short
|
|
187
|
-
- For fixed findings, mention the concrete change or commit if
|
|
188
|
-
- For already-addressed or outdated findings, cite the current code path or behavior
|
|
195
|
+
- Keep inline replies short: one sentence by default, two short sentences only when needed.
|
|
196
|
+
- For fixed findings, mention the concrete change or commit only if it helps the reviewer.
|
|
197
|
+
- For already-addressed or outdated findings, cite the current code path or behavior only as briefly as needed.
|
|
189
198
|
- For deferred or won't-fix findings, provide the reason and any follow-up issue or owner if known.
|
|
199
|
+
- Avoid posting PR-level summary comments unless they communicate a decision, blocker, or requested reviewer action.
|
|
190
200
|
- If a reply or resolve operation fails, continue with the remaining threads and report the failure in the final summary.
|
|
191
201
|
|
|
192
202
|
## Final Summary Checklist
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
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>
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
|
18
18
|
Requires-Python: <3.14,>=3.11
|
|
19
19
|
Requires-Dist: fastapi>=0.115.0
|
|
20
20
|
Requires-Dist: httpx>=0.27.0
|
|
21
|
-
Requires-Dist: pdmt5>=1.
|
|
21
|
+
Requires-Dist: pdmt5>=1.1.1
|
|
22
22
|
Requires-Dist: pyarrow>=18.0.0
|
|
23
23
|
Requires-Dist: python-multipart>=0.0.31
|
|
24
24
|
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
@@ -30,11 +30,18 @@ MetaTrader 5 REST API
|
|
|
30
30
|
|
|
31
31
|
[](https://github.com/dceoy/mt5api/actions/workflows/ci.yml)
|
|
32
32
|
|
|
33
|
-
mt5api exposes MT5 market data, account info, trading history,
|
|
34
|
-
|
|
35
|
-
[`pdmt5`](https://github.com/dceoy/pdmt5), which
|
|
36
|
-
dataframe/
|
|
37
|
-
optional API-key auth and JSON/Parquet response
|
|
33
|
+
mt5api exposes MT5 market data, account info, trading history, trading state,
|
|
34
|
+
order validation, and operational terminal endpoints over HTTP. It is a
|
|
35
|
+
FastAPI/HTTP adapter over [`pdmt5`](https://github.com/dceoy/pdmt5), which
|
|
36
|
+
provides the core MT5 client, dataframe/order primitives, and canonical MT5
|
|
37
|
+
constant parsing. mt5api adds optional API-key auth and JSON/Parquet response
|
|
38
|
+
formatting.
|
|
39
|
+
|
|
40
|
+
The exposed operational surface is deliberately narrow: mt5api validates trade
|
|
41
|
+
requests, manages terminal-side subscriptions and MarketWatch visibility, and
|
|
42
|
+
supports reconnecting the terminal to a different MT5 account via
|
|
43
|
+
`POST /connection/login`, but it does not expose live order-send endpoints or
|
|
44
|
+
implement strategy orchestration.
|
|
38
45
|
|
|
39
46
|
The API server must run on Windows. pdmt5 connects through the MetaTrader 5
|
|
40
47
|
Python API, which is supported only on Windows, so you must host `mt5api` on a
|
|
@@ -69,8 +76,8 @@ graph TB
|
|
|
69
76
|
|
|
70
77
|
## Features
|
|
71
78
|
|
|
72
|
-
- REST endpoints for symbols, market data, account info, orders,
|
|
73
|
-
calculations, and
|
|
79
|
+
- REST endpoints for symbols, market data, account info, orders, positions,
|
|
80
|
+
history, margin/profit calculations, order validation, and terminal operations
|
|
74
81
|
- JSON and Apache Parquet responses (content negotiation)
|
|
75
82
|
- Optional API key authentication
|
|
76
83
|
- Structured JSON logging
|
|
@@ -4,11 +4,18 @@ MetaTrader 5 REST API
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/dceoy/mt5api/actions/workflows/ci.yml)
|
|
6
6
|
|
|
7
|
-
mt5api exposes MT5 market data, account info, trading history,
|
|
8
|
-
|
|
9
|
-
[`pdmt5`](https://github.com/dceoy/pdmt5), which
|
|
10
|
-
dataframe/
|
|
11
|
-
optional API-key auth and JSON/Parquet response
|
|
7
|
+
mt5api exposes MT5 market data, account info, trading history, trading state,
|
|
8
|
+
order validation, and operational terminal endpoints over HTTP. It is a
|
|
9
|
+
FastAPI/HTTP adapter over [`pdmt5`](https://github.com/dceoy/pdmt5), which
|
|
10
|
+
provides the core MT5 client, dataframe/order primitives, and canonical MT5
|
|
11
|
+
constant parsing. mt5api adds optional API-key auth and JSON/Parquet response
|
|
12
|
+
formatting.
|
|
13
|
+
|
|
14
|
+
The exposed operational surface is deliberately narrow: mt5api validates trade
|
|
15
|
+
requests, manages terminal-side subscriptions and MarketWatch visibility, and
|
|
16
|
+
supports reconnecting the terminal to a different MT5 account via
|
|
17
|
+
`POST /connection/login`, but it does not expose live order-send endpoints or
|
|
18
|
+
implement strategy orchestration.
|
|
12
19
|
|
|
13
20
|
The API server must run on Windows. pdmt5 connects through the MetaTrader 5
|
|
14
21
|
Python API, which is supported only on Windows, so you must host `mt5api` on a
|
|
@@ -43,8 +50,8 @@ graph TB
|
|
|
43
50
|
|
|
44
51
|
## Features
|
|
45
52
|
|
|
46
|
-
- REST endpoints for symbols, market data, account info, orders,
|
|
47
|
-
calculations, and
|
|
53
|
+
- REST endpoints for symbols, market data, account info, orders, positions,
|
|
54
|
+
history, margin/profit calculations, order validation, and terminal operations
|
|
48
55
|
- JSON and Apache Parquet responses (content negotiation)
|
|
49
56
|
- Optional API key authentication
|
|
50
57
|
- Structured JSON logging
|
|
@@ -316,8 +316,8 @@ Errors follow RFC 7807 Problem Details:
|
|
|
316
316
|
{
|
|
317
317
|
"type": "/errors/validation-error",
|
|
318
318
|
"title": "Request Validation Failed",
|
|
319
|
-
"status":
|
|
320
|
-
"detail": "
|
|
319
|
+
"status": 422,
|
|
320
|
+
"detail": "Request validation error details",
|
|
321
321
|
"instance": "/rates/from"
|
|
322
322
|
}
|
|
323
323
|
```
|
|
@@ -11,11 +11,7 @@ from .config import (
|
|
|
11
11
|
get_configured_api_log_level,
|
|
12
12
|
get_configured_api_port,
|
|
13
13
|
)
|
|
14
|
-
from .constants import
|
|
15
|
-
API_APP_IMPORT,
|
|
16
|
-
DEFAULT_API_PORT,
|
|
17
|
-
MAX_API_PORT,
|
|
18
|
-
)
|
|
14
|
+
from .constants import API_APP_IMPORT
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
def _get_host() -> str:
|
|
@@ -33,19 +29,7 @@ def _get_port() -> int:
|
|
|
33
29
|
Returns:
|
|
34
30
|
Port number for the API server.
|
|
35
31
|
"""
|
|
36
|
-
|
|
37
|
-
if raw_port is None:
|
|
38
|
-
return DEFAULT_API_PORT
|
|
39
|
-
|
|
40
|
-
try:
|
|
41
|
-
port_value = int(raw_port)
|
|
42
|
-
except ValueError:
|
|
43
|
-
return DEFAULT_API_PORT
|
|
44
|
-
|
|
45
|
-
if not 1 <= port_value <= MAX_API_PORT:
|
|
46
|
-
return DEFAULT_API_PORT
|
|
47
|
-
|
|
48
|
-
return port_value
|
|
32
|
+
return get_configured_api_port()
|
|
49
33
|
|
|
50
34
|
|
|
51
35
|
def _get_log_level() -> str:
|
|
@@ -54,7 +38,7 @@ def _get_log_level() -> str:
|
|
|
54
38
|
Returns:
|
|
55
39
|
Log level string for uvicorn.
|
|
56
40
|
"""
|
|
57
|
-
return get_configured_api_log_level()
|
|
41
|
+
return get_configured_api_log_level()
|
|
58
42
|
|
|
59
43
|
|
|
60
44
|
def main() -> None:
|
|
@@ -8,6 +8,7 @@ import re
|
|
|
8
8
|
from .constants import (
|
|
9
9
|
DEFAULT_API_HOST,
|
|
10
10
|
DEFAULT_API_LOG_LEVEL,
|
|
11
|
+
DEFAULT_API_PORT,
|
|
11
12
|
DEFAULT_API_ROUTER_PREFIX,
|
|
12
13
|
DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS,
|
|
13
14
|
ENV_MT5API_HOST,
|
|
@@ -16,9 +17,21 @@ from .constants import (
|
|
|
16
17
|
ENV_MT5API_PORT,
|
|
17
18
|
ENV_MT5API_ROUTER_PREFIX,
|
|
18
19
|
ENV_MT5API_SECRET_KEY,
|
|
20
|
+
MAX_API_PORT,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
_VALID_API_ROUTER_PREFIX_PATTERN = re.compile(r"^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$")
|
|
24
|
+
_VALID_UVICORN_LOG_LEVELS = {"critical", "error", "warning", "info", "debug", "trace"}
|
|
25
|
+
_LOGGING_LOG_LEVEL_BY_API_LEVEL = {
|
|
26
|
+
"critical": "CRITICAL",
|
|
27
|
+
"error": "ERROR",
|
|
28
|
+
"warning": "WARNING",
|
|
29
|
+
"info": "INFO",
|
|
30
|
+
"debug": "DEBUG",
|
|
31
|
+
"trace": "DEBUG",
|
|
32
|
+
}
|
|
33
|
+
_INVALID_MT5API_PORT_ERROR = "Invalid MT5API_PORT"
|
|
34
|
+
_INVALID_MT5API_LOG_LEVEL_ERROR = "Invalid MT5API_LOG_LEVEL"
|
|
22
35
|
_INVALID_MT5API_ROUTER_PREFIX_ERROR = "Invalid MT5API_ROUTER_PREFIX"
|
|
23
36
|
_INVALID_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS_ERROR = (
|
|
24
37
|
"Invalid MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS"
|
|
@@ -59,22 +72,52 @@ def get_configured_api_host() -> str:
|
|
|
59
72
|
return os.getenv(ENV_MT5API_HOST, DEFAULT_API_HOST)
|
|
60
73
|
|
|
61
74
|
|
|
62
|
-
def get_configured_api_port() ->
|
|
63
|
-
"""Get the configured API port
|
|
75
|
+
def get_configured_api_port() -> int:
|
|
76
|
+
"""Get the configured API port.
|
|
64
77
|
|
|
65
78
|
Returns:
|
|
66
|
-
|
|
79
|
+
Port number for the API server.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the configured port is not an integer in the TCP port range.
|
|
67
83
|
"""
|
|
68
|
-
|
|
84
|
+
raw_port = os.getenv(ENV_MT5API_PORT)
|
|
85
|
+
if raw_port is None:
|
|
86
|
+
return DEFAULT_API_PORT
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
port = int(raw_port)
|
|
90
|
+
except ValueError as error:
|
|
91
|
+
raise ValueError(_INVALID_MT5API_PORT_ERROR) from error
|
|
92
|
+
|
|
93
|
+
if not 1 <= port <= MAX_API_PORT:
|
|
94
|
+
raise ValueError(_INVALID_MT5API_PORT_ERROR)
|
|
95
|
+
|
|
96
|
+
return port
|
|
69
97
|
|
|
70
98
|
|
|
71
99
|
def get_configured_api_log_level() -> str:
|
|
72
100
|
"""Get the configured API log level.
|
|
73
101
|
|
|
74
102
|
Returns:
|
|
75
|
-
|
|
103
|
+
Valid uvicorn log level string from configuration.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If the configured log level is unsupported.
|
|
107
|
+
"""
|
|
108
|
+
log_level = os.getenv(ENV_MT5API_LOG_LEVEL, DEFAULT_API_LOG_LEVEL).strip().lower()
|
|
109
|
+
if log_level not in _VALID_UVICORN_LOG_LEVELS:
|
|
110
|
+
raise ValueError(_INVALID_MT5API_LOG_LEVEL_ERROR)
|
|
111
|
+
return log_level
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_configured_python_log_level() -> str:
|
|
115
|
+
"""Get a stdlib logging-compatible log level for the API logger.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Python logging level name derived from the configured API log level.
|
|
76
119
|
"""
|
|
77
|
-
return
|
|
120
|
+
return _LOGGING_LOG_LEVEL_BY_API_LEVEL[get_configured_api_log_level()]
|
|
78
121
|
|
|
79
122
|
|
|
80
123
|
def get_configured_api_router_prefix() -> str:
|
|
@@ -6,7 +6,7 @@ API_DESCRIPTION = (
|
|
|
6
6
|
"Provides market data, account information, trading history, calculations, "
|
|
7
7
|
"and safe operational endpoints via HTTP."
|
|
8
8
|
)
|
|
9
|
-
API_VERSION = "1.0.
|
|
9
|
+
API_VERSION = "1.0.1"
|
|
10
10
|
API_DOCS_URL = "/docs"
|
|
11
11
|
API_REDOC_URL = "/redoc"
|
|
12
12
|
API_OPENAPI_URL = "/openapi.json"
|
|
@@ -80,12 +80,12 @@ def shutdown_mt5_client() -> None:
|
|
|
80
80
|
async def replace_mt5_client(config: Mt5Config) -> Mt5DataClient:
|
|
81
81
|
"""Swap the MT5 client singleton with a new connection.
|
|
82
82
|
|
|
83
|
-
Constructs and initializes the new ``Mt5DataClient``
|
|
84
|
-
singleton
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
Constructs and initializes the new ``Mt5DataClient`` before replacing the
|
|
84
|
+
singleton reference. The MetaTrader5 Python module has process-global
|
|
85
|
+
connection state, so the previous client object must not be shut down after
|
|
86
|
+
successful reconnect because that would close the connection just opened by
|
|
87
|
+
the new client. Callers MUST hold ``get_mt5_client_lock`` so concurrent
|
|
88
|
+
reconnect requests do not race.
|
|
89
89
|
|
|
90
90
|
The raised ``RuntimeError`` deliberately omits the underlying exception
|
|
91
91
|
text from its message because third-party ``pdmt5``/MetaTrader5
|
|
@@ -101,7 +101,7 @@ async def replace_mt5_client(config: Mt5Config) -> Mt5DataClient:
|
|
|
101
101
|
|
|
102
102
|
Raises:
|
|
103
103
|
RuntimeError: If the new MT5 client cannot be constructed or
|
|
104
|
-
initialized.
|
|
104
|
+
initialized.
|
|
105
105
|
"""
|
|
106
106
|
global _mt5_client # noqa: PLW0603
|
|
107
107
|
|
|
@@ -113,15 +113,8 @@ async def replace_mt5_client(config: Mt5Config) -> Mt5DataClient:
|
|
|
113
113
|
error_message = "Failed to initialize MT5 client"
|
|
114
114
|
raise RuntimeError(error_message) from e
|
|
115
115
|
|
|
116
|
-
old_client = _mt5_client
|
|
117
116
|
_mt5_client = new_client
|
|
118
117
|
|
|
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
118
|
return new_client
|
|
126
119
|
|
|
127
120
|
|
|
@@ -13,9 +13,9 @@ from fastapi.openapi.utils import get_openapi
|
|
|
13
13
|
|
|
14
14
|
from .auth import is_auth_enabled
|
|
15
15
|
from .config import (
|
|
16
|
-
get_configured_api_log_level,
|
|
17
16
|
get_configured_api_router_prefix,
|
|
18
17
|
get_configured_max_market_book_subscriptions,
|
|
18
|
+
get_configured_python_log_level,
|
|
19
19
|
)
|
|
20
20
|
from .constants import (
|
|
21
21
|
ACTIVE_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
|
|
@@ -31,6 +31,7 @@ from .constants import (
|
|
|
31
31
|
)
|
|
32
32
|
from .dependencies import release_market_book_subscriptions, shutdown_mt5_client
|
|
33
33
|
from .middleware import add_middleware
|
|
34
|
+
from .models import ErrorResponse
|
|
34
35
|
from .routers import (
|
|
35
36
|
account,
|
|
36
37
|
calc,
|
|
@@ -65,7 +66,7 @@ class _JsonFormatter(logging.Formatter):
|
|
|
65
66
|
|
|
66
67
|
def _configure_logging() -> None:
|
|
67
68
|
"""Configure structured logging for the API."""
|
|
68
|
-
log_level =
|
|
69
|
+
log_level = get_configured_python_log_level()
|
|
69
70
|
|
|
70
71
|
handler = logging.StreamHandler()
|
|
71
72
|
handler.setFormatter(_JsonFormatter())
|
|
@@ -96,6 +97,37 @@ def _strip_auth_from_openapi(openapi_schema: dict[str, Any]) -> None:
|
|
|
96
97
|
operation.pop("security", None)
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
def _patch_validation_error_responses(openapi_schema: dict[str, Any]) -> None:
|
|
101
|
+
"""Advertise RFC 7807 Problem Details for request validation failures."""
|
|
102
|
+
components = openapi_schema.setdefault("components", {})
|
|
103
|
+
if not isinstance(components, dict):
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
schemas = components.setdefault("schemas", {})
|
|
107
|
+
if isinstance(schemas, dict):
|
|
108
|
+
schemas["ErrorResponse"] = ErrorResponse.model_json_schema(
|
|
109
|
+
ref_template="#/components/schemas/{model}",
|
|
110
|
+
)
|
|
111
|
+
schemas.pop("HTTPValidationError", None)
|
|
112
|
+
schemas.pop("ValidationError", None)
|
|
113
|
+
|
|
114
|
+
error_response = {
|
|
115
|
+
"description": "Validation Error",
|
|
116
|
+
"content": {
|
|
117
|
+
"application/json": {
|
|
118
|
+
"schema": {"$ref": "#/components/schemas/ErrorResponse"},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for methods in openapi_schema.get("paths", {}).values():
|
|
124
|
+
if not isinstance(methods, dict):
|
|
125
|
+
continue
|
|
126
|
+
for operation in methods.values():
|
|
127
|
+
if isinstance(operation, dict) and "422" in operation.get("responses", {}):
|
|
128
|
+
operation["responses"]["422"] = error_response
|
|
129
|
+
|
|
130
|
+
|
|
99
131
|
def _build_openapi_schema(app: FastAPI) -> dict[str, Any]:
|
|
100
132
|
"""Build OpenAPI schema for the current authentication mode.
|
|
101
133
|
|
|
@@ -113,6 +145,7 @@ def _build_openapi_schema(app: FastAPI) -> dict[str, Any]:
|
|
|
113
145
|
)
|
|
114
146
|
if not is_auth_enabled():
|
|
115
147
|
_strip_auth_from_openapi(openapi_schema)
|
|
148
|
+
_patch_validation_error_responses(openapi_schema)
|
|
116
149
|
|
|
117
150
|
app.openapi_schema = openapi_schema
|
|
118
151
|
return openapi_schema
|
|
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from fastapi import Request, status
|
|
9
|
+
from fastapi import HTTPException, Request, status
|
|
10
|
+
from fastapi.exceptions import RequestValidationError
|
|
10
11
|
from fastapi.responses import JSONResponse
|
|
11
12
|
from pdmt5.mt5 import Mt5RuntimeError
|
|
12
13
|
from pydantic import ValidationError
|
|
@@ -14,6 +15,8 @@ from pydantic import ValidationError
|
|
|
14
15
|
from .models import ErrorResponse
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
17
20
|
from fastapi import FastAPI
|
|
18
21
|
from starlette.middleware.base import RequestResponseEndpoint
|
|
19
22
|
from starlette.responses import Response
|
|
@@ -21,6 +24,36 @@ if TYPE_CHECKING:
|
|
|
21
24
|
logger = logging.getLogger(__name__)
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
def _format_validation_error_location(location: Sequence[Any]) -> str:
|
|
28
|
+
"""Format a Pydantic error location for display.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
location: Pydantic validation error location tuple.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Human-readable field path without the request-body prefix.
|
|
35
|
+
"""
|
|
36
|
+
parts = [str(part) for part in location if str(part) != "body"]
|
|
37
|
+
return ".".join(parts) if parts else "request"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _format_request_validation_detail(exc: RequestValidationError) -> str:
|
|
41
|
+
"""Build a sanitized validation detail string without echoing request input.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
exc: FastAPI request validation error.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Validation summary containing field paths and messages only.
|
|
48
|
+
"""
|
|
49
|
+
details: list[str] = []
|
|
50
|
+
for error in exc.errors():
|
|
51
|
+
location = _format_validation_error_location(error.get("loc", ()))
|
|
52
|
+
message = str(error.get("msg", "Invalid value"))
|
|
53
|
+
details.append(f"{location}: {message}")
|
|
54
|
+
return "; ".join(details) if details else "Request validation failed"
|
|
55
|
+
|
|
56
|
+
|
|
24
57
|
def _create_error_response(
|
|
25
58
|
error_type: str,
|
|
26
59
|
title: str,
|
|
@@ -162,11 +195,58 @@ async def logging_middleware(
|
|
|
162
195
|
return response
|
|
163
196
|
|
|
164
197
|
|
|
198
|
+
def http_exception_handler(
|
|
199
|
+
request: Request,
|
|
200
|
+
exc: HTTPException,
|
|
201
|
+
) -> JSONResponse:
|
|
202
|
+
"""Convert FastAPI HTTP exceptions to flat RFC 7807 responses.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Problem Details JSON response.
|
|
206
|
+
"""
|
|
207
|
+
if isinstance(exc.detail, dict):
|
|
208
|
+
detail = exc.detail.get("detail", str(exc.detail))
|
|
209
|
+
error_type = str(exc.detail.get("type", "/errors/http-error"))
|
|
210
|
+
title = str(exc.detail.get("title", "HTTP Error"))
|
|
211
|
+
else:
|
|
212
|
+
detail = str(exc.detail)
|
|
213
|
+
error_type = "/errors/http-error"
|
|
214
|
+
title = "HTTP Error"
|
|
215
|
+
|
|
216
|
+
return _create_error_response(
|
|
217
|
+
error_type,
|
|
218
|
+
title,
|
|
219
|
+
exc.status_code,
|
|
220
|
+
detail,
|
|
221
|
+
str(request.url),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def request_validation_exception_handler(
|
|
226
|
+
request: Request,
|
|
227
|
+
exc: RequestValidationError,
|
|
228
|
+
) -> JSONResponse:
|
|
229
|
+
"""Convert FastAPI request validation failures to RFC 7807 responses.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Problem Details JSON response.
|
|
233
|
+
"""
|
|
234
|
+
return _create_error_response(
|
|
235
|
+
"/errors/validation-error",
|
|
236
|
+
"Request Validation Failed",
|
|
237
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
238
|
+
_format_request_validation_detail(exc),
|
|
239
|
+
str(request.url),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
165
243
|
def add_middleware(app: FastAPI) -> None:
|
|
166
244
|
"""Add middleware and error handlers to the FastAPI application.
|
|
167
245
|
|
|
168
246
|
Args:
|
|
169
247
|
app: FastAPI application instance.
|
|
170
248
|
"""
|
|
249
|
+
app.exception_handler(HTTPException)(http_exception_handler)
|
|
250
|
+
app.exception_handler(RequestValidationError)(request_validation_exception_handler)
|
|
171
251
|
app.middleware("http")(error_handler_middleware)
|
|
172
252
|
app.middleware("http")(logging_middleware)
|
|
@@ -29,8 +29,8 @@ router = APIRouter(
|
|
|
29
29
|
summary="Login to MT5 terminal",
|
|
30
30
|
description=(
|
|
31
31
|
"Reconnect the MT5 terminal using the supplied credentials. The current "
|
|
32
|
-
"MT5
|
|
33
|
-
"
|
|
32
|
+
"MT5 singleton is replaced after the new connection is established, then "
|
|
33
|
+
"any active market-book subscription tracking is cleared."
|
|
34
34
|
),
|
|
35
35
|
)
|
|
36
36
|
async def post_connection_login(
|
|
@@ -56,8 +56,8 @@ async def post_connection_login(
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
async with get_mt5_client_lock():
|
|
59
|
-
await release_market_book_subscriptions(app_request.app)
|
|
60
59
|
await replace_mt5_client(config)
|
|
60
|
+
await release_market_book_subscriptions(app_request.app)
|
|
61
61
|
|
|
62
62
|
logger.info(
|
|
63
63
|
"MT5 client reconnected (login=%d, server=%s)",
|
|
@@ -46,12 +46,12 @@ async def get_health() -> HealthResponse:
|
|
|
46
46
|
mt5_version = None
|
|
47
47
|
|
|
48
48
|
try:
|
|
49
|
-
client = get_mt5_client
|
|
50
|
-
mt5_connected = True
|
|
49
|
+
client = await run_in_threadpool(get_mt5_client)
|
|
51
50
|
version_dict = await run_in_threadpool(client.version_as_dict)
|
|
52
51
|
if version_dict:
|
|
53
|
-
mt5_version = f"{version_dict.get('
|
|
54
|
-
|
|
52
|
+
mt5_version = f"{version_dict.get('mt5_terminal_version', 'unknown')}"
|
|
53
|
+
mt5_connected = True
|
|
54
|
+
except Exception as e: # noqa: BLE001
|
|
55
55
|
logger.debug("MT5 not available for health check: %s", e)
|
|
56
56
|
|
|
57
57
|
return HealthResponse(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5api"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.1"
|
|
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"}]
|
|
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
|
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">= 3.11, < 3.14"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pdmt5 >= 1.
|
|
12
|
+
"pdmt5 >= 1.1.1",
|
|
13
13
|
"fastapi >= 0.115.0",
|
|
14
14
|
"uvicorn[standard] >= 0.32.0",
|
|
15
15
|
"pyarrow >= 18.0.0",
|
|
@@ -44,9 +44,9 @@ def mock_mt5_client() -> Mock:
|
|
|
44
44
|
|
|
45
45
|
# Mock version method
|
|
46
46
|
client.version_as_dict.return_value = {
|
|
47
|
-
"
|
|
47
|
+
"mt5_terminal_version": "5.0.4321",
|
|
48
48
|
"build": 4321,
|
|
49
|
-
"
|
|
49
|
+
"build_release_date": "2024-01-01",
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
# Mock account info
|