mt5cli 1.3.3__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.3.3 → mt5cli-1.4.1}/.agents/skills/mt5cli/SKILL.md +2 -2
  3. mt5cli-1.4.1/.github/CODEOWNERS +2 -0
  4. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/workflows/ci.yml +11 -14
  5. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/workflows/claude.yml +2 -2
  6. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/workflows/release.yml +2 -2
  7. mt5cli-1.4.1/AGENTS.md +29 -0
  8. {mt5cli-1.3.3 → mt5cli-1.4.1}/PKG-INFO +2 -3
  9. {mt5cli-1.3.3 → mt5cli-1.4.1}/README.md +0 -1
  10. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/public-contract.md +23 -15
  11. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/trading.md +54 -8
  12. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/index.md +0 -1
  13. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/__init__.py +4 -2
  14. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/contract.py +5 -1
  15. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/trading.py +315 -81
  16. {mt5cli-1.3.3 → mt5cli-1.4.1}/pyproject.toml +3 -3
  17. mt5cli-1.4.1/tests/cli/test_cli.bats +122 -0
  18. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_contracts.py +2 -2
  19. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_examples.py +1 -1
  20. mt5cli-1.4.1/tests/unit/test_stable_sdk_cleanup.py +31 -0
  21. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_trading.py +913 -86
  22. {mt5cli-1.3.3 → mt5cli-1.4.1}/uv.lock +220 -14
  23. mt5cli-1.3.3/.agents/skills/local-qa/scripts/qa.sh +0 -26
  24. mt5cli-1.3.3/.agents/skills/pr-feedback-triage/SKILL.md +0 -211
  25. mt5cli-1.3.3/AGENTS.md +0 -37
  26. {mt5cli-1.3.3 → mt5cli-1.4.1}/.agents/skills/local-qa/SKILL.md +0 -0
  27. {mt5cli-1.3.3 → mt5cli-1.4.1}/.claude/agents/codex.md +0 -0
  28. {mt5cli-1.3.3 → mt5cli-1.4.1}/.claude/settings.json +0 -0
  29. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/FUNDING.yml +0 -0
  30. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/dependabot.yml +0 -0
  31. {mt5cli-1.3.3 → mt5cli-1.4.1}/.github/renovate.json +0 -0
  32. {mt5cli-1.3.3 → mt5cli-1.4.1}/.gitignore +0 -0
  33. {mt5cli-1.3.3 → mt5cli-1.4.1}/CLAUDE.md +0 -0
  34. {mt5cli-1.3.3 → mt5cli-1.4.1}/LICENSE +0 -0
  35. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/cli.md +0 -0
  36. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/client.md +0 -0
  37. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/converters.md +0 -0
  38. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/exceptions.md +0 -0
  39. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/grafana.md +0 -0
  40. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/history.md +0 -0
  41. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/index.md +0 -0
  42. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/marketdata.md +0 -0
  43. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/observability.md +0 -0
  44. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/schemas.md +0 -0
  45. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/telemetry.md +0 -0
  46. {mt5cli-1.3.3 → mt5cli-1.4.1}/docs/api/utils.md +0 -0
  47. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/README.md +0 -0
  48. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/compose.yml +0 -0
  49. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  50. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  51. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  52. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  53. {mt5cli-1.3.3 → mt5cli-1.4.1}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  54. {mt5cli-1.3.3 → mt5cli-1.4.1}/mkdocs.yml +0 -0
  55. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/__main__.py +0 -0
  56. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/cli.py +0 -0
  57. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/client.py +0 -0
  58. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/converters.py +0 -0
  59. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/exceptions.py +0 -0
  60. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/grafana.py +0 -0
  61. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/history.py +0 -0
  62. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/marketdata.py +0 -0
  63. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/observability.py +0 -0
  64. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/retry.py +0 -0
  65. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/schemas.py +0 -0
  66. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/telemetry.py +0 -0
  67. {mt5cli-1.3.3 → mt5cli-1.4.1}/mt5cli/utils.py +0 -0
  68. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/__init__.py +0 -0
  69. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/conftest.py +0 -0
  70. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_cli.py +0 -0
  71. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_client.py +0 -0
  72. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_grafana.py +0 -0
  73. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_history.py +0 -0
  74. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_marketdata.py +0 -0
  75. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_observability.py +0 -0
  76. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_packaging.py +0 -0
  77. {mt5cli-1.3.3/tests → mt5cli-1.4.1/tests/unit}/test_telemetry.py +0 -0
  78. {mt5cli-1.3.3/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.3.3
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.
@@ -264,7 +264,9 @@ A normalized snapshot keeps the raw/UTC distinction explicit:
264
264
 
265
265
  `copy_ticks_range()` is not used for calibration: its documented UTC contract
266
266
  is broker/terminal-dependent and not independently verified here (see
267
- above), so it cannot serve as an out-of-band UTC reference. Instead, the only
267
+ above), so neither the query timestamps nor the returned historical tick
268
+ timestamps can serve as an out-of-band UTC reference, and historical copied
269
+ rows are never used to infer the server offset. Instead, the only
268
270
  independent reference available is **this process's own clock**: every
