mt5cli 1.7.1__tar.gz → 1.7.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.
- mt5cli-1.7.3/.agents/skills/local-qa/SKILL.md +17 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.agents/skills/mt5cli/SKILL.md +28 -21
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/dependabot.yml +2 -2
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/renovate.json +1 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/workflows/ci.yml +6 -6
- mt5cli-1.7.3/.github/workflows/claude.yml +29 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/workflows/codeql.yml +1 -1
- mt5cli-1.7.3/.github/workflows/release.yml +228 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/PKG-INFO +15 -7
- {mt5cli-1.7.1 → mt5cli-1.7.3}/README.md +13 -5
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/analytics.py +1 -1
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/client.py +18 -2
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/history.py +95 -17
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/observability.py +2 -1
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/trading.py +24 -6
- {mt5cli-1.7.1 → mt5cli-1.7.3}/pyproject.toml +3 -2
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_analytics.py +27 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_client.py +57 -1
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_history.py +102 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_observability.py +22 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_trading.py +42 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/uv.lock +454 -404
- mt5cli-1.7.1/.agents/skills/local-qa/SKILL.md +0 -22
- mt5cli-1.7.1/.github/workflows/claude.yml +0 -56
- mt5cli-1.7.1/.github/workflows/release.yml +0 -44
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.claude/agents/codex.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.claude/settings.json +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/CODEOWNERS +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.github/FUNDING.yml +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/.gitignore +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/LICENSE +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/analytics.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/cli.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/client.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/converters.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/exceptions.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/history.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/index.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/marketdata.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/observability.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/public-contract.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/schemas.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/telemetry.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/trading.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/api/utils.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/docs/index.md +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mkdocs.yml +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/__main__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/cli.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/contract.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/converters.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/lifecycle.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/marketdata.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/rates.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/retry.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/schemas.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/sdk.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/telemetry.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/mt5cli/utils.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/architecture/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/architecture/test_unit_module_correspondence.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/cli/test_cli.bats +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/conftest.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/contracts/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/contracts/test_public_api.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/docs/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/docs/test_public_contract.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/docs/test_rate_storage_contract.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/integration/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/integration/test_history_capture_persist.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/packaging/test_metadata.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/__init__.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/conftest.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_cli.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_contract.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_converters.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_exceptions.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_lifecycle.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_marketdata.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_rates.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_retry.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_schemas.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_sdk.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_telemetry.py +0 -0
- {mt5cli-1.7.1 → mt5cli-1.7.3}/tests/unit/test_utils.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: local-qa
|
|
3
|
+
description: Run local QA including formatting, linting, and testing for the repository. Use whenever any file has been updated.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Local QA (format, lint, and test)
|
|
7
|
+
|
|
8
|
+
Run `scripts/qa.sh` relative to this skill's directory, not the shell's current working directory. The script changes to the repository root itself. It formats and fixes files in place, so check `git status` afterward.
|
|
9
|
+
|
|
10
|
+
After the run, summarize the outcome: success or failure, major warnings, and any files modified.
|
|
11
|
+
|
|
12
|
+
## When something fails
|
|
13
|
+
|
|
14
|
+
- **Missing tooling** (`command not found` or equivalent): install the tool, preferring the project's package manager (`uv`, `pnpm`), then a system package manager, then a language-specific installer (`pipx`, `npm`, `go install`). Rerun `scripts/qa.sh` and repeat until QA completes. If installation fails or needs privileges you lack, report what was attempted and the exact failure, then stop.
|
|
15
|
+
- **Any other failure**: fix the root cause and rerun until QA passes or you hit a blocker. Do not bypass or weaken checks, because the script is the repository's quality gate.
|
|
16
|
+
|
|
17
|
+
Run only commands needed for QA, fixes, and tool installation.
|
|
@@ -12,9 +12,10 @@ command. Output format is auto-detected from the file extension (`.csv`,
|
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- Install: `pip install -U mt5cli MetaTrader5`.
|
|
15
|
+
- Windows with a MetaTrader 5 terminal installed (required by the MetaTrader5
|
|
16
|
+
Python package).
|
|
17
|
+
- Install: `pip install -U mt5cli MetaTrader5`. Add the `parquet` extra
|
|
18
|
+
(`pip install -U "mt5cli[parquet]" MetaTrader5`) for Parquet output.
|
|
18
19
|
- In this repo, run via `uv run mt5cli ...` or `uv run python -m mt5cli ...`.
|
|
19
20
|
|
|
20
21
|
## Invocation shape
|
|
@@ -23,7 +24,8 @@ MT5 terminal).
|
|
|
23
24
|
mt5cli [GLOBAL OPTIONS] -o OUTPUT COMMAND [COMMAND OPTIONS]
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Put global options before the subcommand; they belong to the top-level
|
|
28
|
+
command, and `-o` is required.
|
|
27
29
|
|
|
28
30
|
### Global options (apply to every subcommand)
|
|
29
31
|
|
|
@@ -32,7 +34,7 @@ Global options MUST precede the subcommand.
|
|
|
32
34
|
| `-o, --output PATH` | Output file path (required). |
|
|
33
35
|
| `-f, --format FORMAT` | `csv`, `json`, `parquet`, or `sqlite3` (auto from extension). |
|
|
34
36
|
| `--table NAME` | Table name for SQLite3 output (default: `data`). |
|
|
35
|
-
| `--login
|
|
37
|
+
| `--login TEXT` | MT5 trading account login (`MT5_LOGIN`). |
|
|
36
38
|
| `--password TEXT` | MT5 trading account password (`MT5_PASSWORD`). |
|
|
37
39
|
| `--server TEXT` | MT5 trading server name (`MT5_SERVER`). |
|
|
38
40
|
| `--path TEXT` | Path to MetaTrader 5 terminal EXE (`MT5_PATH`). |
|
|
@@ -52,22 +54,22 @@ Global options MUST precede the subcommand.
|
|
|
52
54
|
|
|
53
55
|
## Commands
|
|
54
56
|
|
|
55
|
-
| Command | Required options | Optional options
|
|
56
|
-
| ----------------- | ----------------------------------------------------- |
|
|
57
|
-
| `rates-from` | `--symbol`, `--timeframe`, `--date-from`, `--count` | —
|
|
58
|
-
| `rates-from-pos` | `--symbol`, `--timeframe`, `--start-pos`, `--count` | —
|
|
59
|
-
| `rates-range` | `--symbol`, `--timeframe`, `--date-from`, `--date-to` | —
|
|
60
|
-
| `ticks-from` | `--symbol`, `--date-from`, `--count`, `--flags` | —
|
|
61
|
-
| `ticks-range` | `--symbol`, `--date-from`, `--date-to`, `--flags` | —
|
|
62
|
-
| `account-info` | — | —
|
|
63
|
-
| `terminal-info` | — | —
|
|
64
|
-
| `symbols` | — | `--group` (e.g., `*USD*`)
|
|
65
|
-
| `symbol-info` | `--symbol` | —
|
|
66
|
-
| `orders` | — | `--symbol`, `--group`, `--ticket`
|
|
67
|
-
| `positions` | — | `--symbol`, `--group`, `--ticket`
|
|
68
|
-
| `history-orders` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position`
|
|
69
|
-
| `history-deals` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position`
|
|
70
|
-
| `collect-history` | `--symbol` (repeatable), `--date-from`, `--date-to` | `--dataset` (repeatable; rates/
|
|
57
|
+
| Command | Required options | Optional options |
|
|
58
|
+
| ----------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `rates-from` | `--symbol`, `--timeframe`, `--date-from`, `--count` | — |
|
|
60
|
+
| `rates-from-pos` | `--symbol`, `--timeframe`, `--start-pos`, `--count` | — |
|
|
61
|
+
| `rates-range` | `--symbol`, `--timeframe`, `--date-from`, `--date-to` | — |
|
|
62
|
+
| `ticks-from` | `--symbol`, `--date-from`, `--count`, `--flags` | — |
|
|
63
|
+
| `ticks-range` | `--symbol`, `--date-from`, `--date-to`, `--flags` | — |
|
|
64
|
+
| `account-info` | — | — |
|
|
65
|
+
| `terminal-info` | — | — |
|
|
66
|
+
| `symbols` | — | `--group` (e.g., `*USD*`) |
|
|
67
|
+
| `symbol-info` | `--symbol` | — |
|
|
68
|
+
| `orders` | — | `--symbol`, `--group`, `--ticket` |
|
|
69
|
+
| `positions` | — | `--symbol`, `--group`, `--ticket` |
|
|
70
|
+
| `history-orders` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position` |
|
|
71
|
+
| `history-deals` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position` |
|
|
72
|
+
| `collect-history` | `--symbol` (repeatable), `--date-from`, `--date-to` | `--dataset` (repeatable; rates/history-orders/history-deals by default, `ticks` and `symbols` are opt-in), `--timeframe` (M1; recorded on rates), `--flags` (ALL), `--if-exists` (append/replace/fail; default fail), `--with-views` (needs history-deals) |
|
|
71
73
|
|
|
72
74
|
## Examples
|
|
73
75
|
|
|
@@ -100,6 +102,11 @@ mt5cli -o history.db collect-history \
|
|
|
100
102
|
|
|
101
103
|
## Guidelines
|
|
102
104
|
|
|
105
|
+
- The table lists the common export commands. Run `mt5cli COMMAND --help` for
|
|
106
|
+
exact options and for commands not listed here (for example `ticks-recent`,
|
|
107
|
+
`history-gaps`).
|
|
108
|
+
- `collect-history` writes SQLite3 only (`.db`/`.sqlite`/`.sqlite3` or
|
|
109
|
+
`--format sqlite3`).
|
|
103
110
|
- Pick the output extension to avoid passing `--format`.
|
|
104
111
|
- Use `--table` only with SQLite3 outputs; it is otherwise ignored.
|
|
105
112
|
- `--count` is required for `rates-from`, `rates-from-pos`, and `ticks-from`.
|
|
@@ -6,7 +6,7 @@ updates:
|
|
|
6
6
|
schedule:
|
|
7
7
|
interval: daily
|
|
8
8
|
cooldown:
|
|
9
|
-
default-days:
|
|
9
|
+
default-days: 7
|
|
10
10
|
open-pull-requests-limit: 10
|
|
11
11
|
groups:
|
|
12
12
|
github-actions-dependencies:
|
|
@@ -17,7 +17,7 @@ updates:
|
|
|
17
17
|
schedule:
|
|
18
18
|
interval: daily
|
|
19
19
|
cooldown:
|
|
20
|
-
default-days:
|
|
20
|
+
default-days: 7
|
|
21
21
|
open-pull-requests-limit: 10
|
|
22
22
|
groups:
|
|
23
23
|
uv-dependencies:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
3
|
"extends": ["config:recommended"],
|
|
4
4
|
"minimumReleaseAge": "1 day",
|
|
5
|
+
"lockFileMaintenance": {"enabled": true},
|
|
5
6
|
"packageRules": [
|
|
6
7
|
{
|
|
7
8
|
"description": "Automatically merge minor and patch-level updates",
|
|
@@ -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/gha-for-devops/.github/workflows/python-package-lint-and-scan.yml@
|
|
38
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-lint-and-scan.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
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/gha-for-devops/.github/workflows/python-package-test.yml@
|
|
49
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-test.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
50
50
|
with:
|
|
51
51
|
package-path: .
|
|
52
52
|
runs-on: windows-latest
|
|
@@ -57,7 +57,7 @@ jobs:
|
|
|
57
57
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
58
58
|
permissions:
|
|
59
59
|
contents: read
|
|
60
|
-
uses: dceoy/gha-for-devops/.github/workflows/bats-test.yml@
|
|
60
|
+
uses: dceoy/gha-for-devops/.github/workflows/bats-test.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
61
61
|
with:
|
|
62
62
|
search-path: ./tests/cli/
|
|
63
63
|
runs-on: windows-latest
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'docs-deploy')
|
|
68
68
|
permissions:
|
|
69
69
|
contents: write
|
|
70
|
-
uses: dceoy/gha-for-devops/.github/workflows/python-package-mkdocs-gh-deploy.yml@
|
|
70
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-mkdocs-gh-deploy.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
71
71
|
with:
|
|
72
72
|
package-path: .
|
|
73
73
|
runs-on: ubuntu-slim
|
|
@@ -75,12 +75,12 @@ jobs:
|
|
|
75
75
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
76
76
|
dependabot-auto-merge:
|
|
77
77
|
if: >
|
|
78
|
-
github.event_name == 'pull_request' && github.
|
|
78
|
+
github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
|
|
79
79
|
needs:
|
|
80
80
|
- python-lint-and-scan
|
|
81
81
|
- python-test
|
|
82
82
|
- bats-test
|
|
83
|
-
uses: dceoy/gha-for-devops/.github/workflows/dependabot-auto-merge.yml@
|
|
83
|
+
uses: dceoy/gha-for-devops/.github/workflows/dependabot-auto-merge.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
84
84
|
permissions:
|
|
85
85
|
contents: write
|
|
86
86
|
pull-requests: write
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Claude Code review
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
types:
|
|
8
|
+
- opened
|
|
9
|
+
- ready_for_review
|
|
10
|
+
paths-ignore:
|
|
11
|
+
- .github/workflows/claude.yml
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
jobs:
|
|
15
|
+
claude-code-review:
|
|
16
|
+
if: >
|
|
17
|
+
github.event_name == 'pull_request'
|
|
18
|
+
&& (! github.event.pull_request.draft)
|
|
19
|
+
&& github.event.pull_request.user.type != 'Bot'
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
pull-requests: write
|
|
23
|
+
issues: write
|
|
24
|
+
id-token: write
|
|
25
|
+
actions: read
|
|
26
|
+
uses: dceoy/pr-loop/.github/workflows/claude-code-review.yml@66193c0383663b48ea144b438d7dd4a42e02b6e2 # v1.1.5
|
|
27
|
+
secrets:
|
|
28
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
|
29
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
contents: read
|
|
37
37
|
security-events: write
|
|
38
38
|
actions: read
|
|
39
|
-
uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@
|
|
39
|
+
uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
40
40
|
with:
|
|
41
41
|
language: >
|
|
42
42
|
["actions","python"]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Release
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
concurrency:
|
|
8
|
+
group: python-release-${{ github.repository }}
|
|
9
|
+
cancel-in-progress: false
|
|
10
|
+
defaults:
|
|
11
|
+
run:
|
|
12
|
+
shell: bash -euo pipefail {0}
|
|
13
|
+
working-directory: .
|
|
14
|
+
jobs:
|
|
15
|
+
plan:
|
|
16
|
+
if: >-
|
|
17
|
+
github.repository == 'dceoy/mt5cli'
|
|
18
|
+
&& github.ref == 'refs/heads/main'
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
actions: read
|
|
22
|
+
contents: read
|
|
23
|
+
outputs:
|
|
24
|
+
release-tag: ${{ steps.plan.outputs.tag }}
|
|
25
|
+
source-sha: ${{ steps.plan.outputs.source-sha }}
|
|
26
|
+
steps:
|
|
27
|
+
- name: Check out validated source and tags
|
|
28
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
with:
|
|
30
|
+
ref: ${{ github.sha }}
|
|
31
|
+
fetch-depth: 0
|
|
32
|
+
persist-credentials: false
|
|
33
|
+
- name: Plan release from pyproject.toml
|
|
34
|
+
id: plan
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
SOURCE_SHA: ${{ github.sha }}
|
|
38
|
+
run: |
|
|
39
|
+
if [[ "$(git rev-parse HEAD)" != "${SOURCE_SHA}" || "${GITHUB_SHA}" != "${SOURCE_SHA}" ]]; then
|
|
40
|
+
echo '::notice::Workflow SHA differs from the validated source; skipping.'
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" --jq '.object.sha')"
|
|
44
|
+
if [[ "${main_sha}" != "${SOURCE_SHA}" ]]; then
|
|
45
|
+
echo '::notice::main advanced since validation; skipping stale release.'
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
ci_runs="$(gh run list --repo "${GITHUB_REPOSITORY}" --workflow ci.yml --commit "${SOURCE_SHA}" --event push --branch main --status success --json databaseId --jq 'length')"
|
|
49
|
+
if [[ "${ci_runs}" == 0 ]]; then
|
|
50
|
+
echo "::error::No successful CI/CD push run found for ${SOURCE_SHA}."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
version="$(python3 - <<'PY'
|
|
54
|
+
import tomllib
|
|
55
|
+
with open("pyproject.toml", "rb") as file:
|
|
56
|
+
print(tomllib.load(file)["project"]["version"])
|
|
57
|
+
PY
|
|
58
|
+
)"
|
|
59
|
+
if [[ ! "${version}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
|
60
|
+
echo '::error::project.version must be MAJOR.MINOR.PATCH.'
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
tag="v${version}"
|
|
64
|
+
if git rev-parse --verify -q "refs/tags/${tag}^{commit}" > /dev/null; then
|
|
65
|
+
tagged_sha="$(git rev-parse "refs/tags/${tag}^{commit}")"
|
|
66
|
+
if gh release view "${tag}" --json isDraft --jq '.isDraft' > /tmp/release-draft 2>/dev/null; then
|
|
67
|
+
if [[ "$(cat /tmp/release-draft)" == false ]]; then
|
|
68
|
+
echo "::notice::${tag} is already published; skipping."
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
if [[ "${tagged_sha}" != "${SOURCE_SHA}" ]]; then
|
|
73
|
+
echo "::error::${tag} already points to another commit."
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
latest="$(gh release list --json tagName,isLatest --jq '.[] | select(.isLatest) | .tagName')"
|
|
78
|
+
if [[ -n "${latest}" ]]; then
|
|
79
|
+
if [[ ! "${latest}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
80
|
+
echo "::error::Latest release tag is not stable SemVer: ${latest}"
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
if ! python3 - "${version}" "${latest#v}" <<'PY'
|
|
84
|
+
import sys
|
|
85
|
+
current, previous = (tuple(map(int, s.split("."))) for s in sys.argv[1:])
|
|
86
|
+
sys.exit(0 if current > previous else 1)
|
|
87
|
+
PY
|
|
88
|
+
then
|
|
89
|
+
echo "::error::project.version must exceed the latest release (${latest})."
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
{
|
|
94
|
+
echo "tag=${tag}"
|
|
95
|
+
echo "source-sha=${SOURCE_SHA}"
|
|
96
|
+
} >> "${GITHUB_OUTPUT}"
|
|
97
|
+
build:
|
|
98
|
+
needs: plan
|
|
99
|
+
if: needs.plan.outputs.release-tag != ''
|
|
100
|
+
# Nested jobs declare these permissions even when their publish flags are false.
|
|
101
|
+
permissions:
|
|
102
|
+
contents: write
|
|
103
|
+
id-token: write
|
|
104
|
+
uses: dceoy/gha-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
105
|
+
with:
|
|
106
|
+
package-path: .
|
|
107
|
+
create-github-release: false
|
|
108
|
+
publish-to-pypi: false
|
|
109
|
+
verify-source:
|
|
110
|
+
needs:
|
|
111
|
+
- plan
|
|
112
|
+
- build
|
|
113
|
+
if: needs.plan.outputs.release-tag != ''
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
permissions:
|
|
116
|
+
contents: read
|
|
117
|
+
outputs:
|
|
118
|
+
current: ${{ steps.verify.outputs.current }}
|
|
119
|
+
steps:
|
|
120
|
+
- name: Recheck that main still matches the validated source
|
|
121
|
+
id: verify
|
|
122
|
+
env:
|
|
123
|
+
GH_TOKEN: ${{ github.token }}
|
|
124
|
+
SOURCE_SHA: ${{ needs.plan.outputs.source-sha }}
|
|
125
|
+
RELEASE_TAG: ${{ needs.plan.outputs.release-tag }}
|
|
126
|
+
run: |
|
|
127
|
+
main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" --jq '.object.sha')"
|
|
128
|
+
if [[ -z "${SOURCE_SHA}" || "${main_sha}" != "${SOURCE_SHA}" ]]; then
|
|
129
|
+
echo '::notice::main advanced during the build; skipping stale release.'
|
|
130
|
+
echo 'current=false' >> "${GITHUB_OUTPUT}"
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
# The release helper accepts an existing tag without checking its commit.
|
|
134
|
+
# The last ls-remote line is the peeled commit for annotated tags.
|
|
135
|
+
tagged_sha="$(git ls-remote "https://github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${RELEASE_TAG}" "refs/tags/${RELEASE_TAG}^{}" | awk 'END {print $1}')"
|
|
136
|
+
if [[ -n "${tagged_sha}" && "${tagged_sha}" != "${SOURCE_SHA}" ]]; then
|
|
137
|
+
echo "::error::${RELEASE_TAG} points to ${tagged_sha}, not the validated source ${SOURCE_SHA}."
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo 'current=true' >> "${GITHUB_OUTPUT}"
|
|
141
|
+
draft-release:
|
|
142
|
+
needs:
|
|
143
|
+
- plan
|
|
144
|
+
- build
|
|
145
|
+
- verify-source
|
|
146
|
+
if: needs.verify-source.outputs.current == 'true'
|
|
147
|
+
permissions:
|
|
148
|
+
contents: write
|
|
149
|
+
uses: dceoy/gha-for-devops/.github/workflows/github-release.yml@9e394d7c18b3a00fb853cd3c693cc4eeb5f0c825 # v2.3.0
|
|
150
|
+
with:
|
|
151
|
+
tag-name: ${{ needs.plan.outputs.release-tag }}
|
|
152
|
+
create-new-tag: true
|
|
153
|
+
draft: true
|
|
154
|
+
attach-assets:
|
|
155
|
+
needs:
|
|
156
|
+
- build
|
|
157
|
+
- draft-release
|
|
158
|
+
runs-on: ubuntu-latest
|
|
159
|
+
permissions:
|
|
160
|
+
contents: write
|
|
161
|
+
id-token: write
|
|
162
|
+
steps:
|
|
163
|
+
- name: Download distributions
|
|
164
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
165
|
+
with:
|
|
166
|
+
name: ${{ needs.build.outputs.distribution-artifact-name }}
|
|
167
|
+
path: dist/
|
|
168
|
+
- name: Check distribution version
|
|
169
|
+
env:
|
|
170
|
+
RELEASE_TAG: ${{ needs.draft-release.outputs.release-tag }}
|
|
171
|
+
run: |
|
|
172
|
+
version="${RELEASE_TAG#v}"
|
|
173
|
+
test -f "dist/mt5cli-${version}.tar.gz"
|
|
174
|
+
shopt -s nullglob
|
|
175
|
+
wheels=(dist/mt5cli-"${version}"-*.whl)
|
|
176
|
+
if ((${#wheels[@]} == 0)); then
|
|
177
|
+
echo "::error::No wheel found for ${RELEASE_TAG}."
|
|
178
|
+
exit 1
|
|
179
|
+
fi
|
|
180
|
+
- name: Sign wheels with Sigstore
|
|
181
|
+
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
|
|
182
|
+
with:
|
|
183
|
+
inputs: ./dist/*.whl
|
|
184
|
+
- name: Attach distributions and signatures
|
|
185
|
+
env:
|
|
186
|
+
GH_TOKEN: ${{ github.token }}
|
|
187
|
+
RELEASE_TAG: ${{ needs.draft-release.outputs.release-tag }}
|
|
188
|
+
run: |
|
|
189
|
+
gh release upload "${RELEASE_TAG}" dist/* --repo "${GITHUB_REPOSITORY}" --clobber
|
|
190
|
+
publish-to-pypi:
|
|
191
|
+
needs:
|
|
192
|
+
- build
|
|
193
|
+
- attach-assets
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
environment:
|
|
196
|
+
name: pypi
|
|
197
|
+
url: https://pypi.org/p/mt5cli
|
|
198
|
+
permissions:
|
|
199
|
+
id-token: write
|
|
200
|
+
steps:
|
|
201
|
+
- name: Download distributions
|
|
202
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
203
|
+
with:
|
|
204
|
+
name: ${{ needs.build.outputs.distribution-artifact-name }}
|
|
205
|
+
path: dist/
|
|
206
|
+
- name: Publish distribution to PyPI
|
|
207
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
208
|
+
with:
|
|
209
|
+
verbose: true
|
|
210
|
+
# Recovery: if publish-to-pypi succeeded but this job failed, a rerun fails closed at
|
|
211
|
+
# PyPI (duplicate version) and re-uploads draft assets via `--clobber`. Do not publish
|
|
212
|
+
# the draft by hand until the wheel/sdist filenames and SHA-256 digests in
|
|
213
|
+
# https://pypi.org/pypi/mt5cli/<version>/json match the draft's assets; then run
|
|
214
|
+
# `gh release edit v<version> --draft=false`.
|
|
215
|
+
publish-release:
|
|
216
|
+
needs:
|
|
217
|
+
- draft-release
|
|
218
|
+
- publish-to-pypi
|
|
219
|
+
runs-on: ubuntu-latest
|
|
220
|
+
permissions:
|
|
221
|
+
contents: write
|
|
222
|
+
steps:
|
|
223
|
+
- name: Publish GitHub Release
|
|
224
|
+
env:
|
|
225
|
+
GH_TOKEN: ${{ github.token }}
|
|
226
|
+
RELEASE_TAG: ${{ needs.draft-release.outputs.release-tag }}
|
|
227
|
+
run: |
|
|
228
|
+
gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.3
|
|
4
4
|
Summary: Generic MT5 data and execution infrastructure for Python applications
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
18
|
Requires-Python: <3.15,>=3.11
|
|
19
19
|
Requires-Dist: pandas>=2.2.2
|
|
20
|
-
Requires-Dist: pdmt5>=1.2.
|
|
20
|
+
Requires-Dist: pdmt5>=1.2.4
|
|
21
21
|
Requires-Dist: typer>=0.26.0
|
|
22
22
|
Provides-Extra: otel
|
|
23
23
|
Requires-Dist: opentelemetry-api>=1.23.0; extra == 'otel'
|
|
@@ -211,7 +211,7 @@ python -m mt5cli -o account.csv account-info
|
|
|
211
211
|
| `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
|
|
212
212
|
| `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--filling-mode` / `--deviation` / `--comment` / `--magic`) |
|
|
213
213
|
| `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
|
|
214
|
-
| `history-gaps` | Export a SQLite-only one-row-per-gap report from
|
|
214
|
+
| `history-gaps` | Export a SQLite-only one-row-per-gap report from the collected `rates` table (or custom rate tables) without connecting to MT5 |
|
|
215
215
|
| `publish-dashboard` | Publish Parquet analytics datasets and a manifest from a SQLite history database for static dashboards (`--sqlite3`; `-o` is the manifest path; no MT5) |
|
|
216
216
|
| `snapshot` | Snapshot current account, position, order, and terminal state into SQLite |
|
|
217
217
|
|
|
@@ -223,13 +223,21 @@ and `MT5_PATH` when unset, and explicit CLI values still win.
|
|
|
223
223
|
|
|
224
224
|
### `history-gaps`
|
|
225
225
|
|
|
226
|
-
Inspect collected SQLite
|
|
227
|
-
For
|
|
228
|
-
|
|
226
|
+
Inspect collected SQLite rates offline and export one row per detected gap.
|
|
227
|
+
For the canonical `rates` table, the command infers bar spacing from each
|
|
228
|
+
series' `timeframe` column (series without a fixed interval, such as `MN1`, are
|
|
229
|
+
skipped). Use `--table` with `--granularity-seconds` for custom tables or views.
|
|
229
230
|
|
|
230
231
|
```bash
|
|
231
232
|
mt5cli -o gaps.json history-gaps --sqlite3 history.db
|
|
232
|
-
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`--table` targets a custom table or view that you create yourself. Such tables
|
|
236
|
+
carry no `timeframe` metadata, so pass the bar interval explicitly:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
sqlite3 history.db "CREATE VIEW eurusd_m1 AS SELECT time, close FROM rates WHERE symbol = 'EURUSD' AND timeframe = 1"
|
|
240
|
+
mt5cli -o eurusd.csv history-gaps --sqlite3 history.db --table eurusd_m1 --granularity-seconds 60
|
|
233
241
|
```
|
|
234
242
|
|
|
235
243
|
### `collect-history`
|
|
@@ -182,7 +182,7 @@ python -m mt5cli -o account.csv account-info
|
|
|
182
182
|
| `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
|
|
183
183
|
| `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--filling-mode` / `--deviation` / `--comment` / `--magic`) |
|
|
184
184
|
| `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
|
|
185
|
-
| `history-gaps` | Export a SQLite-only one-row-per-gap report from
|
|
185
|
+
| `history-gaps` | Export a SQLite-only one-row-per-gap report from the collected `rates` table (or custom rate tables) without connecting to MT5 |
|
|
186
186
|
| `publish-dashboard` | Publish Parquet analytics datasets and a manifest from a SQLite history database for static dashboards (`--sqlite3`; `-o` is the manifest path; no MT5) |
|
|
187
187
|
| `snapshot` | Snapshot current account, position, order, and terminal state into SQLite |
|
|
188
188
|
|
|
@@ -194,13 +194,21 @@ and `MT5_PATH` when unset, and explicit CLI values still win.
|
|
|
194
194
|
|
|
195
195
|
### `history-gaps`
|
|
196
196
|
|
|
197
|
-
Inspect collected SQLite
|
|
198
|
-
For
|
|
199
|
-
|
|
197
|
+
Inspect collected SQLite rates offline and export one row per detected gap.
|
|
198
|
+
For the canonical `rates` table, the command infers bar spacing from each
|
|
199
|
+
series' `timeframe` column (series without a fixed interval, such as `MN1`, are
|
|
200
|
+
skipped). Use `--table` with `--granularity-seconds` for custom tables or views.
|
|
200
201
|
|
|
201
202
|
```bash
|
|
202
203
|
mt5cli -o gaps.json history-gaps --sqlite3 history.db
|
|
203
|
-
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`--table` targets a custom table or view that you create yourself. Such tables
|
|
207
|
+
carry no `timeframe` metadata, so pass the bar interval explicitly:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
sqlite3 history.db "CREATE VIEW eurusd_m1 AS SELECT time, close FROM rates WHERE symbol = 'EURUSD' AND timeframe = 1"
|
|
211
|
+
mt5cli -o eurusd.csv history-gaps --sqlite3 history.db --table eurusd_m1 --granularity-seconds 60
|
|
204
212
|
```
|
|
205
213
|
|
|
206
214
|
### `collect-history`
|
|
@@ -200,7 +200,7 @@ _STRATEGY_STATS_SQL = (
|
|
|
200
200
|
_EQUITY_SQL = (
|
|
201
201
|
"SELECT time, date, ticket, position_id, symbol, magic, leg_index, role,"
|
|
202
202
|
" net_profit,"
|
|
203
|
-
" SUM(net_profit) OVER (ORDER BY time, ticket,
|
|
203
|
+
" SUM(net_profit) OVER (ORDER BY time, ticket, leg_index, role)"
|
|
204
204
|
" AS cumulative_net_profit"
|
|
205
205
|
" FROM analytics_realized_events"
|
|
206
206
|
)
|
|
@@ -21,7 +21,7 @@ import pandas as pd
|
|
|
21
21
|
from pdmt5 import Mt5Config, Mt5DataClient, Mt5RuntimeError
|
|
22
22
|
|
|
23
23
|
from .converters import ensure_trade_server_time, recent_window
|
|
24
|
-
from .exceptions import normalize_mt5_exception
|
|
24
|
+
from .exceptions import Mt5OperationError, normalize_mt5_exception
|
|
25
25
|
from .utils import coerce_login as _coerce_login
|
|
26
26
|
from .utils import parse_tick_flags, parse_timeframe
|
|
27
27
|
|
|
@@ -272,6 +272,22 @@ def _fetch_recent_ticks(
|
|
|
272
272
|
return frame
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def _fetch_market_book(client: Mt5DataClient, symbol: str) -> pd.DataFrame:
|
|
276
|
+
# MT5 only serves market depth for symbols subscribed via market_book_add,
|
|
277
|
+
# which reports failure by returning False rather than raising.
|
|
278
|
+
if not client.market_book_add(symbol):
|
|
279
|
+
msg = f"Failed to subscribe to market depth for {symbol!r}."
|
|
280
|
+
raise Mt5OperationError(msg)
|
|
281
|
+
try:
|
|
282
|
+
return client.market_book_get_as_df(symbol=symbol)
|
|
283
|
+
finally:
|
|
284
|
+
if not client.market_book_release(symbol):
|
|
285
|
+
logger.warning(
|
|
286
|
+
"Failed to release market depth subscription for %s.",
|
|
287
|
+
symbol,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
275
291
|
def _fetch_minimum_margins(client: Mt5DataClient, symbol: str) -> pd.DataFrame:
|
|
276
292
|
sym = client.symbol_info(symbol)
|
|
277
293
|
account = client.account_info()
|
|
@@ -822,7 +838,7 @@ class _BaseMT5Client:
|
|
|
822
838
|
|
|
823
839
|
def market_book(self, symbol: str) -> pd.DataFrame:
|
|
824
840
|
"""Return market depth for a symbol."""
|
|
825
|
-
return self._fetch(lambda c: c
|
|
841
|
+
return self._fetch(lambda c: _fetch_market_book(c, symbol))
|
|
826
842
|
|
|
827
843
|
def recent_ticks(
|
|
828
844
|
self,
|