mt5cli 1.4.0__tar.gz → 1.4.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.
Files changed (78) hide show
  1. mt5cli-1.4.1/.agents/skills/local-qa/scripts/qa.sh +41 -0
  2. {mt5cli-1.4.0 → mt5cli-1.4.1}/.agents/skills/mt5cli/SKILL.md +2 -2
  3. mt5cli-1.4.1/.github/CODEOWNERS +2 -0
  4. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/workflows/ci.yml +11 -14
  5. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/workflows/claude.yml +2 -2
  6. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/workflows/release.yml +2 -2
  7. mt5cli-1.4.1/AGENTS.md +29 -0
  8. {mt5cli-1.4.0 → mt5cli-1.4.1}/PKG-INFO +2 -3
  9. {mt5cli-1.4.0 → mt5cli-1.4.1}/README.md +0 -1
  10. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/public-contract.md +23 -15
  11. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/index.md +0 -1
  12. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/__init__.py +4 -2
  13. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/contract.py +5 -1
  14. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/trading.py +0 -38
  15. {mt5cli-1.4.0 → mt5cli-1.4.1}/pyproject.toml +3 -3
  16. mt5cli-1.4.1/tests/cli/test_cli.bats +122 -0
  17. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_contracts.py +2 -2
  18. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_examples.py +1 -1
  19. mt5cli-1.4.1/tests/unit/test_stable_sdk_cleanup.py +31 -0
  20. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_trading.py +0 -86
  21. {mt5cli-1.4.0 → mt5cli-1.4.1}/uv.lock +219 -13
  22. mt5cli-1.4.0/.agents/skills/local-qa/scripts/qa.sh +0 -26
  23. mt5cli-1.4.0/.agents/skills/pr-feedback-triage/SKILL.md +0 -211
  24. mt5cli-1.4.0/AGENTS.md +0 -37
  25. {mt5cli-1.4.0 → mt5cli-1.4.1}/.agents/skills/local-qa/SKILL.md +0 -0
  26. {mt5cli-1.4.0 → mt5cli-1.4.1}/.claude/agents/codex.md +0 -0
  27. {mt5cli-1.4.0 → mt5cli-1.4.1}/.claude/settings.json +0 -0
  28. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/FUNDING.yml +0 -0
  29. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/dependabot.yml +0 -0
  30. {mt5cli-1.4.0 → mt5cli-1.4.1}/.github/renovate.json +0 -0
  31. {mt5cli-1.4.0 → mt5cli-1.4.1}/.gitignore +0 -0
  32. {mt5cli-1.4.0 → mt5cli-1.4.1}/CLAUDE.md +0 -0
  33. {mt5cli-1.4.0 → mt5cli-1.4.1}/LICENSE +0 -0
  34. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/cli.md +0 -0
  35. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/client.md +0 -0
  36. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/converters.md +0 -0
  37. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/exceptions.md +0 -0
  38. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/grafana.md +0 -0
  39. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/history.md +0 -0
  40. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/index.md +0 -0
  41. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/marketdata.md +0 -0
  42. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/observability.md +0 -0
  43. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/schemas.md +0 -0
  44. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/telemetry.md +0 -0
  45. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/trading.md +0 -0
  46. {mt5cli-1.4.0 → mt5cli-1.4.1}/docs/api/utils.md +0 -0
  47. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/README.md +0 -0
  48. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/compose.yml +0 -0
  49. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  50. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  51. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  52. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  53. {mt5cli-1.4.0 → mt5cli-1.4.1}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  54. {mt5cli-1.4.0 → mt5cli-1.4.1}/mkdocs.yml +0 -0
  55. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/__main__.py +0 -0
  56. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/cli.py +0 -0
  57. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/client.py +0 -0
  58. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/converters.py +0 -0
  59. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/exceptions.py +0 -0
  60. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/grafana.py +0 -0
  61. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/history.py +0 -0
  62. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/marketdata.py +0 -0
  63. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/observability.py +0 -0
  64. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/retry.py +0 -0
  65. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/schemas.py +0 -0
  66. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/telemetry.py +0 -0
  67. {mt5cli-1.4.0 → mt5cli-1.4.1}/mt5cli/utils.py +0 -0
  68. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/__init__.py +0 -0
  69. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/conftest.py +0 -0
  70. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_cli.py +0 -0
  71. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_client.py +0 -0
  72. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_grafana.py +0 -0
  73. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_history.py +0 -0
  74. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_marketdata.py +0 -0
  75. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_observability.py +0 -0
  76. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_packaging.py +0 -0
  77. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_telemetry.py +0 -0
  78. {mt5cli-1.4.0/tests → mt5cli-1.4.1/tests/unit}/test_utils.py +0 -0
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euox pipefail
4
+ cd "$(git rev-parse --show-toplevel)"
5
+
6
+ COOLDOWN_DAYS=7
7
+ export UV_EXCLUDE_NEWER="${COOLDOWN_DAYS} days"
8
+ export NPM_CONFIG_MIN_RELEASE_AGE="${COOLDOWN_DAYS}"
9
+ export PNPM_CONFIG_MINIMUM_RELEASE_AGE=$((COOLDOWN_DAYS * 24 * 60))
10
+
11
+ # Python
12
+ uv run ruff format .
13
+ uv run ruff check --fix .
14
+ uv run pyright .
15
+ uv run pytest
16
+
17
+ # Markdown and JSON
18
+ npx -y prettier --write './**/*.{md,json}'
19
+
20
+ # YAML
21
+ git ls-files -z -- '*.yml' \
22
+ | xargs -0 -t uvx yamllint -d '{"extends": "relaxed", "rules": {"line-length": "disable"}}'
23
+
24
+ # GitHub Actions
25
+ case "${OSTYPE}" in
26
+ darwin* | linux*)
27
+ # Shell scripts
28
+ git ls-files -z -- '*.sh' '*.bash' '*.bats' \
29
+ | xargs -0 -t shfmt --write --indent=2 --binary-next-line --case-indent --space-redirects
30
+ git ls-files -z -- '*.sh' '*.bash' '*.bats' \
31
+ | xargs -0 -t shellcheck
32
+
33
+ # GitHub Actions
34
+ zizmor --fix=safe .github/workflows
35
+ git ls-files -z -- '.github/workflows/*.yml' | xargs -0 -t actionlint
36
+ checkov --framework=all --output=github_failed_only --directory=.
37
+ ;;
38
+ *)
39
+ echo 'GitHub Actions and shell script linting is only supported on Linux and macOS.'
40
+ ;;
41
+ esac
@@ -12,8 +12,8 @@ command. Output format is auto-detected from the file extension (`.csv`,
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Python 3.11+ on Windows with MetaTrader 5 installed (pdmt5 requires the
16
- MT5 terminal).
15
+ MT5 terminal).
16
+
17
17
  - Install: `pip install -U mt5cli MetaTrader5`.
