mt5cli 1.7.2__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.
Files changed (87) hide show
  1. mt5cli-1.7.3/.agents/skills/local-qa/SKILL.md +17 -0
  2. {mt5cli-1.7.2 → mt5cli-1.7.3}/.agents/skills/mt5cli/SKILL.md +28 -21
  3. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/dependabot.yml +2 -2
  4. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/workflows/ci.yml +6 -6
  5. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/workflows/claude.yml +1 -1
  6. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/workflows/codeql.yml +1 -1
  7. mt5cli-1.7.3/.github/workflows/release.yml +228 -0
  8. {mt5cli-1.7.2 → mt5cli-1.7.3}/PKG-INFO +14 -6
  9. {mt5cli-1.7.2 → mt5cli-1.7.3}/README.md +13 -5
  10. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/analytics.py +1 -1
  11. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/client.py +18 -2
  12. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/history.py +95 -17
  13. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/observability.py +2 -1
  14. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/trading.py +24 -6
  15. {mt5cli-1.7.2 → mt5cli-1.7.3}/pyproject.toml +1 -1
  16. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_analytics.py +27 -0
  17. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_client.py +57 -1
  18. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_history.py +102 -0
  19. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_observability.py +22 -0
  20. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_trading.py +42 -0
  21. {mt5cli-1.7.2 → mt5cli-1.7.3}/uv.lock +82 -43
  22. mt5cli-1.7.2/.agents/skills/local-qa/SKILL.md +0 -22
  23. mt5cli-1.7.2/.github/workflows/release.yml +0 -44
  24. {mt5cli-1.7.2 → mt5cli-1.7.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  25. {mt5cli-1.7.2 → mt5cli-1.7.3}/.claude/agents/codex.md +0 -0
  26. {mt5cli-1.7.2 → mt5cli-1.7.3}/.claude/settings.json +0 -0
  27. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/CODEOWNERS +0 -0
  28. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/FUNDING.yml +0 -0
  29. {mt5cli-1.7.2 → mt5cli-1.7.3}/.github/renovate.json +0 -0
  30. {mt5cli-1.7.2 → mt5cli-1.7.3}/.gitignore +0 -0
  31. {mt5cli-1.7.2 → mt5cli-1.7.3}/LICENSE +0 -0
  32. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/analytics.md +0 -0
  33. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/cli.md +0 -0
  34. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/client.md +0 -0
  35. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/converters.md +0 -0
  36. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/exceptions.md +0 -0
  37. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/history.md +0 -0
  38. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/index.md +0 -0
  39. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/marketdata.md +0 -0
  40. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/observability.md +0 -0
  41. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/public-contract.md +0 -0
  42. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/schemas.md +0 -0
  43. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/telemetry.md +0 -0
  44. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/trading.md +0 -0
  45. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/api/utils.md +0 -0
  46. {mt5cli-1.7.2 → mt5cli-1.7.3}/docs/index.md +0 -0
  47. {mt5cli-1.7.2 → mt5cli-1.7.3}/mkdocs.yml +0 -0
  48. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/__init__.py +0 -0
  49. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/__main__.py +0 -0
  50. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/cli.py +0 -0
  51. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/contract.py +0 -0
  52. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/converters.py +0 -0
  53. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/exceptions.py +0 -0
  54. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/lifecycle.py +0 -0
  55. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/marketdata.py +0 -0
  56. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/rates.py +0 -0
  57. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/retry.py +0 -0
  58. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/schemas.py +0 -0
  59. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/sdk.py +0 -0
  60. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/telemetry.py +0 -0
  61. {mt5cli-1.7.2 → mt5cli-1.7.3}/mt5cli/utils.py +0 -0
  62. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/architecture/__init__.py +0 -0
  63. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/architecture/test_unit_module_correspondence.py +0 -0
  64. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/cli/test_cli.bats +0 -0
  65. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/conftest.py +0 -0
  66. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/contracts/__init__.py +0 -0
  67. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/contracts/test_public_api.py +0 -0
  68. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/docs/__init__.py +0 -0
  69. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/docs/test_public_contract.py +0 -0
  70. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/docs/test_rate_storage_contract.py +0 -0
  71. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/integration/__init__.py +0 -0
  72. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/integration/test_history_capture_persist.py +0 -0
  73. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/packaging/test_metadata.py +0 -0
  74. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/__init__.py +0 -0
  75. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/conftest.py +0 -0
  76. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_cli.py +0 -0
  77. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_contract.py +0 -0
  78. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_converters.py +0 -0
  79. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_exceptions.py +0 -0
  80. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_lifecycle.py +0 -0
  81. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_marketdata.py +0 -0
  82. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_rates.py +0 -0
  83. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_retry.py +0 -0
  84. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_schemas.py +0 -0
  85. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_sdk.py +0 -0
  86. {mt5cli-1.7.2 → mt5cli-1.7.3}/tests/unit/test_telemetry.py +0 -0
  87. {mt5cli-1.7.2 → 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
- MT5 terminal).
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
- Global options MUST precede the subcommand.
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 INT` | MT5 trading account login (`MT5_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/ticks/history-orders/history-deals; default all), `--timeframe` (M1; recorded on rates), `--flags` (ALL), `--if-exists` (append/replace/fail; default fail), `--with-views` (SQLite3 output only) |
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: 1
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: 1
20
+ default-days: 7
21
21
  open-pull-requests-limit: 10
22
22
  groups:
23
23
  uv-dependencies:
@@ -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@main # zizmor: ignore[unpinned-uses]
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@main # zizmor: ignore[unpinned-uses]
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@main # zizmor: ignore[unpinned-uses]
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@main # zizmor: ignore[unpinned-uses]
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.actor == 'dependabot[bot]'
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@main # zizmor: ignore[unpinned-uses]
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
@@ -23,7 +23,7 @@ jobs:
23
23
  issues: write
24
24
  id-token: write
25
25
  actions: read
26
- uses: dceoy/pr-loop/.github/workflows/claude-code-review.yml@main # zizmor: ignore[unpinned-uses]
26
+ uses: dceoy/pr-loop/.github/workflows/claude-code-review.yml@66193c0383663b48ea144b438d7dd4a42e02b6e2 # v1.1.5
27
27
  secrets:
28
28
  CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
29
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@main # zizmor: ignore[unpinned-uses]
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.2
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>
@@ -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 managed rate compatibility views without connecting to MT5 |
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 rate views offline and export one row per detected gap.
227
- For managed compatibility views, the command infers bar spacing from the view
228
- name. Use `--granularity-seconds` for custom tables or views.
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
- mt5cli -o eurusd.csv history-gaps --sqlite3 history.db --table rate_EURUSD__M1_1
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 managed rate compatibility views without connecting to MT5 |
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 rate views offline and export one row per detected gap.
198
- For managed compatibility views, the command infers bar spacing from the view
199
- name. Use `--granularity-seconds` for custom tables or views.
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
- mt5cli -o eurusd.csv history-gaps --sqlite3 history.db --table rate_EURUSD__M1_1
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, role, leg_index)"
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.market_book_get_as_df(symbol=symbol))
841
+ return self._fetch(lambda c: _fetch_market_book(c, symbol))
826
842
 
827
843
  def recent_ticks(
828
844
  self,