269
271
  calibration poll
270
272
  fetches one live `symbol_info_tick()` value and stamps it with
@@ -303,7 +305,11 @@ to a plausible bucket (`unstable_offset`), an unrealistic rounded delta
303
305
  missing live data (`no_live_tick`) — snapshots fail closed: `clock_status` is
304
306
  `"uncalibrated"`, `time_utc` and `server_clock_offset_seconds` are `None`,
305
307
  and the raw fields remain available. A raw timestamp is never silently
306
- treated as UTC. `normalizer.calibration` exposes the full
308
+ treated as UTC. The same fail-closed rule applies after a successful
309
+ calibration: once periodic revalidation invalidates the cached offset (see
310
+ below), subsequent snapshots are normalized only by a successful full
311
+ recalibration — never by the refuted cache. `normalizer.calibration`
312
+ exposes the full
307
313
  `TickClockCalibration` diagnostics: status, offset, distinct sample count,
308
314
  evidence symbols, last calibration time, and (for troubleshooting a failed
309
315
  attempt) the most recently considered sample's symbol, raw server-labeled
@@ -323,15 +329,55 @@ symbols and calls. A cached offset is recomputed in three cases:
323
329
  future — evidence that the broker offset grew, e.g. a UTC+2 to UTC+3
324
330
  transition.
325
331
  - Periodically, at most once per `revalidation_interval_seconds` (default 5
326
- minutes), each configured symbol is polled once and compared against that
327
- symbol's last known observation (from the initial calibration or a prior
328
- revalidation) until one confirms the still-cached offset. Closed or
329
- inconclusive symbols are skipped; a detected disagreement forces full
330
- recalibration. This is what catches a broker offset _decrease_ (e.g. UTC+3
331
- to UTC+2): the resulting normalized time looks stale rather than future,
332
+ minutes), through one full revalidation round over every configured
333
+ symbol. This is what catches a broker offset _decrease_ (e.g. UTC+3 to
334
+ UTC+2): the resulting normalized time looks stale rather than future,
332
335
  which a future-skew check alone cannot distinguish from ordinary
333
336
  quiet-market staleness.
334
337
 
338
+ In a revalidation round each configured symbol is polled once and compared
339
+ against that symbol's last known observation (from the initial calibration
340
+ or a prior revalidation), and the whole round is evaluated before any
341
+ decision is made — revalidation never stops at the first confirming symbol.
342
+ A symbol is inconclusive — skipped, neither confirming nor refuting the
343
+ cache — when its fetch is unusable, when it has no prior observation to
344
+ compare against, or when its tick epoch is unchanged since that observation
345
+ (ordinary tick age, e.g. a closed or illiquid symbol). Every other advancing
346
+ sample is evidence the cache must be able to explain:
347
+
348
+ - an accepted offset equal to the cached one confirms the cache, but never
349
+ cancels another symbol's refuting evidence from the same round;
350
+ - an accepted offset that differs from the cached one refutes the cache (a
351
+ clean broker offset change) — unless the cached offset, widened by the
352
+ elapsed time since that symbol's prior observation plus that prior
353
+ observation's own inferred delay under the cached offset, still explains
354
+ the disagreeing raw offset. That case is forgiven once per symbol and held as
355
+ a pending disagreement instead of refuting immediately, since it cannot
356
+ yet be told apart from a maximally delayed tick under the cached offset.
357
+ A pending disagreement escalates to a refutation only if that same
358
+ symbol's next conclusive round rounds to the identical bucket again; any
359
+ inconclusive, differently-bucketed, or unusable round in between clears
360
+ it instead of letting unrelated samples accumulate into false
361
+ corroboration;
362
+ - an advancing sample with no usable offset (`unstable_offset` or
363
+ `implausible_offset`) also refutes the cache, subject to the same
364
+ once-per-symbol forgiveness above — but an unusable sample can never
365
+ itself be held as, or match, a pending disagreement, since it carries no
366
+ specific offset to corroborate. A fresh market event whose raw offset is
367
+ neither reconciled with any plausible 30-minute bucket nor forgiven this
368
+ way is contradictory evidence the cached calibration cannot explain.
369
+
370
+ Any refuting evidence invalidates the cached calibration and forces full
371
+ recalibration; when several symbols refute the cache in one round, the
372
+ reported evidence is deterministic (`implausible_offset` first, then
373
+ `unstable_offset`, then a clean disagreement). The recalibration either
374
+ re-establishes a validated offset or fails closed — a raw tick is never
375
+ normalized with a cached offset that advancing evidence has just refuted. A
376
+ wholly inconclusive round keeps the cache, and a round in which no symbol
377
+ had a comparable baseline does not consume the interval, so the next call
378
+ retries immediately instead of waiting a full
379
+ `revalidation_interval_seconds`.
380
+
335
381
  If recalibration still cannot validate the timestamp, the snapshot fails
336
382
  closed. Failed calibrations are recorded for diagnostics but never reused;
337
383
  retries are bounded to at most once per `failed_calibration_retry_seconds`
@@ -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",