18
18
  - In this repo, run via `uv run mt5cli ...` or `uv run python -m mt5cli ...`.
19
19
 
@@ -0,0 +1,2 @@
1
+ # Default owner
2
+ * @dceoy
@@ -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/gh-actions-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@main # zizmor: ignore[unpinned-uses]
39
39
  with:
40
40
  package-path: .
41
41
  runs-on: windows-latest
@@ -46,32 +46,28 @@ jobs:
46
46
  || (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
47
47
  permissions:
48
48
  contents: read
49
- uses: dceoy/gh-actions-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@main # zizmor: ignore[unpinned-uses]
50
50
  with:
51
51
  package-path: .
52
52
  runs-on: windows-latest
53
- minimum-dependency-smoke-test:
53
+ bats-test:
54
54
  if: >
55
55
  github.event_name == 'push'
56
56
  || github.event_name == 'pull_request'
57
57
  || (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
58
58
  permissions:
59
59
  contents: read
60
- runs-on: windows-latest
61
- steps:
62
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6.0.2
63
- with:
64
- persist-credentials: false
65
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
66
- - name: Check CLI help with minimum Click and Typer versions
67
- run: uv run --with click==8.4.0 --with typer==0.26.0 mt5cli --help
60
+ uses: dceoy/gha-for-devops/.github/workflows/bats-test.yml@main # zizmor: ignore[unpinned-uses]
61
+ with:
62
+ search-path: ./tests/cli/
63
+ runs-on: windows-latest
68
64
  python-docs-deploy:
69
65
  if: >
70
66
  github.event_name == 'push'
71
67
  || (github.event_name == 'workflow_dispatch' && inputs.workflow == 'docs-deploy')
72
68
  permissions:
73
69
  contents: write
74
- uses: dceoy/gh-actions-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@main # zizmor: ignore[unpinned-uses]
75
71
  with:
76
72
  package-path: .
77
73
  runs-on: ubuntu-slim
@@ -86,7 +82,7 @@ jobs:
86
82
  contents: read
87
83
  security-events: write
88
84
  actions: read
89
- uses: dceoy/gh-actions-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
85
+ uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
90
86
  with:
91
87
  language: >
92
88
  ["python"]
@@ -96,7 +92,8 @@ jobs:
96
92
  needs:
97
93
  - python-lint-and-scan
98
94
  - python-test
99
- uses: dceoy/gh-actions-for-devops/.github/workflows/dependabot-auto-merge.yml@main # zizmor: ignore[unpinned-uses]
95
+ - bats-test
96
+ uses: dceoy/gha-for-devops/.github/workflows/dependabot-auto-merge.yml@main # zizmor: ignore[unpinned-uses]
100
97
  permissions:
101
98
  contents: write
102
99
  pull-requests: write
@@ -35,7 +35,7 @@ jobs:
35
35
  issues: write
36
36
  id-token: write
37
37
  actions: read
38
- uses: dceoy/gh-actions-for-devops/.github/workflows/claude-code-review.yml@main # zizmor: ignore[unpinned-uses]
38
+ uses: dceoy/gha-for-devops/.github/workflows/claude-code-review.yml@main # zizmor: ignore[unpinned-uses]
39
39
  secrets:
40
40
  CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
41
41
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
51
51
  issues: write
52
52
  id-token: write
53
53
  actions: read
54
- uses: dceoy/gh-actions-for-devops/.github/workflows/claude-code-bot.yml@main # zizmor: ignore[unpinned-uses]
54
+ uses: dceoy/gha-for-devops/.github/workflows/claude-code-bot.yml@main # zizmor: ignore[unpinned-uses]
55
55
  secrets:
56
56
  CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
57
57
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -13,7 +13,7 @@ jobs:
13
13
  permissions:
14
14
  contents: write
15
15
  id-token: write
16
- uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
16
+ uses: dceoy/gha-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
17
17
  with:
18
18
  package-path: .
19
19
  create-github-release: true
@@ -39,6 +39,6 @@ jobs:
39
39
  name: ${{ needs.build-and-release.outputs.distribution-artifact-name }}
40
40
  path: dist/
41
41
  - name: Publish distribution 📦 to PyPI
42
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
42
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
43
43
  with:
44
44
  verbose: true
mt5cli-1.4.1/AGENTS.md ADDED
@@ -0,0 +1,29 @@
1
+ # Repository Guidelines
2
+
3
+ ## Build, Test, and Development Commands
4
+
5
+ Use `.agents/skills/local-qa/SKILL.md` for pre-handoff QA. It runs `.agents/skills/local-qa/scripts/qa.sh`, which formats, lints, type-checks, tests, formats Markdown, and checks GitHub workflows.
6
+
7
+ `bats --verbose-run ./tests/cli/` runs the Windows/MT5-oriented CLI tests against the installed `mt5cli` entry point. These require a Windows environment where `uv sync` can install MetaTrader5; state clearly when they were not run.
8
+
9
+ `skills/mt5cli/SKILL.md` documents the mt5cli agent skill.
10
+
11
+ ## Coding Style & Naming Conventions
12
+
13
+ Preserve the package’s small, typed helper style rather than adding broad abstractions.
14
+
15
+ ## Design Principles
16
+
17
+ Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest implementation that satisfies the current CLI/API contract. Remove duplication when shared behavior is already proven by at least two concrete call sites, but avoid generic helpers for speculative reuse. Do not add configuration flags, extension hooks, or alternate backends until a real repository use case requires them.
18
+
19
+ ## Testing Guidelines
20
+
21
+ Parametrize unit tests with `pytest.mark.parametrize` when the same behavior needs coverage across multiple inputs. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
22
+
23
+ ## Commit & Pull Request Guidelines
24
+
25
+ Recent history uses concise imperative commits, sometimes with conventional prefixes such as `feat:` or `chore:` and PR numbers appended by GitHub. Keep commits scoped to one logical change. Pull requests should describe behavior changes, note tests run, link related issues, and call out MT5/live-trading risk where relevant.
26
+
27
+ ## Security & Configuration Tips
28
+
29
+ Never commit account credentials, broker passwords, exported private data, or local `.venv` contents. Treat `order_send` and CLI `order-send --yes` as live execution paths; gate examples and tests so they cannot place real trades accidentally.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 1.4.0
3
+ Version: 1.4.1
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>
@@ -15,7 +15,7 @@ Classifier: Operating System :: Microsoft :: Windows
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Office/Business :: Financial :: Investment
18
- Requires-Python: <3.14,>=3.11
18
+ Requires-Python: <3.15,>=3.11
19
19
  Requires-Dist: pandas>=2.2.2
20
20
  Requires-Dist: pdmt5>=1.2.0
21
21
  Requires-Dist: typer>=0.26.0
@@ -385,7 +385,6 @@ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
385
385
 
386
386
  ## Requirements
387
387
 
388
- - Python 3.11+
389
388
  - Windows OS (MetaTrader 5 requirement)
390
389
  - MetaTrader 5 platform installed
391
390
 
@@ -356,7 +356,6 @@ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
356
356
 
357
357
  ## Requirements
358
358
 
359
- - Python 3.11+
360
359
  - Windows OS (MetaTrader 5 requirement)
361
360
  - MetaTrader 5 platform installed
362
361
 
@@ -24,10 +24,11 @@ in pdmt5 — the name changed, it was not simply moved.
24
24
 
25
25
  Downstream packages should import from the package root (`from mt5cli import
26
26
  ...`). The contract set `STABLE_SDK_EXPORTS` in `mt5cli.contract` enumerates
27
- every package-root symbol. Lower-level helpers (schema utilities, export
28
- functions, parser helpers, low-level MT5 wrappers) are available directly from
29
- their owning modules (`mt5cli.schemas`, `mt5cli.utils`, `mt5cli.converters`,
30
- `mt5cli.marketdata`, etc.) and are not part of the root SDK surface.
27
+ every package-root symbol. Helpers promoted for downstream use, including selected configuration and
28
+ parsing utilities, history timeframe resolution, and Grafana schema setup, are
29
+ part of that root contract. Other low-level
30
+ schema, export, conversion, and implementation helpers remain available only
31
+ from their owning modules.
31
32
 
32
33
  ### Module responsibility map
33
34
 
@@ -56,13 +57,14 @@ These names are exported from `mt5cli` and enumerated in
56
57
 
57
58
  ### Session lifecycle and configuration
58
59
 
59
- | Symbol | Role |
60
- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
- | `MT5Client` | The single connected client for data, history, calculations, order checking, and order sending |
62
- | `build_config` | Build `pdmt5.Mt5Config` from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
63
- | `mt5_session` | Canonical context manager: initialize/login once, yield `MT5Client`, shut down once; a supplied `client=` remains caller-owned. |
64
- | `AccountSpec` | Generic account group: symbols plus optional credentials |
65
- | `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
60
+ | Symbol | Role |
61
+ | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `MT5Client` | The single connected client for data, history, calculations, order checking, and order sending |
63
+ | `build_config` | Build the underlying MT5 connection config from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
64
+ | `substitute_mapping_values` | Generic selected-key environment substitution for downstream configuration adapters |
65
+ | `mt5_session` | Canonical context manager: initialize/login once, yield `MT5Client`, shut down once; a supplied `client=` remains caller-owned. |
66
+ | `AccountSpec` | Generic account group: symbols plus optional credentials |
67
+ | `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
66
68
 
67
69
  ### Closed-bar rate helpers
68
70
 
@@ -93,6 +95,11 @@ timestamp normalization in downstream apps.
93
95
  See [History Collection (SQLite)](history.md) for schema, view naming, and ER
94
96
  diagrams.
95
97
 
98
+ Shared package-root helpers used by downstream adapters include `Dataset`,
99
+ `parse_timeframe`, and `resolve_history_timeframes`. These are stable exports;
100
+ downstream applications should not import them from `mt5cli.utils` or
101
+ `mt5cli.history`.
102
+
96
103
  ### Trading and sizing primitives (generic)
97
104
 
98
105
  These helpers implement broker-facing calculations only. They do not encode
@@ -108,7 +115,7 @@ strategy entries, exits, Kelly sizing, or signal logic.
108
115
  | `CalibrationStatus`, `ClockStatus` | Literal status types for clock calibration and normalization |
109
116
  | `detect_position_side` | Net long / short / flat from open positions |
110
117
  | `calculate_spread_ratio` | Relative bid-ask spread |
111
- | `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_new_position_margin_ratio` | Margin budget and volume sizing |
118
+ | `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_account_projected_margin_ratio` | Margin budget and volume sizing |
112
119
  | `normalize_order_volume`, `estimate_order_margin`, `calculate_positions_margin` | Broker volume normalization and margin totals |
113
120
  | `calculate_positions_margin_by_symbol` | Per-symbol margin map (resilient, first-seen order) |
114
121
  | `calculate_positions_margin_safe` | Summed total margin across symbols (failed symbols skipped) |
@@ -230,8 +237,9 @@ workflows call the facade's canonical data methods (`account_info`, `positions`,
230
237
  | `grafana_realized_pnl` | Cumulative realized PnL per symbol |
231
238
  | `grafana_trade_stats` | Win/loss counts and profit per symbol |
232
239
 
233
- Lower-level helpers (`ensure_grafana_schema`, `create_grafana_views`,
234
- `create_grafana_indexes`, `create_snapshot_tables`, `start_snapshot_run`,
240
+ `ensure_grafana_schema` is a stable package-root helper for downstream setup.
241
+ Other lower-level helpers (`create_grafana_views`, `create_grafana_indexes`,
242
+ `create_snapshot_tables`, `start_snapshot_run`,
235
243
  `insert_account_snapshot`, `insert_position_snapshots`, `insert_order_snapshots`,
236
244
  `insert_terminal_snapshot`, `record_snapshot_run`) are available directly from
237
245
  `mt5cli.grafana` and are not part of the package-root stable surface.
@@ -367,7 +375,7 @@ their own adapter layer.
367
375
 
368
376
  ## Contract verification
369
377
 
370
- `tests/test_contracts.py` asserts that every name in `STABLE_SDK_EXPORTS` is
378
+ `tests/unit/test_contracts.py` asserts that every name in `STABLE_SDK_EXPORTS` is
371
379
  importable from `mt5cli`, that all package-root exports are covered by the
372
380
  stable set, and documents key closed-bar, SQLite loading, account-resolution,
373
381
  and trading-session behaviors.
@@ -224,7 +224,6 @@ See the [History schema diagram](api/history.md#entity-relationship-diagram) for
224
224
 
225
225
  ## Requirements
226
226
 
227
- - Python 3.11+
228
227
  - Windows OS (MetaTrader 5 requirement)
229
228
  - MetaTrader 5 platform
230
229
 
@@ -9,7 +9,7 @@ strategy responsibilities.
9
9
 
10
10
  from importlib.metadata import version
11
11
 
12
- from .client import MT5Client, build_config, mt5_session
12
+ from .client import MT5Client, build_config, mt5_session, substitute_mapping_values
13
13
  from .contract import STABLE_SDK_EXPORTS
14
14
  from .exceptions import (
15
15
  Mt5CliError,
@@ -17,6 +17,7 @@ from .exceptions import (
17
17
  Mt5OperationError,
18
18
  Mt5SchemaError,
19
19
  )
20
+ from .grafana import ensure_grafana_schema
20
21
  from .history import (
21
22
  RateTarget,
22
23
  ThrottledHistoryUpdater,
@@ -26,6 +27,7 @@ from .history import (
26
27
  load_rate_series_by_granularity,
27
28
  load_rate_series_from_sqlite,
28
29
  report_rate_gaps,
30
+ resolve_history_timeframes,
29
31
  update_history,
30
32
  update_history_with_config,
31
33
  )
@@ -56,7 +58,6 @@ from .trading import (
56
58
  TickClockNormalizer,
57
59
  calculate_account_projected_margin_ratio,
58
60
  calculate_margin_and_volume,
59
- calculate_new_position_margin_ratio,
60
61
  calculate_positions_margin,
61
62
  calculate_positions_margin_by_symbol,
62
63
  calculate_positions_margin_safe,
@@ -82,6 +83,7 @@ from .trading import (
82
83
  update_sltp_for_open_positions,
83
84
  update_trailing_stop_loss_for_open_positions,
84
85
  )
86
+ from .utils import Dataset, parse_timeframe
85
87
 
86
88
  __version__ = version(__package__) if __package__ else None
87
89
 
@@ -123,11 +123,11 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
123
123
  "TickClockNormalizer",
124
124
  "RateTarget",
125
125
  "ThrottledHistoryUpdater",
126
+ "Dataset",
126
127
  "build_config",
127
128
  "build_rate_targets",
128
129
  "calculate_account_projected_margin_ratio",
129
130
  "calculate_margin_and_volume",
130
- "calculate_new_position_margin_ratio",
131
131
  "calculate_projected_margin_ratio",
132
132
  "calculate_positions_margin",
133
133
  "calculate_positions_margin_by_symbol",
@@ -144,6 +144,7 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
144
144
  "detect_position_side",
145
145
  "determine_order_limits",
146
146
  "drop_forming_rate_bar",
147
+ "ensure_grafana_schema",
147
148
  "ensure_symbol_selected",
148
149
  "estimate_order_margin",
149
150
  "extract_tick_price",
@@ -157,11 +158,14 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
157
158
  "load_rate_series_from_sqlite",
158
159
  "mt5_session",
159
160
  "normalize_order_volume",
161
+ "parse_timeframe",
160
162
  "place_market_order",
161
163
  "report_rate_gaps",
162
164
  "resolve_broker_filling_mode",
163
165
  "resolve_account_spec",
164
166
  "resolve_account_specs",
167
+ "resolve_history_timeframes",
168
+ "substitute_mapping_values",
165
169
  "update_history",
166
170
  "update_history_with_config",
167
171
  "update_observability",
@@ -310,7 +310,6 @@ __all__ = [
310
310
  "TickClockNormalizer",
311
311
  "calculate_account_projected_margin_ratio",
312
312
  "calculate_margin_and_volume",
313
- "calculate_new_position_margin_ratio",
314
313
  "calculate_positions_margin",
315
314
  "calculate_positions_margin_by_symbol",
316
315
  "calculate_positions_margin_safe",
@@ -2269,43 +2268,6 @@ def calculate_spread_ratio(client: _Mt5ClientProtocol, symbol: str) -> float:
2269
2268
  return (ask - bid) / ((ask + bid) / 2.0)
2270
2269
 
2271
2270
 
2272
- def calculate_new_position_margin_ratio(
2273
- client: _Mt5ClientProtocol,
2274
- *,
2275
- symbol: str,
2276
- new_position_side: OrderSide | None = None,
2277
- new_position_volume: float = 0.0,
2278
- ) -> float:
2279
- """Return total margin/equity ratio after an optional hypothetical position.
2280
-
2281
- Missing or falsy account ``equity``/``margin`` values are normalized to
2282
- ``0.0``, and the hypothetical order margin from ``order_calc_margin()`` is
2283
- accepted as-is without a positivity check.
2284
-
2285
- Raises:
2286
- Mt5OperationError: If equity or required tick data is invalid.
2287
- """
2288
- account = get_account_snapshot(client)
2289
- equity = float(account.get("equity") or 0.0)
2290
- if equity <= 0:
2291
- msg = "Account equity must be positive to calculate margin ratio."
2292
- raise Mt5OperationError(msg)
2293
- margin = float(account.get("margin") or 0.0)
2294
- if new_position_side is not None and new_position_volume > 0:
2295
- side = _normalize_order_side(new_position_side)
2296
- price = extract_tick_price(
2297
- get_tick_snapshot(client, symbol), "ask" if side == "BUY" else "bid"
2298
- )
2299
- price = _require_tick_price(price, symbol)
2300
- order_type = (
2301
- client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
2302
- )
2303
- margin += float(
2304
- client.order_calc_margin(order_type, symbol, new_position_volume, price),
2305
- )
2306
- return margin / equity
2307
-
2308
-
2309
2271
  def _account_equity(client: _Mt5ClientProtocol) -> float:
2310
2272
  account = get_account_snapshot(client)
2311
2273
  return _required_account_number(account, "equity", allow_zero=False)
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "mt5cli"
3
- version = "1.4.0"
3
+ version = "1.4.1"
4
4
  description = "Generic MT5 data and execution infrastructure for Python applications"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
7
7
  license = "MIT"
8
8
  license-files = ["LICENSE"]
9
9
  readme = "README.md"
10
- requires-python = ">= 3.11, < 3.14"
10
+ requires-python = ">= 3.11, < 3.15"
11
11
  dependencies = [
12
12
  "pdmt5 >= 1.2.0",
13
13
  "pandas >= 2.2.2",
@@ -175,7 +175,7 @@ addopts = [
175
175
  "--capture=no",
176
176
  ]
177
177
  pythonpath = ["."]
178
- testpaths = ["tests"]
178
+ testpaths = ["tests/unit"]
179
179
  python_files = ["test_*.py", "*_test.py"]
180
180
  python_classes = ["Test*"]
181
181
  python_functions = ["test_*"]
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # CLI integration tests for the installed mt5cli entry point.
4
+ #
5
+ # These cases only exercise paths that never connect to MetaTrader 5: help
6
+ # output, SQLite-only commands, and validation failures. No case passes --yes,
7
+ # so the suite can never place a live trade.
8
+
9
+ setup_file() {
10
+ set -euo pipefail
11
+ uv sync
12
+ }
13
+
14
+ teardown_file() {
15
+ :
16
+ }
17
+
18
+ seed_rate_database() {
19
+ local database="$1"
20
+ cat > "${BATS_TEST_TMPDIR}/seed_rates.py" << 'PY'
21
+ import sqlite3
22
+ import sys
23
+
24
+ with sqlite3.connect(sys.argv[1]) as conn:
25
+ conn.execute('CREATE TABLE "rate_EURUSD__M1_1"(time TEXT, close REAL)')
26
+ conn.executemany(
27
+ 'INSERT INTO "rate_EURUSD__M1_1"(time, close) VALUES (?, ?)',
28
+ [("2024-01-01T00:00:00+00:00", 1.0), ("2024-01-01T00:02:00+00:00", 1.1)],
29
+ )
30
+ PY
31
+ uv run python "${BATS_TEST_TMPDIR}/seed_rates.py" "${database}"
32
+ }
33
+
34
+ @test "pass with \"mt5cli --help\"" {
35
+ run uv run mt5cli --help
36
+ [[ "${status}" -eq 0 ]]
37
+ }
38
+
39
+ @test "pass with \"mt5cli collect-history --help\"" {
40
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/history.db" collect-history --help
41
+ [[ "${status}" -eq 0 ]]
42
+ }
43
+
44
+ @test "pass with \"mt5cli order-send --help\"" {
45
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/out.csv" order-send --help
46
+ [[ "${status}" -eq 0 ]]
47
+ }
48
+
49
+ @test "pass with \"mt5cli grafana-schema\"" {
50
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/grafana.db" grafana-schema
51
+ [[ "${status}" -eq 0 ]]
52
+ [[ -f "${BATS_TEST_TMPDIR}/grafana.db" ]]
53
+ }
54
+
55
+ @test "pass with \"mt5cli grafana-schema\" run twice" {
56
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/grafana.db" grafana-schema
57
+ [[ "${status}" -eq 0 ]]
58
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/grafana.db" grafana-schema
59
+ [[ "${status}" -eq 0 ]]
60
+ }
61
+
62
+ @test "pass with \"mt5cli grafana-schema --publish-copy\"" {
63
+ run uv run mt5cli \
64
+ --output "${BATS_TEST_TMPDIR}/grafana.db" \
65
+ grafana-schema \
66
+ --publish-copy "${BATS_TEST_TMPDIR}/published.db"
67
+ [[ "${status}" -eq 0 ]]
68
+ [[ -f "${BATS_TEST_TMPDIR}/published.db" ]]
69
+ }
70
+
71
+ @test "pass with \"mt5cli history-gaps\"" {
72
+ seed_rate_database "${BATS_TEST_TMPDIR}/history.db"
73
+ run uv run mt5cli \
74
+ --output "${BATS_TEST_TMPDIR}/gaps.json" \
75
+ history-gaps \
76
+ --sqlite3 "${BATS_TEST_TMPDIR}/history.db"
77
+ [[ "${status}" -eq 0 ]]
78
+ [[ -f "${BATS_TEST_TMPDIR}/gaps.json" ]]
79
+ }
80
+
81
+ @test "fail with \"mt5cli history-gaps\" for a missing database" {
82
+ run uv run mt5cli \
83
+ --output "${BATS_TEST_TMPDIR}/gaps.json" \
84
+ history-gaps \
85
+ --sqlite3 "${BATS_TEST_TMPDIR}/no-such.db"
86
+ [[ "${status}" -ne 0 ]]
87
+ [[ ! -e "${BATS_TEST_TMPDIR}/no-such.db" ]]
88
+ }
89
+
90
+ @test "fail with \"mt5cli order-send\" without --yes" {
91
+ run uv run mt5cli \
92
+ --output "${BATS_TEST_TMPDIR}/out.csv" \
93
+ order-send \
94
+ --request '{"action": 1, "symbol": "EURUSD"}'
95
+ [[ "${status}" -ne 0 ]]
96
+ [[ ! -e "${BATS_TEST_TMPDIR}/out.csv" ]]
97
+ }
98
+
99
+ @test "fail with \"mt5cli close-positions\" without a symbol or ticket" {
100
+ run uv run mt5cli \
101
+ --output "${BATS_TEST_TMPDIR}/close.json" \
102
+ close-positions
103
+ [[ "${status}" -ne 0 ]]
104
+ [[ ! -e "${BATS_TEST_TMPDIR}/close.json" ]]
105
+ }
106
+
107
+ @test "fail with \"mt5cli collect-history\" without SQLite3 output" {
108
+ run uv run mt5cli \
109
+ --output "${BATS_TEST_TMPDIR}/history.csv" \
110
+ collect-history \
111
+ --symbol EURUSD \
112
+ --date-from 2024-01-01 \
113
+ --date-to 2024-02-01
114
+ [[ "${status}" -ne 0 ]]
115
+ [[ ! -e "${BATS_TEST_TMPDIR}/history.csv" ]]
116
+ }
117
+
118
+ @test "fail with an undetectable output format" {
119
+ run uv run mt5cli --output "${BATS_TEST_TMPDIR}/out.xyz" account-info
120
+ [[ "${status}" -ne 0 ]]
121
+ [[ ! -e "${BATS_TEST_TMPDIR}/out.xyz" ]]
122
+ }
@@ -526,7 +526,7 @@ class TestStableSdkContract:
526
526
  def test_documented_contract_identifies_runtime_export_set(self) -> None:
527
527
  """The public-contract document points to the same runtime authority."""
528
528
  contract_doc = (
529
- Path(__file__).parents[1] / "docs" / "api" / "public-contract.md"
529
+ Path(__file__).parents[2] / "docs" / "api" / "public-contract.md"
530
530
  ).read_text(encoding="utf-8")
531
531
  assert "`mt5cli.STABLE_SDK_EXPORTS`" in contract_doc
532
532
  assert set(mt5cli.__all__) - {"STABLE_SDK_EXPORTS"} == set(STABLE_SDK_EXPORTS)
@@ -548,7 +548,7 @@ class TestStableSdkContract:
548
548
  ],
549
549
  )
550
550
  def test_pdmt5_pass_through_names_stay_out_of_public_contract(name: str) -> None:
551
- """Downstream code must import pdmt5 types/constants directly from pdmt5.
551
+ """Low-level pdmt5 types/constants stay out of the stable package root.
552
552
 
553
553
  docs/api/public-contract.md documents that mt5cli is not a pass-through
554
554
  compatibility namespace for pdmt5; these names must never reappear on the
@@ -11,7 +11,7 @@ import pytest
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Iterator
13
13
 
14
- _EXAMPLES_DIR = Path(__file__).parent.parent / "examples" / "grafana"
14
+ _EXAMPLES_DIR = Path(__file__).parents[2] / "examples" / "grafana"
15
15
  _DASHBOARDS_DIR = _EXAMPLES_DIR / "dashboards"
16
16
 
17
17