Compare commits

..

77 Commits

Author SHA1 Message Date
dceoy a95ade1800 deploy: f87683a431 2026-07-04 05:22:54 +00:00
dceoy f11eded3c1 deploy: 74e3754a80 2026-07-04 05:20:25 +00:00
dceoy c3b527fe5e deploy: c2cf0656dd 2026-07-03 16:31:01 +00:00
dceoy 29a3ddd8b7 deploy: efc0de230a 2026-07-03 16:04:05 +00:00
dceoy ef7e9ef5d2 deploy: a5c2aa8e8f 2026-07-02 18:55:03 +00:00
dceoy 569abbb5d0 deploy: a05b6b896d 2026-07-01 11:56:35 +00:00
dceoy e33c0a9e63 deploy: e2522f111b 2026-06-30 18:18:11 +00:00
dceoy ed76a412a1 deploy: 513eb7617d 2026-06-29 20:29:14 +00:00
dceoy d77cf5211b deploy: 1ffac45d57 2026-06-28 08:14:55 +00:00
dceoy 2caed332ff deploy: d27da02f3f 2026-06-27 23:21:20 +00:00
dceoy 2614ef8f2c deploy: 36b2e22086 2026-06-27 23:18:37 +00:00
dceoy f27febb6f5 deploy: d27da02f3f 2026-06-27 22:56:08 +00:00
dceoy bba1008be8 deploy: 4bc36d09d2 2026-06-27 22:19:32 +00:00
dceoy 7dc097cdf6 deploy: 3a126ced30 2026-06-27 22:06:51 +00:00
dceoy 938b7eda5e deploy: 831864d188 2026-06-27 22:05:16 +00:00
dceoy d3dabad610 deploy: 3a126ced30 2026-06-27 21:14:33 +00:00
dceoy b2354b0429 deploy: b7621e6954fe87f946e5d5d53027cabbed224895 2026-06-27 21:09:37 +00:00
dceoy d3b21df369 deploy: 80c3f3f65e 2026-06-27 21:01:21 +00:00
dceoy 40ab7ee96f deploy: 43f632bc40 2026-06-27 16:23:59 +00:00
dceoy f909f5d05f deploy: 8028263b24 2026-06-26 17:07:04 +00:00
dceoy cc016044ca deploy: 93565681e1 2026-06-26 13:28:39 +00:00
dceoy a228b52245 deploy: f435544f07 2026-06-26 09:24:23 +00:00
dceoy da5afc496b deploy: 668f38d8aa 2026-06-26 03:08:57 +00:00
dceoy 3f804a3064 deploy: 8da5ee9242 2026-06-25 05:12:21 +00:00
dceoy 62063bbf22 deploy: 9dbb46fbb1 2026-06-25 05:04:06 +00:00
dceoy 2ec68691e8 deploy: dfe80ce500 2026-06-25 01:40:44 +00:00
dceoy 6e5c775f68 deploy: 15bfd17db3 2026-06-25 01:08:02 +00:00
dceoy e171fe2864 deploy: 5100cb1a48 2026-06-24 17:09:59 +00:00
dceoy 39a883c279 deploy: 71b99ada26 2026-06-24 17:08:58 +00:00
dceoy e034d43720 deploy: 15bfd17db3 2026-06-24 16:55:41 +00:00
dceoy 3ee7140302 deploy: 37eef16e99 2026-06-24 16:05:06 +00:00
dceoy 8eeac933a7 deploy: 96c75f7852 2026-06-23 18:44:34 +00:00
dceoy e962a6adbf deploy: 292fac899a 2026-06-23 16:59:18 +00:00
dceoy 35ac854ab0 deploy: 9ac3b885c3 2026-06-23 12:42:30 +00:00
dceoy 74a6dbf9dc deploy: 823cb5b0a4 2026-06-23 10:28:21 +00:00
dceoy e5235a4f7a deploy: 7a17b8ca3e 2026-06-23 10:27:04 +00:00
dceoy 5bf54fa0dc deploy: 823cb5b0a4 2026-06-23 10:18:39 +00:00
dceoy 35c79d7fbb deploy: 1c57be5c44 2026-06-23 09:49:26 +00:00
dceoy ed45281274 deploy: f1ada55bce 2026-06-23 09:31:04 +00:00
dceoy 206f5fd771 deploy: d292fbb9d9 2026-06-23 07:31:36 +00:00
dceoy 57a53f9c8c deploy: 8e53212a24 2026-06-23 05:02:32 +00:00
dceoy ea951c615c deploy: b878a61c07 2026-06-23 03:32:24 +00:00
dceoy 947bc45fe3 deploy: 2adb8d9ecf 2026-06-23 03:31:44 +00:00
dceoy 788b22a85b deploy: b878a61c07 2026-06-22 19:40:55 +00:00
dceoy dbf11de061 deploy: 0610ea732c 2026-06-22 14:04:09 +00:00
dceoy 0d9226d875 deploy: 82a39731ed 2026-06-22 13:53:09 +00:00
dceoy a70021a72e deploy: c4a4253fbc 2026-06-18 16:03:06 +00:00
dceoy e7c6c0b25f deploy: 9f2968cc98 2026-06-18 15:22:47 +00:00
dceoy 8cfcb983fe deploy: 7de3ce0b7a 2026-06-18 13:58:54 +00:00
dceoy 3eb51a1a12 deploy: 897f7f0a0d 2026-06-18 10:12:55 +00:00
dceoy 6fe33bae25 deploy: d156dd7176 2026-06-14 17:47:52 +00:00
dceoy 45026a9e61 deploy: 307d6f5320 2026-06-14 14:09:17 +00:00
dceoy 03b8599ae3 deploy: 8031389a67 2026-06-14 13:55:41 +00:00
dceoy d4787abe3d deploy: 254c159ad5 2026-06-12 16:34:51 +00:00
dceoy 4eb2fcd07e deploy: 78c49238cf 2026-06-12 16:32:35 +00:00
dceoy e9180ad565 deploy: 9356d5dcdf 2026-06-12 14:15:10 +00:00
dceoy 42f16af1aa deploy: 0fad55d609 2026-06-11 14:23:26 +00:00
dceoy 019c16a261 deploy: d654b82f9d 2026-06-11 10:38:38 +00:00
dceoy b0be48c9ba deploy: b5e82e71c7 2026-06-11 10:33:27 +00:00
dceoy 2722447ae6 deploy: 18df96872b 2026-06-10 17:31:32 +00:00
dceoy 8994954964 deploy: 5b1d54bfe9 2026-06-09 15:15:52 +00:00
dceoy 83d6643f1e deploy: ad9e513253 2026-06-09 14:28:29 +00:00
dceoy 55940af10b deploy: 334f01b647 2026-06-09 06:52:57 +00:00
dceoy f10e14d775 deploy: 1b69e8f08e 2026-06-09 06:37:58 +00:00
dceoy d68a2ac7c6 deploy: 9957b0a1de 2026-06-09 02:27:54 +00:00
dceoy b2e216edd1 deploy: b2bb2ad0a0 2026-06-08 18:29:32 +00:00
dceoy fad800632a deploy: 756faf747b 2026-06-08 17:40:53 +00:00
dceoy 1de77405de deploy: c4232bf44d 2026-06-08 16:28:50 +00:00
dceoy 19df5ea4dc deploy: 5b44318d55 2026-06-08 13:55:22 +00:00
dceoy 12d776c8ff deploy: 7f70073301 2026-06-07 14:48:09 +00:00
dceoy a58f209556 deploy: da74c11087 2026-05-28 16:23:36 +00:00
dceoy 9e808ad915 deploy: c45efb953c 2026-05-24 17:12:47 +00:00
dceoy cc4895b72a deploy: c1eea3fa3d 2026-05-24 16:50:44 +00:00
dceoy b6cab3da7c deploy: c1eea3fa3d 2026-05-24 16:49:15 +00:00
dceoy a67a2c732e deploy: b8ce76c9d6 2026-04-26 12:50:38 +00:00
dceoy c1bfabacb2 deploy: 5750625e14 2026-04-25 21:22:19 +00:00
dceoy 626bdad318 deploy: 45ea0b459b 2026-04-22 17:11:38 +00:00
86 changed files with 42158 additions and 15413 deletions
-22
View File
@@ -1,22 +0,0 @@
---
name: local-qa
description: Run local QA including formatting, linting, and testing for the repository. Use whenever any file has been updated.
disable-model-invocation: false
---
# Local QA (format, lint, and test)
Run the local QA script `scripts/qa.sh` in this skill.
## Procedure
- Execute the script exactly as shown above when this skill is triggered.
- Capture and summarize key output (success/failure, major warnings, and any files modified).
- If the script fails due to missing tooling (`command not found`, missing executable, or equivalent), install the missing tool(s) and rerun `./scripts/qa.sh`.
- Install tools using this order of preference:
1. Use the project's package manager when applicable (`uv`/`poetry` for Python, package manager scripts/dependencies for Node.js).
2. Use a system package manager (`brew` on macOS, `apt` on Debian/Ubuntu) when project-local install is not applicable.
3. Use language-specific installers as fallback (`pipx`/`pip`, `npm`, `go install`, etc.).
- If multiple tools are missing, repeat install -> rerun until QA completes or you hit a blocker.
- If installation fails or requires unavailable privileges, report what was attempted, the exact failure, and stop.
- Do not run unrelated commands; only run commands needed for QA and missing-tool installation.
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euox pipefail
cd "$(git rev-parse --show-toplevel)"
# Python
uv run ruff format .
uv run ruff check --fix .
uv run pyright .
uv run pytest
# Markdown
npx -y prettier --write './**/*.md'
# GitHub Actions
case "${OSTYPE}" in
darwin* | linux* )
zizmor --fix=safe .github/workflows
git ls-files -z -- '.github/workflows/*.yml' | xargs -0 -t actionlint
git ls-files -z -- '.github/workflows/*.yml' | xargs -0 -t yamllint -d '{"extends": "relaxed", "rules": {"line-length": "disable"}}'
checkov --framework=all --output=github_failed_only --directory=.
;;
* )
echo "GitHub Actions linting is only supported on Linux and macOS."
;;
esac
-1
View File
@@ -1 +0,0 @@
../../skills/mt5cli
-46
View File
@@ -1,46 +0,0 @@
# Codex Agent
Specialized Claude agent for autonomous development work using OpenAI's Codex CLI.
## Modes
### Ask Mode
Read-only code analysis: answer questions about implementation, architecture, and debugging with specific file references and code examples.
### Exec Mode
Generate and modify code: create new components, refactor existing code, fix bugs, and write tests while maintaining quality standards.
### Review Mode
Comprehensive code review: identify security vulnerabilities, bugs, performance issues, and quality improvements without making changes.
### Search Mode
Research current documentation, best practices, solutions, and technology comparisons using web resources.
## Core Requirements
- Prioritize Codex CLI as the primary execution engine.
- Ask, Review, and Search modes are read-only; only Exec mode modifies code.
- All answers require verification:
- Ask mode must confirm file paths exist.
- Exec mode requires test passage and linting.
- Review mode needs severity prioritization.
- Search mode demands sourced citations.
## Workflow
1. Understand the task.
2. Gather context from the codebase.
3. Execute via Codex CLI with specific parameters.
4. Verify results.
5. Communicate findings with appropriate structure and detail.
## Constraints
- No hardcoded secrets.
- Thorough testing.
- Specific file references.
- Honest communication about limitations.
-18
View File
@@ -1,18 +0,0 @@
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": ".claude/skills/local-qa/scripts/qa.sh"
}
]
}
]
},
"enabledPlugins": {
"code-simplifier@claude-plugins-official": true
}
}
-1
View File
@@ -1 +0,0 @@
../.agents/skills
-3
View File
@@ -1,3 +0,0 @@
---
github:
- dceoy
-17
View File
@@ -1,17 +0,0 @@
---
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
cooldown:
default-days: 7
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: /
schedule:
interval: daily
cooldown:
default-days: 7
open-pull-requests-limit: 10
-13
View File
@@ -1,13 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"minimumReleaseAge": "7 days",
"packageRules": [
{
"description": "Automatically merge minor and patch-level updates",
"matchUpdateTypes": ["minor", "patch", "digest"],
"automerge": true,
"automergeType": "branch"
}
]
}
-77
View File
@@ -1,77 +0,0 @@
---
name: CI/CD
on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
workflow_dispatch: # checkov:skip=CKV_GHA_7:workflow_dispatch inputs are required for manual runs
inputs:
workflow:
required: true
type: choice
options:
- lint-and-test
- docs-deploy
description: Choose the workflow to run
default: lint-and-test
permissions:
contents: read
defaults:
run:
shell: bash -euo pipefail {0}
working-directory: .
jobs:
python-lint-and-scan:
if: >
github.event_name == 'push'
|| github.event_name == 'pull_request'
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
permissions:
contents: read
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-lint-and-scan.yml@main # zizmor: ignore[unpinned-uses]
with:
package-path: .
runs-on: windows-latest
python-test:
if: >
github.event_name == 'push'
|| github.event_name == 'pull_request'
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
permissions:
contents: read
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-test.yml@main # zizmor: ignore[unpinned-uses]
with:
package-path: .
runs-on: windows-latest
python-docs-deploy:
if: >
github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'docs-deploy')
permissions:
contents: write
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-mkdocs-gh-deploy.yml@main # zizmor: ignore[unpinned-uses]
with:
package-path: .
runs-on: ubuntu-slim
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dependabot-auto-merge:
if: >
github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
needs:
- python-lint-and-scan
- python-test
uses: dceoy/gh-actions-for-devops/.github/workflows/dependabot-auto-merge.yml@main # zizmor: ignore[unpinned-uses]
permissions:
contents: write
pull-requests: write
actions: read
with:
unconditional: true
-57
View File
@@ -1,57 +0,0 @@
---
name: Claude Code review and mention bot
on:
pull_request:
branches:
- main
types:
- opened
- ready_for_review
issue_comment:
types:
- created
pull_request_review_comment:
types:
- created
issues:
types:
- opened
- assigned
pull_request_review:
types:
- submitted
permissions:
contents: read
jobs:
claude-code-review:
if: >
github.event_name == 'pull_request'
&& (! github.event.pull_request.draft)
&& (! startsWith(github.head_ref, 'dependabot/'))
&& (! startsWith(github.head_ref, 'renovate/'))
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
uses: dceoy/gh-actions-for-devops/.github/workflows/claude-code-review.yml@main # zizmor: ignore[unpinned-uses]
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
claude-code-bot:
if: >
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude'))
|| (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|| (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude'))
|| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
uses: dceoy/gh-actions-for-devops/.github/workflows/claude-code-bot.yml@main # zizmor: ignore[unpinned-uses]
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env]
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-44
View File
@@ -1,44 +0,0 @@
---
name: Release
on:
workflow_dispatch:
permissions:
contents: read
defaults:
run:
shell: bash -euo pipefail {0}
working-directory: .
jobs:
build-and-release:
permissions:
contents: write
id-token: write
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-release-on-pypi-and-github.yml@main # zizmor: ignore[unpinned-uses]
with:
package-path: .
create-github-release: true
publish-to-pypi: false
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-to-pypi:
name: Publish the Python 🐍 distribution 📦 to PyPI
if: >
startsWith(github.ref, 'refs/tags/')
needs:
- build-and-release
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/${{ needs.build-and-release.outputs.project-name }}
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.build-and-release.outputs.distribution-artifact-name }}
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
verbose: true
-207
View File
@@ -1,207 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
View File
+202
View File
@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="dceoy">
<link rel="shortcut icon" href="/dceoy/mt5cli/img/favicon.ico">
<title>mt5cli API Documentation</title>
<link href="/dceoy/mt5cli/css/bootstrap.min.css" rel="stylesheet">
<link href="/dceoy/mt5cli/css/fontawesome.min.css" rel="stylesheet">
<link href="/dceoy/mt5cli/css/brands.min.css" rel="stylesheet">
<link href="/dceoy/mt5cli/css/solid.min.css" rel="stylesheet">
<link href="/dceoy/mt5cli/css/v4-font-face.min.css" rel="stylesheet">
<link href="/dceoy/mt5cli/css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="/dceoy/mt5cli/assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/dceoy/mt5cli/.">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href="/dceoy/mt5cli/." class="nav-link">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="/dceoy/mt5cli/api/" class="dropdown-item">Overview</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/public-contract/" class="dropdown-item">Public API Contract</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/exceptions/" class="dropdown-item">Exceptions</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/telemetry/" class="dropdown-item">Telemetry</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="/dceoy/mt5cli/api/utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli" class="nav-link">dceoy/mt5cli</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="row-fluid">
<div id="main-content" class="span12">
<h1 id="404-page-not-found" style="text-align: center">404</h1>
<p style="text-align: center"><strong>Page not found</strong></p>
</div>
</div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="/dceoy/mt5cli/js/bootstrap.bundle.min.js"></script>
<script>
var base_url = "/dceoy/mt5cli/",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="/dceoy/mt5cli/js/base.js"></script>
<script src="/dceoy/mt5cli/search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
-81
View File
@@ -1,81 +0,0 @@
# Repository Guidelines
## Commands
### Development Setup
```bash
uv sync
```
### Code Quality and Documentation
**Important**: Run these before committing or creating a PR.
1. **format, lint, and test**: Use `local-qa` skill.
2. **Documentation build** (if any public API changes): `uv run mkdocs build`
## Architecture
### Key Dependencies
- **pdmt5**: Pandas-based data handler for MetaTrader 5 (core library)
- **typer**: CLI framework for building command-line interfaces
- **click**: Parameter type customization for CLI options
- **pandas**: Core data manipulation and analysis
### Package Structure
- `mt5cli/`: Main package directory
- `__init__.py`: Package initialization and exports (`detect_format`, `export_dataframe`)
- `cli.py`: CLI application with typer-based commands for data export
- `utils.py`: Constants, enums, parameter types, parsers, and export utilities
- `__main__.py`: Entry point for `python -m mt5cli`
- `tests/`: Comprehensive test suite (pytest-based)
- `test_cli.py`: Tests for CLI commands and collect-history behavior
- `test_utils.py`: Tests for utility constants, parameter types, parsers, and export functions
- `docs/`: MkDocs documentation with API reference
- `docs/index.md`: Main documentation
- `docs/api/`: Auto-generated API documentation for all modules
- Modern Python packaging with `pyproject.toml` and uv dependency management
### Quality Standards
- Type hints required (pyright strict mode)
- Comprehensive linting with 35+ rule categories (ruff)
- Test coverage tracking with 100% (pytest-cov)
- Parametrized tests for input/result matrices using `pytest.mark.parametrize` (pytest)
- Test doubles (mocks, stubs) using `pytest_mock` for external dependencies (pytest-mock)
- Pydantic models for data validation and configuration
### Documentation workflow
1. Add Google-style docstrings to functions/classes
2. Local preview: `uv run mkdocs serve`
3. Build: `uv run mkdocs build`
4. Deploy: `uv run mkdocs gh-deploy`
## Commit & Pull Request Guidelines
- Run QA checks using `local-qa` skill before committing or creating a PR.
- Branch names use appropriate prefixes on creation (e.g., `feature/...`, `bugfix/...`, `refactor/...`, `docs/...`, `chore/...`).
- When instructed to create a PR, create it as a draft with appropriate labels by default.
## Code Design Principles
Always prefer the simplest design that works.
- **KISS**: Choose straightforward solutions and avoid unnecessary abstraction.
- **DRY**: Remove duplication when it improves clarity and maintainability.
- **YAGNI**: Do not add features, hooks, or flexibility until they are needed.
- **SOLID/Clean Code**: Apply these as tools, only when they keep the design simpler and easier to change.
## Development Methodology
Keep delivery incremental, test-backed, and easy to review.
- Make small, safe, reversible changes.
- Prefer `Red -> Green -> Refactor`.
- Do not mix feature work and refactoring in the same commit.
- Refactor when it improves clarity or removes real duplication (Rule of Three).
- Keep tests fast, focused, and self-validating.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Daichi Narushima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-241
View File
@@ -1,241 +0,0 @@
# mt5cli
[![CI/CD](https://github.com/dceoy/mt5cli/actions/workflows/ci.yml/badge.svg)](https://github.com/dceoy/mt5cli/actions/workflows/ci.yml)
Command-line tool for exporting MetaTrader 5 data to CSV, JSON, Parquet, and SQLite3.
Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
## Architecture
- **pdmt5** — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (`TIMEFRAME_*`, `COPY_TICKS_*`, order types).
- **mt5cli** — CLI commands, CSV/JSON/Parquet/SQLite export, SQLite history collection, rate views, and local batch/automation SDK helpers built on pdmt5.
- **mt5api** — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.
## Features
- **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
- **Auto-detection**: Format detection from file extensions
- **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
- **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
- **Connection management**: Optional credentials, server, and timeout configuration
- **SQLite rate loading**: Load mt5cli-managed rate tables/views for offline workflows
## Installation
```bash
pip install -U mt5cli MetaTrader5
```
## Usage
```bash
# Export account information to CSV
mt5cli -o account.csv account-info
# Export EURUSD M1 rates to Parquet
mt5cli -o rates.parquet rates-from --symbol EURUSD --timeframe M1 \
--date-from 2024-01-01 --count 1000
# Export ticks to JSON
mt5cli -o ticks.json ticks-from --symbol EURUSD \
--date-from 2024-01-01 --count 500 --flags ALL
# Export symbols to SQLite3 with custom table name
mt5cli -o data.db --table symbols symbols --group "*USD*"
# Export with connection credentials
mt5cli --login 12345 --password mypass --server MyBroker-Demo \
-o positions.csv positions
```
Run as a Python module:
```bash
python -m mt5cli -o account.csv account-info
```
## Commands
| Command | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `rates-from` | Export rates from a start date |
| `rates-from-pos` | Export rates from a start position |
| `latest-rates` | Export latest rates from a start position |
| `rates-range` | Export rates for a date range |
| `ticks-from` | Export ticks from a start date |
| `ticks-range` | Export ticks for a date range |
| `ticks-recent` | Export ticks from a recent trailing window |
| `account-info` | Export account information |
| `terminal-info` | Export terminal information |
| `version` | Export MetaTrader 5 version information |
| `last-error` | Export the last error information |
| `symbols` | Export symbol list |
| `symbol-info` | Export symbol details |
| `symbol-info-tick` | Export the last tick for a symbol |
| `minimum-margins` | Export minimum-volume buy and sell margin requirements |
| `market-book` | Export market depth (order book) |
| `orders` | Export active orders |
| `positions` | Export open positions |
| `history-orders` | Export historical orders |
| `history-deals` | Export historical deals |
| `recent-history-deals` | Export historical deals from a recent trailing window |
| `mt5-summary` | Export terminal/account status summary |
| `order-check` | Check funds sufficiency for a trade request |
| `order-send` | Send a trade request to the trade server (`--yes` required) |
| `collect-history` | Bundle rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database |
Use `order-check` to validate a request payload before running `order-send --yes`.
### `collect-history`
Collect several historical datasets per symbol into one SQLite database in a single MT5 session. Pick datasets with repeatable `--dataset` (default: all four), choose conflict behavior with `--if-exists append|replace|fail` (default: `fail`), and optionally derive `cash_events` / `positions_reconstructed` views from `history_deals` via `--with-views`.
```bash
mt5cli -o history.db collect-history \
--symbol EURUSD --symbol GBPUSD \
--date-from 2024-01-01 --date-to 2024-02-01 \
--dataset rates --dataset history-deals \
--timeframe M1 --flags ALL --if-exists append --with-views
```
History orders and deals are fetched per symbol and concatenated, so the symbol filter is applied consistently across all datasets. The `cash_events` view is derived from symbol-filtered `history_deals`, so account-level cash events with empty or non-matching symbols may be excluded. The `rates` table records the requested `timeframe` so appended runs at different timeframes remain distinguishable. The `positions_reconstructed` view aggregates trade deals by `position_id`, excludes positions without closing-side entries, and uses volume-weighted open/close prices; reversal deals (`DEAL_ENTRY_INOUT`) are reported via `volume_reversal` / `reversal_count` columns.
### Incremental history SDK
For automated pipelines, use the importable incremental API instead of re-fetching fixed date ranges:
```python
from pdmt5 import Mt5Config, Mt5DataClient
from mt5cli import Dataset, update_history, update_history_with_config
# Reuse an already-connected pdmt5 client (does not open/close MT5)
client = Mt5DataClient(config=Mt5Config(login=12345))
client.initialize_and_login_mt5()
try:
update_history(
client=client,
output="history.db",
symbols=["EURUSD", "GBPUSD"],
datasets={Dataset.rates, Dataset.history_deals},
timeframes=["M1", "H1"], # default: all fixed MT5 timeframes
lookback_hours=24,
create_rate_views=True,
with_views=True,
include_account_events=True,
)
finally:
client.shutdown()
# Standalone wrapper that opens and closes MT5 for you
update_history_with_config(
output="history.db",
symbols=["EURUSD"],
config=Mt5Config(login=12345),
)
```
- **`collect-history`**: explicit date-range export into SQLite.
- **`update_history`**: incremental append based on existing SQLite `MAX(time)` per symbol (and timeframe for rates); account-level deals use a separate cursor when `include_account_events=True`.
- **`rates` table**: normalized storage with `symbol` and `timeframe` columns.
- **Rate compatibility views**: mt5cli manages all `rate_*` views. Naming is `rate_<symbol>__<timeframe>` when a symbol has one timeframe, otherwise `rate_<symbol>__<granularity>_<timeframe>` (for example `rate_EURUSD__M1_1`). Stale `rate_*` views are dropped and recreated when rates change for offline tools such as mteor optimize.
- **Rate view resolution**: use `resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases. Both accept `None` (or a missing path) and return deterministic default names unless `require_existing=True`.
- **Rate view loading**: use `load_rate_data()` / `load_rate_data_from_connection()` to load a SQLite rate table or view into a `DatetimeIndex` DataFrame.
- **Multi-series rate loading**: use `build_rate_targets()` to build neutral `RateTarget(symbol, timeframe)` pairs, `resolve_rate_tables()` to map them to table/view names (pass `require_existing=True` for strict resolution), and `load_rate_series_from_sqlite()` to load them into a mapping keyed by `(symbol, integer timeframe)`. The loader requires existing managed views unless `explicit_tables` is supplied, and rejects duplicate `(symbol, timeframe)` targets.
- **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping. For long-running pollers, `collect_latest_rates_for_accounts_with_retries()` adds bounded exponential backoff that retries only `pdmt5.Mt5TradingError` / `pdmt5.Mt5RuntimeError` and re-raises once `retry_count` is exhausted.
- **Latest closed bars**: use `collect_latest_closed_rates_for_accounts()` when downstream logic must exclude the still-forming current bar. It fetches `count + 1` bars at `start_pos=0`, drops the last row with `drop_forming_rate_bar()`, and validates each series is non-empty. `collect_latest_closed_rates_by_granularity()` returns the same data keyed by `(symbol, granularity_name)` such as `("EURUSD", "M1")`.
```python
from mt5cli import AccountSpec, collect_latest_closed_rates_by_granularity
rates = collect_latest_closed_rates_by_granularity(
[AccountSpec(symbols=["EURUSD", "GBPUSD"], login=12345)],
["M1", "H1"],
count=500,
retry_count=3,
)
eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
```
- **Credential resolution**: use `resolve_account_spec()` / `resolve_account_specs()` to merge explicit override values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders (via `substitute_env_placeholders()`), raising `ValueError` for missing variables. This keeps secrets out of plan/config files without coupling to any strategy code.
- **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`, `sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability errors for history API methods without advancing the throttle (other `AttributeError` / `TypeError` values always propagate).
- **Trading session helpers**: use `mt5_trading_session()` for a trading-capable `pdmt5.Mt5TradingClient` that initializes/logs in via `Mt5Config.path` and always shuts down safely. Pair with `detect_position_side()`, `calculate_margin_and_volume()`, and `determine_order_limits()` for generic position and sizing utilities. The read-only `mt5_session()` / `Mt5CliClient` SDK is unchanged.
- **Granularity-keyed rate loading**: `load_rate_series_by_granularity()` builds targets with `build_rate_targets()`, loads them with `load_rate_series_from_sqlite()`, and returns a mapping keyed by `(symbol | None, granularity_name)` such as `("EURUSD", "M1")` to reduce downstream boilerplate.
- **MT5 session helper**: use the `mt5_session()` context manager to attach to (or, when `Mt5Config.path` is set, launch) an MT5 terminal, log in, and yield a connected `Mt5CliClient` that shuts down on exit.
- **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
- **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
## Requirements
- Python 3.11+
- Windows OS (MetaTrader 5 requirement)
- MetaTrader 5 platform installed
### Migration note for mteor
Replace local MT5 lifecycle and trading helper code with mt5cli imports:
```python
# Before (local mteor helpers)
# with local_mt5_trading_session(config) as client:
# side = local_detect_position_side(client, symbol)
# sizing = local_calculate_margin_and_volume(client, symbol, unit_ratio, preserved_ratio)
# limits = local_determine_order_limits(client, symbol, side, sl_ratio, tp_ratio)
# After (mt5cli shared layer)
from pdmt5 import Mt5Config
from mt5cli import (
calculate_margin_and_volume,
detect_position_side,
determine_order_limits,
mt5_trading_session,
)
with mt5_trading_session(
Mt5Config(path=terminal_path, login=login), retry_count=2
) as client:
side = detect_position_side(client, symbol)
sizing = calculate_margin_and_volume(
client, symbol, unit_margin_ratio=0.5, preserved_margin_ratio=0.2
)
if side is not None:
limits = determine_order_limits(
client,
symbol,
side,
stop_loss_limit_ratio=0.01,
take_profit_limit_ratio=0.02,
)
```
Throttled history updates use a separate read-only session:
```python
from pdmt5 import Mt5Config, Mt5DataClient
from mt5cli import ThrottledHistoryUpdater
updater = ThrottledHistoryUpdater(
output="history.db", interval_seconds=60, suppress_errors=True
)
client = Mt5DataClient(config=Mt5Config(login=login))
client.initialize_and_login_mt5()
try:
updater.update(client, ["EURUSD"])
finally:
client.shutdown()
```
Read-only collectors can keep using `mt5_session()` and `Mt5CliClient` without changes.
## Development
```bash
git clone https://github.com/dceoy/mt5cli.git
cd mt5cli
uv sync
```
## License
[MIT](LICENSE)
+3240
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+787
View File
@@ -0,0 +1,787 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="dceoy">
<link rel="canonical" href="https://github.com/dceoy/mt5cli/api/exceptions/">
<link rel="shortcut icon" href="../../img/favicon.ico">
<title>Exceptions - mt5cli API Documentation</title>
<link href="../../css/bootstrap.min.css" rel="stylesheet">
<link href="../../css/fontawesome.min.css" rel="stylesheet">
<link href="../../css/brands.min.css" rel="stylesheet">
<link href="../../css/solid.min.css" rel="stylesheet">
<link href="../../css/v4-font-face.min.css" rel="stylesheet">
<link href="../../css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="../../assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="../..">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href="../.." class="nav-link">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle active" aria-current="page" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="../" class="dropdown-item">Overview</a>
</li>
<li>
<a href="../public-contract/" class="dropdown-item">Public API Contract</a>
</li>
<li>
<a href="../client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="../schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="../converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="./" class="dropdown-item active" aria-current="page">Exceptions</a>
</li>
<li>
<a href="../cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="../sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="../trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="../history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="../telemetry/" class="dropdown-item">Telemetry</a>
</li>
<li>
<a href="../grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="../utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a rel="prev" href="../converters/" class="nav-link">
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li class="nav-item">
<a rel="next" href="../cli/" class="nav-link">
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli/edit/master/docs/api/exceptions.md" class="nav-link">Edit on dceoy/mt5cli
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"><div class="navbar-expand-md bs-sidebar hidden-print affix" role="complementary">
<div class="navbar-header">
<button type="button" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#toc-collapse" title="Table of Contents">
<span class="fa fa-angle-down"></span>
</button>
</div>
<div id="toc-collapse" class="navbar-collapse collapse card bg-body-tertiary">
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="1"><a href="#exceptions" class="nav-link">Exceptions</a>
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="2"><a href="#mt5cli.exceptions" class="nav-link">exceptions</a>
<ul class="nav flex-column">
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div></div>
<div class="col-md-9" role="main">
<h1 id="exceptions">Exceptions<a class="headerlink" href="#exceptions" title="Permanent link">&para;</a></h1>
<div class="doc doc-object doc-module">
<h2 id="mt5cli.exceptions" class="doc doc-heading">
<span class="doc doc-object-name doc-module-name">mt5cli.exceptions</span>
<a href="#mt5cli.exceptions" class="headerlink" title="Permanent link">&para;</a></h2>
<div class="doc doc-contents first">
<p>Normalized exception types for MT5 and mt5cli operations.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-attribute">
<h3 id="mt5cli.exceptions.T" class="doc doc-heading">
<span class="doc doc-object-name doc-attribute-name">T</span>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-module-attribute"><code>module-attribute</code></small>
</span>
<a href="#mt5cli.exceptions.T" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="n">T</span> <span class="o">=</span> <span class="n"><span title="typing.TypeVar">TypeVar</span></span><span class="p">(</span><span class="s1">&#39;T&#39;</span><span class="p">)</span>
</code></pre></div>
<div class="doc doc-contents ">
</div>
</div>
<div class="doc doc-object doc-attribute">
<h3 id="mt5cli.exceptions.__all__" class="doc doc-heading">
<span class="doc doc-object-name doc-attribute-name">__all__</span>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-module-attribute"><code>module-attribute</code></small>
</span>
<a href="#mt5cli.exceptions.__all__" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a> <span class="s2">&quot;Mt5CliError&quot;</span><span class="p">,</span>
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> <span class="s2">&quot;Mt5ConnectionError&quot;</span><span class="p">,</span>
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> <span class="s2">&quot;Mt5OperationError&quot;</span><span class="p">,</span>
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a> <span class="s2">&quot;Mt5SchemaError&quot;</span><span class="p">,</span>
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a> <span class="s2">&quot;call_with_normalized_errors&quot;</span><span class="p">,</span>
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a> <span class="s2">&quot;is_recoverable_mt5_error&quot;</span><span class="p">,</span>
<a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a> <span class="s2">&quot;normalize_mt5_exception&quot;</span><span class="p">,</span>
<a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a><span class="p">]</span>
</code></pre></div>
<div class="doc doc-contents ">
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="mt5cli.exceptions.Mt5CliError" class="doc doc-heading">
<span class="doc doc-object-name doc-class-name">Mt5CliError</span>
<a href="#mt5cli.exceptions.Mt5CliError" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="Exception">Exception</span></code></p>
<p>Base exception for mt5cli public API errors.</p>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="mt5cli.exceptions.Mt5ConnectionError" class="doc doc-heading">
<span class="doc doc-object-name doc-class-name">Mt5ConnectionError</span>
<a href="#mt5cli.exceptions.Mt5ConnectionError" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></code></p>
<p>Raised when MT5 initialization, login, or shutdown fails.</p>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="mt5cli.exceptions.Mt5OperationError" class="doc doc-heading">
<span class="doc doc-object-name doc-class-name">Mt5OperationError</span>
<a href="#mt5cli.exceptions.Mt5OperationError" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></code></p>
<p>Raised when an MT5 data or trading operation fails.</p>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="mt5cli.exceptions.Mt5SchemaError" class="doc doc-heading">
<span class="doc doc-object-name doc-class-name">Mt5SchemaError</span>
<a href="#mt5cli.exceptions.Mt5SchemaError" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></code></p>
<p>Raised when a DataFrame does not match an expected dataset schema.</p>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.exceptions.call_with_normalized_errors" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">call_with_normalized_errors</span>
<a href="#mt5cli.exceptions.call_with_normalized_errors" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">call_with_normalized_errors</span><span class="p">(</span><span class="n">fn</span><span class="p">:</span> <span class="n"><span title="collections.abc.Callable">Callable</span></span><span class="p">[[],</span> <span class="n"><a class="autorefs autorefs-internal" title="T
module-attribute
(mt5cli.exceptions.T)" href="#mt5cli.exceptions.T">T</a></span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n"><a class="autorefs autorefs-internal" title="T
module-attribute
(mt5cli.exceptions.T)" href="#mt5cli.exceptions.T">T</a></span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Run <code>fn</code> and map recoverable MT5 errors to mt5cli types.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>fn</code>
</td>
<td>
<code><span title="collections.abc.Callable">Callable</span>[[], <a class="autorefs autorefs-internal" title="T
module-attribute
(mt5cli.exceptions.T)" href="#mt5cli.exceptions.T">T</a>]</code>
</td>
<td>
<div class="doc-md-description">
<p>Callable performing MT5 work.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="T
module-attribute
(mt5cli.exceptions.T)" href="#mt5cli.exceptions.T">T</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Value returned by <code>fn</code>.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/exceptions.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-72">72</a></span>
<span class="normal"><a href="#__codelineno-0-73">73</a></span>
<span class="normal"><a href="#__codelineno-0-74">74</a></span>
<span class="normal"><a href="#__codelineno-0-75">75</a></span>
<span class="normal"><a href="#__codelineno-0-76">76</a></span>
<span class="normal"><a href="#__codelineno-0-77">77</a></span>
<span class="normal"><a href="#__codelineno-0-78">78</a></span>
<span class="normal"><a href="#__codelineno-0-79">79</a></span>
<span class="normal"><a href="#__codelineno-0-80">80</a></span>
<span class="normal"><a href="#__codelineno-0-81">81</a></span>
<span class="normal"><a href="#__codelineno-0-82">82</a></span>
<span class="normal"><a href="#__codelineno-0-83">83</a></span>
<span class="normal"><a href="#__codelineno-0-84">84</a></span>
<span class="normal"><a href="#__codelineno-0-85">85</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-72" name="__codelineno-0-72"></a><span class="k">def</span><span class="w"> </span><span class="nf">call_with_normalized_errors</span><span class="p">(</span><span class="n">fn</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[],</span> <span class="n">T</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
<a id="__codelineno-0-73" name="__codelineno-0-73"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Run ``fn`` and map recoverable MT5 errors to mt5cli types.</span>
<a id="__codelineno-0-74" name="__codelineno-0-74"></a>
<a id="__codelineno-0-75" name="__codelineno-0-75"></a><span class="sd"> Args:</span>
<a id="__codelineno-0-76" name="__codelineno-0-76"></a><span class="sd"> fn: Callable performing MT5 work.</span>
<a id="__codelineno-0-77" name="__codelineno-0-77"></a>
<a id="__codelineno-0-78" name="__codelineno-0-78"></a><span class="sd"> Returns:</span>
<a id="__codelineno-0-79" name="__codelineno-0-79"></a><span class="sd"> Value returned by ``fn``.</span>
<a id="__codelineno-0-80" name="__codelineno-0-80"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-81" name="__codelineno-0-81"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-0-82" name="__codelineno-0-82"></a> <span class="k">return</span> <span class="n">fn</span><span class="p">()</span>
<a id="__codelineno-0-83" name="__codelineno-0-83"></a> <span class="k">except</span> <span class="n">_RECOVERABLE_MT5_ERRORS</span> <span class="k">as</span> <span class="n">exc</span><span class="p">:</span>
<a id="__codelineno-0-84" name="__codelineno-0-84"></a> <span class="n">normalized</span> <span class="o">=</span> <span class="n">normalize_mt5_exception</span><span class="p">(</span><span class="n">exc</span><span class="p">)</span>
<a id="__codelineno-0-85" name="__codelineno-0-85"></a> <span class="k">raise</span> <span class="n">normalized</span> <span class="kn">from</span><span class="w"> </span><span class="nn">exc</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.exceptions.is_recoverable_mt5_error" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">is_recoverable_mt5_error</span>
<a href="#mt5cli.exceptions.is_recoverable_mt5_error" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">is_recoverable_mt5_error</span><span class="p">(</span><span class="n">exc</span><span class="p">:</span> <span class="n"><span title="BaseException">BaseException</span></span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n"><span title="bool">bool</span></span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Return whether an exception is a transient MT5 failure worth retrying.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>exc</code>
</td>
<td>
<code><span title="BaseException">BaseException</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Exception raised by MT5 or pdmt5.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bool">bool</span></code>
</td>
<td>
<div class="doc-md-description">
<p>True for <code>Mt5RuntimeError</code>.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/exceptions.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-43">43</a></span>
<span class="normal"><a href="#__codelineno-0-44">44</a></span>
<span class="normal"><a href="#__codelineno-0-45">45</a></span>
<span class="normal"><a href="#__codelineno-0-46">46</a></span>
<span class="normal"><a href="#__codelineno-0-47">47</a></span>
<span class="normal"><a href="#__codelineno-0-48">48</a></span>
<span class="normal"><a href="#__codelineno-0-49">49</a></span>
<span class="normal"><a href="#__codelineno-0-50">50</a></span>
<span class="normal"><a href="#__codelineno-0-51">51</a></span>
<span class="normal"><a href="#__codelineno-0-52">52</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-43" name="__codelineno-0-43"></a><span class="k">def</span><span class="w"> </span><span class="nf">is_recoverable_mt5_error</span><span class="p">(</span><span class="n">exc</span><span class="p">:</span> <span class="ne">BaseException</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-0-44" name="__codelineno-0-44"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Return whether an exception is a transient MT5 failure worth retrying.</span>
<a id="__codelineno-0-45" name="__codelineno-0-45"></a>
<a id="__codelineno-0-46" name="__codelineno-0-46"></a><span class="sd"> Args:</span>
<a id="__codelineno-0-47" name="__codelineno-0-47"></a><span class="sd"> exc: Exception raised by MT5 or pdmt5.</span>
<a id="__codelineno-0-48" name="__codelineno-0-48"></a>
<a id="__codelineno-0-49" name="__codelineno-0-49"></a><span class="sd"> Returns:</span>
<a id="__codelineno-0-50" name="__codelineno-0-50"></a><span class="sd"> True for ``Mt5RuntimeError``.</span>
<a id="__codelineno-0-51" name="__codelineno-0-51"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-52" name="__codelineno-0-52"></a> <span class="k">return</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exc</span><span class="p">,</span> <span class="n">_RECOVERABLE_MT5_ERRORS</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.exceptions.normalize_mt5_exception" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">normalize_mt5_exception</span>
<a href="#mt5cli.exceptions.normalize_mt5_exception" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">normalize_mt5_exception</span><span class="p">(</span><span class="n">exc</span><span class="p">:</span> <span class="n"><span title="BaseException">BaseException</span></span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n"><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Map pdmt5/MT5 exceptions to stable mt5cli exception types.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>exc</code>
</td>
<td>
<code><span title="BaseException">BaseException</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Original exception from MT5 or pdmt5.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></code>
</td>
<td>
<div class="doc-md-description">
<p><code>Mt5ConnectionError</code> for runtime failures, or the original exception</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="Mt5CliError (mt5cli.exceptions.Mt5CliError)" href="#mt5cli.exceptions.Mt5CliError">Mt5CliError</a></code>
</td>
<td>
<div class="doc-md-description">
<p>when it is not recognized.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/exceptions.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-55">55</a></span>
<span class="normal"><a href="#__codelineno-0-56">56</a></span>
<span class="normal"><a href="#__codelineno-0-57">57</a></span>
<span class="normal"><a href="#__codelineno-0-58">58</a></span>
<span class="normal"><a href="#__codelineno-0-59">59</a></span>
<span class="normal"><a href="#__codelineno-0-60">60</a></span>
<span class="normal"><a href="#__codelineno-0-61">61</a></span>
<span class="normal"><a href="#__codelineno-0-62">62</a></span>
<span class="normal"><a href="#__codelineno-0-63">63</a></span>
<span class="normal"><a href="#__codelineno-0-64">64</a></span>
<span class="normal"><a href="#__codelineno-0-65">65</a></span>
<span class="normal"><a href="#__codelineno-0-66">66</a></span>
<span class="normal"><a href="#__codelineno-0-67">67</a></span>
<span class="normal"><a href="#__codelineno-0-68">68</a></span>
<span class="normal"><a href="#__codelineno-0-69">69</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-55" name="__codelineno-0-55"></a><span class="k">def</span><span class="w"> </span><span class="nf">normalize_mt5_exception</span><span class="p">(</span><span class="n">exc</span><span class="p">:</span> <span class="ne">BaseException</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Mt5CliError</span><span class="p">:</span>
<a id="__codelineno-0-56" name="__codelineno-0-56"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Map pdmt5/MT5 exceptions to stable mt5cli exception types.</span>
<a id="__codelineno-0-57" name="__codelineno-0-57"></a>
<a id="__codelineno-0-58" name="__codelineno-0-58"></a><span class="sd"> Args:</span>
<a id="__codelineno-0-59" name="__codelineno-0-59"></a><span class="sd"> exc: Original exception from MT5 or pdmt5.</span>
<a id="__codelineno-0-60" name="__codelineno-0-60"></a>
<a id="__codelineno-0-61" name="__codelineno-0-61"></a><span class="sd"> Returns:</span>
<a id="__codelineno-0-62" name="__codelineno-0-62"></a><span class="sd"> ``Mt5ConnectionError`` for runtime failures, or the original exception</span>
<a id="__codelineno-0-63" name="__codelineno-0-63"></a><span class="sd"> when it is not recognized.</span>
<a id="__codelineno-0-64" name="__codelineno-0-64"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-65" name="__codelineno-0-65"></a> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exc</span><span class="p">,</span> <span class="n">Mt5RuntimeError</span><span class="p">):</span>
<a id="__codelineno-0-66" name="__codelineno-0-66"></a> <span class="k">return</span> <span class="n">Mt5ConnectionError</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">exc</span><span class="p">))</span>
<a id="__codelineno-0-67" name="__codelineno-0-67"></a> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exc</span><span class="p">,</span> <span class="n">Mt5CliError</span><span class="p">):</span>
<a id="__codelineno-0-68" name="__codelineno-0-68"></a> <span class="k">return</span> <span class="n">exc</span>
<a id="__codelineno-0-69" name="__codelineno-0-69"></a> <span class="k">return</span> <span class="n">Mt5CliError</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">exc</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div></div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="../../js/bootstrap.bundle.min.js"></script>
<script>
var base_url = "../..",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="../../js/base.js"></script>
<script src="../../search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+326
View File
@@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="dceoy">
<link rel="canonical" href="https://github.com/dceoy/mt5cli/api/">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>Overview - mt5cli API Documentation</title>
<link href="../css/bootstrap.min.css" rel="stylesheet">
<link href="../css/fontawesome.min.css" rel="stylesheet">
<link href="../css/brands.min.css" rel="stylesheet">
<link href="../css/solid.min.css" rel="stylesheet">
<link href="../css/v4-font-face.min.css" rel="stylesheet">
<link href="../css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="../assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="..">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href=".." class="nav-link">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle active" aria-current="page" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="./" class="dropdown-item active" aria-current="page">Overview</a>
</li>
<li>
<a href="public-contract/" class="dropdown-item">Public API Contract</a>
</li>
<li>
<a href="client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="exceptions/" class="dropdown-item">Exceptions</a>
</li>
<li>
<a href="cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="telemetry/" class="dropdown-item">Telemetry</a>
</li>
<li>
<a href="grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a rel="prev" href=".." class="nav-link">
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li class="nav-item">
<a rel="next" href="public-contract/" class="nav-link">
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli/edit/master/docs/api/index.md" class="nav-link">Edit on dceoy/mt5cli
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"><div class="navbar-expand-md bs-sidebar hidden-print affix" role="complementary">
<div class="navbar-header">
<button type="button" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#toc-collapse" title="Table of Contents">
<span class="fa fa-angle-down"></span>
</button>
</div>
<div id="toc-collapse" class="navbar-collapse collapse card bg-body-tertiary">
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="1"><a href="#api-reference" class="nav-link">API Reference</a>
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="2"><a href="#public-api-layers" class="nav-link">Public API layers</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#architecture-overview" class="nav-link">Architecture overview</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#quick-start" class="nav-link">Quick start</a>
<ul class="nav flex-column">
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div></div>
<div class="col-md-9" role="main">
<h1 id="api-reference">API Reference<a class="headerlink" href="#api-reference" title="Permanent link">&para;</a></h1>
<p>This section documents the mt5cli public Python API and CLI modules.</p>
<p>Start with the <a href="public-contract/">Public API Contract</a> for the stable
downstream SDK surface, CLI boundary, internal modules, and out-of-scope strategy
responsibilities.</p>
<h2 id="public-api-layers">Public API layers<a class="headerlink" href="#public-api-layers" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Module</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="public-contract/">Public API Contract</a></td>
<td>Stable downstream SDK exports, CLI boundary, and out-of-scope items</td>
</tr>
<tr>
<td><a href="client/">Client</a></td>
<td><code>MT5Client</code> session abstraction for data access and order primitives</td>
</tr>
<tr>
<td><a href="schemas/">Schemas</a></td>
<td>Canonical DataFrame contracts and normalization helpers</td>
</tr>
<tr>
<td><a href="converters/">Converters</a></td>
<td>Symbol, timeframe, timezone, and date-range utilities</td>
</tr>
<tr>
<td><a href="exceptions/">Exceptions</a></td>
<td>Stable mt5cli exception types and MT5 error normalization</td>
</tr>
<tr>
<td><a href="sdk/">SDK</a></td>
<td>Module-level fetch helpers, multi-account collectors, incremental history</td>
</tr>
<tr>
<td><a href="trading/">Trading</a></td>
<td>Trading-capable sessions and operational helpers</td>
</tr>
<tr>
<td><a href="history/">History Collection (SQLite)</a></td>
<td>SQLite schema, incremental writes, dedup, and rate views</td>
</tr>
<tr>
<td><a href="telemetry/">Telemetry</a></td>
<td>OpenTelemetry metrics setup, meters, and emitted metric names</td>
</tr>
<tr>
<td><a href="grafana/">Grafana</a></td>
<td>Grafana-ready SQLite schema, views, snapshots, and published copies</td>
</tr>
<tr>
<td><a href="cli/">CLI</a></td>
<td>Typer commands that delegate to the Python API</td>
</tr>
<tr>
<td><a href="utils/">Utils</a></td>
<td>Parsing helpers and Click parameter types</td>
</tr>
</tbody>
</table>
<h2 id="architecture-overview">Architecture overview<a class="headerlink" href="#architecture-overview" title="Permanent link">&para;</a></h2>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>flowchart TD
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a> App[&quot;Downstream application&quot;] --&gt; Client[&quot;MT5Client&quot;]
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> CLI[&quot;mt5cli CLI&quot;] --&gt; Client
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> Client --&gt; SDK[&quot;sdk / pdmt5&quot;]
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a> Client --&gt; Schemas[&quot;schemas&quot;]
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a> History[&quot;history SQLite&quot;] --&gt; Utils[&quot;utils export&quot;]
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a> SDK --&gt; PDMT5[&quot;pdmt5.Mt5DataClient&quot;]
</code></pre></div>
<p>Downstream packages should depend on the package root exports documented in the
<a href="public-contract/">Public API Contract</a> (<code>MT5Client</code>,
<code>collect_history</code>, <code>load_rate_series_from_sqlite</code>, etc.) rather than private
modules. Lower-level helpers are accessible directly from their owning modules.</p>
<p><code>MT5Client.order_send()</code> is a live execution primitive that can place real trades. mt5cli exposes minimal execution helpers only; strategy logic, signals, backtests, and optimization remain out of scope and must be implemented downstream with explicit execution gating.</p>
<h2 id="quick-start">Quick start<a class="headerlink" href="#quick-start" title="Permanent link">&para;</a></h2>
<div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli</span><span class="w"> </span><span class="kn">import</span> <span class="n">MT5Client</span><span class="p">,</span> <span class="n">build_config</span><span class="p">,</span> <span class="n">mt5_session</span>
<a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>
<a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="k">with</span> <span class="n">mt5_session</span><span class="p">(</span><span class="n">build_config</span><span class="p">(</span><span class="n">login</span><span class="o">=</span><span class="mi">12345</span><span class="p">))</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
<a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a> <span class="n">rates</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">copy_rates_range</span><span class="p">(</span><span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="s2">&quot;H1&quot;</span><span class="p">,</span> <span class="s2">&quot;2024-01-01&quot;</span><span class="p">,</span> <span class="s2">&quot;2024-02-01&quot;</span><span class="p">)</span>
<a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a> <span class="n">positions</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">positions</span><span class="p">()</span>
</code></pre></div>
<div class="highlight"><pre><span></span><code><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>account.csv<span class="w"> </span>account-info
<a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>rates.parquet<span class="w"> </span>rates-range<span class="w"> </span>--symbol<span class="w"> </span>EURUSD<span class="w"> </span>--timeframe<span class="w"> </span>H1<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="w"> </span>--date-from<span class="w"> </span><span class="m">2024</span>-01-01<span class="w"> </span>--date-to<span class="w"> </span><span class="m">2024</span>-02-01
</code></pre></div>
<p>See individual module pages for detailed usage examples.</p></div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="../js/bootstrap.bundle.min.js"></script>
<script>
var base_url = "..",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="../js/base.js"></script>
<script src="../search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
+805
View File
@@ -0,0 +1,805 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="dceoy">
<link rel="canonical" href="https://github.com/dceoy/mt5cli/api/public-contract/">
<link rel="shortcut icon" href="../../img/favicon.ico">
<title>Public API Contract - mt5cli API Documentation</title>
<link href="../../css/bootstrap.min.css" rel="stylesheet">
<link href="../../css/fontawesome.min.css" rel="stylesheet">
<link href="../../css/brands.min.css" rel="stylesheet">
<link href="../../css/solid.min.css" rel="stylesheet">
<link href="../../css/v4-font-face.min.css" rel="stylesheet">
<link href="../../css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="../../assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="../..">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href="../.." class="nav-link">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle active" aria-current="page" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="../" class="dropdown-item">Overview</a>
</li>
<li>
<a href="./" class="dropdown-item active" aria-current="page">Public API Contract</a>
</li>
<li>
<a href="../client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="../schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="../converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="../exceptions/" class="dropdown-item">Exceptions</a>
</li>
<li>
<a href="../cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="../sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="../trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="../history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="../telemetry/" class="dropdown-item">Telemetry</a>
</li>
<li>
<a href="../grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="../utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a rel="prev" href="../" class="nav-link">
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li class="nav-item">
<a rel="next" href="../client/" class="nav-link">
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli/edit/master/docs/api/public-contract.md" class="nav-link">Edit on dceoy/mt5cli
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"><div class="navbar-expand-md bs-sidebar hidden-print affix" role="complementary">
<div class="navbar-header">
<button type="button" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#toc-collapse" title="Table of Contents">
<span class="fa fa-angle-down"></span>
</button>
</div>
<div id="toc-collapse" class="navbar-collapse collapse card bg-body-tertiary">
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="1"><a href="#public-api-contract" class="nav-link">Public API Contract</a>
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="2"><a href="#responsibility-boundary" class="nav-link">Responsibility boundary</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#stable-downstream-sdk-api" class="nav-link">Stable downstream SDK API</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#module-scoped-helpers" class="nav-link">Module-scoped helpers</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#cli-commands" class="nav-link">CLI commands</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#internal-helpers-not-stable" class="nav-link">Internal helpers (not stable)</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#explicitly-out-of-scope" class="nav-link">Explicitly out of scope</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#contract-verification" class="nav-link">Contract verification</a>
<ul class="nav flex-column">
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div></div>
<div class="col-md-9" role="main">
<h1 id="public-api-contract">Public API Contract<a class="headerlink" href="#public-api-contract" title="Permanent link">&para;</a></h1>
<p>mt5cli is the canonical operational trading SDK and CLI/batch layer over pdmt5.
The intended dependency direction is:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>downstream app -&gt; mt5cli -&gt; pdmt5 -&gt; MetaTrader 5
</code></pre></div>
<h2 id="responsibility-boundary">Responsibility boundary<a class="headerlink" href="#responsibility-boundary" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Owns</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>pdmt5</strong></td>
<td>MT5 core wrapper; DataFrame/dict conversion; canonical MT5 constants and parsers; direct low-level order primitives</td>
</tr>
<tr>
<td><strong>mt5cli</strong></td>
<td>CLI/batch workflows; SQLite history collection; normalized datasets; closed-bar helpers; small downstream operational SDK; generic broker-facing margin/volume/order orchestration</td>
</tr>
<tr>
<td><strong>downstream</strong></td>
<td>Strategy logic; signals; risk policy; backtesting; optimization; YAML/application semantics</td>
</tr>
</tbody>
</table>
<p>Downstream code should import raw pdmt5 types and constants (such as
<code>Mt5Config</code>, <code>Mt5RuntimeError</code>, <code>TIMEFRAME_MAP</code>, <code>COPY_TICKS_MAP</code>) directly
from <code>pdmt5</code> when needed. mt5cli does not serve as a pass-through compatibility
namespace for pdmt5. mt5cli's trading helpers type their client parameter against
an internal protocol backed by <code>pdmt5.Mt5DataClient</code>; <code>Mt5TradingClient</code> is no
longer required. <code>pdmt5.Mt5TradingError</code> was removed upstream in pdmt5 1.0.4;
mt5cli raises <code>Mt5OperationError</code> for all trading-related failures.</p>
<p>Note: the former <code>mt5cli</code> re-export <code>TICK_FLAG_MAP</code> corresponds to <code>COPY_TICKS_MAP</code>
in pdmt5 — the name changed, it was not simply moved.</p>
<p>Downstream packages should import from the package root (<code>from mt5cli import
...</code>). The contract set <code>STABLE_SDK_EXPORTS</code> in <code>mt5cli.contract</code> enumerates
every package-root symbol. Lower-level helpers (schema utilities, export
functions, parser helpers, low-level MT5 wrappers) are available directly from
their owning modules (<code>mt5cli.schemas</code>, <code>mt5cli.utils</code>, <code>mt5cli.converters</code>,
<code>mt5cli.sdk</code>, etc.) and are not part of the root SDK surface.</p>
<h2 id="stable-downstream-sdk-api">Stable downstream SDK API<a class="headerlink" href="#stable-downstream-sdk-api" title="Permanent link">&para;</a></h2>
<p>These names are exported from <code>mt5cli</code> and enumerated in
<code>mt5cli.STABLE_SDK_EXPORTS</code> (defined in <code>mt5cli.contract</code>).</p>
<h3 id="session-lifecycle-and-configuration">Session lifecycle and configuration<a class="headerlink" href="#session-lifecycle-and-configuration" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MT5Client</code></td>
<td>Read-only data client with optional <code>order_check</code> / <code>order_send</code></td>
</tr>
<tr>
<td><code>build_config</code></td>
<td>Build <code>pdmt5.Mt5Config</code> from connection fields; <code>login</code> accepts <code>int \| str \| None</code> — numeric strings are coerced to <code>int</code>, blank strings are treated as unset, and <code>${ENV_VAR}</code> / <code>$ENV_NAME</code> placeholders in string parameters are expanded when <code>allow_whole_dollar_env=True</code></td>
</tr>
<tr>
<td><code>mt5_session</code></td>
<td>Context manager: initialize, login, yield client, shutdown</td>
</tr>
<tr>
<td><code>create_trading_client</code>, <code>mt5_trading_session</code></td>
<td>Trading-capable MT5 client lifecycle; returns a raw <code>pdmt5.Mt5DataClient</code> (not <code>MT5Client</code>) supporting order execution, account management, and history deal retrieval</td>
</tr>
<tr>
<td><code>AccountSpec</code></td>
<td>Generic account group: symbols plus optional credentials</td>
</tr>
<tr>
<td><code>resolve_account_spec</code>, <code>resolve_account_specs</code></td>
<td>Merge overrides and expand <code>${ENV_VAR}</code> placeholders; opt-in <code>allow_whole_dollar_env</code> for bare <code>$NAME</code></td>
</tr>
</tbody>
</table>
<h3 id="closed-bar-rate-helpers">Closed-bar rate helpers<a class="headerlink" href="#closed-bar-rate-helpers" title="Permanent link">&para;</a></h3>
<p>MetaTrader 5 returns the still-forming bar as the last row when
<code>start_pos=0</code>. Use these helpers instead of reimplementing bar trimming or
timestamp normalization in downstream apps.</p>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>drop_forming_rate_bar</code></td>
<td>Remove the last row from chronologically ordered rate data</td>
</tr>
<tr>
<td><code>fetch_latest_closed_rates</code></td>
<td>Single connected client: fetch <code>count + 1</code>, drop forming bar</td>
</tr>
<tr>
<td><code>fetch_latest_closed_rates_for_trading_client</code></td>
<td>Closed bars from an active trading client session; returns RangeIndex</td>
</tr>
<tr>
<td><code>fetch_latest_closed_rates_indexed</code></td>
<td>Same as above but returns a UTC <code>DatetimeIndex</code> named <code>"time"</code> (no time column)</td>
</tr>
<tr>
<td><code>collect_latest_closed_rates_for_accounts</code></td>
<td>Multi-account closed bars with optional retry wrapper</td>
</tr>
<tr>
<td><code>collect_latest_closed_rates_by_granularity</code></td>
<td>Same data keyed by <code>(symbol, granularity_name)</code></td>
</tr>
<tr>
<td><code>collect_latest_rates_for_accounts_with_retries</code></td>
<td>Bounded exponential backoff for transient MT5 errors</td>
</tr>
</tbody>
</table>
<h3 id="sqlite-history-collection-and-rate-loading">SQLite history collection and rate loading<a class="headerlink" href="#sqlite-history-collection-and-rate-loading" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>collect_history</code></td>
<td>One-shot date-range export into SQLite</td>
</tr>
<tr>
<td><code>report_rate_gaps</code></td>
<td>SQLite-only one-row-per-gap report for a rate table or compatibility view</td>
</tr>
<tr>
<td><code>update_history</code>, <code>update_history_with_config</code></td>
<td>Incremental append from <code>MAX(time)</code> cursors</td>
</tr>
<tr>
<td><code>ThrottledHistoryUpdater</code></td>
<td>Minimum interval between successful incremental updates; optional <code>update_backend</code> injection</td>
</tr>
<tr>
<td><code>RateTarget</code>, <code>build_rate_targets</code></td>
<td>Neutral <code>(symbol, timeframe)</code> series descriptors</td>
</tr>
<tr>
<td><code>load_rate_series_from_sqlite</code>, <code>load_rate_series_by_granularity</code></td>
<td>Load one or many series; fail clearly when managed views are missing</td>
</tr>
</tbody>
</table>
<p>See <a href="../history/">History Collection (SQLite)</a> for schema, view naming, and ER
diagrams.</p>
<h3 id="trading-and-sizing-primitives-generic">Trading and sizing primitives (generic)<a class="headerlink" href="#trading-and-sizing-primitives-generic" title="Permanent link">&para;</a></h3>
<p>These helpers implement broker-facing calculations only. They do not encode
strategy entries, exits, Kelly sizing, or signal logic.</p>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>get_account_snapshot</code>, <code>get_symbol_snapshot</code>, <code>get_tick_snapshot</code>, <code>get_positions_frame</code></td>
<td>Normalized account/symbol/tick/position views</td>
</tr>
<tr>
<td><code>extract_tick_price</code></td>
<td>Positive finite bid/ask extraction from tick mappings</td>
</tr>
<tr>
<td><code>detect_position_side</code></td>
<td>Net long / short / flat from open positions</td>
</tr>
<tr>
<td><code>calculate_spread_ratio</code></td>
<td>Relative bid-ask spread</td>
</tr>
<tr>
<td><code>calculate_margin_and_volume</code>, <code>calculate_volume_by_margin</code>, <code>calculate_new_position_margin_ratio</code></td>
<td>Margin budget and volume sizing</td>
</tr>
<tr>
<td><code>normalize_order_volume</code>, <code>estimate_order_margin</code>, <code>calculate_positions_margin</code></td>
<td>Broker volume normalization and margin totals</td>
</tr>
<tr>
<td><code>calculate_positions_margin_by_symbol</code></td>
<td>Per-symbol margin map (resilient, first-seen order)</td>
</tr>
<tr>
<td><code>calculate_positions_margin_safe</code></td>
<td>Summed total margin across symbols (failed symbols skipped)</td>
</tr>
<tr>
<td><code>calculate_projected_margin_ratio</code></td>
<td>Estimated symbol-scoped margin/equity after optional new exposure</td>
</tr>
<tr>
<td><code>calculate_account_projected_margin_ratio</code></td>
<td>Account snapshot margin/equity after optional new exposure</td>
</tr>
<tr>
<td><code>calculate_symbol_group_margin_ratio</code></td>
<td>Estimated symbol-group margin/equity with optional exposure</td>
</tr>
<tr>
<td><code>determine_order_limits</code></td>
<td>SL/TP price levels from ratios</td>
</tr>
<tr>
<td><code>calculate_trailing_stop_updates</code></td>
<td>Per-ticket generic trailing stop-loss update plan</td>
</tr>
<tr>
<td><code>resolve_broker_filling_mode</code></td>
<td>Broker-supported filling-mode selection helper</td>
</tr>
<tr>
<td><code>ensure_symbol_selected</code></td>
<td>Select/verify Market Watch visibility</td>
</tr>
<tr>
<td><code>fetch_recent_history_deals_for_trading_client</code></td>
<td>Recent deal history from a connected trading client</td>
</tr>
<tr>
<td><code>place_market_order</code>, <code>close_open_positions</code>, <code>update_sltp_for_open_positions</code>, <code>update_trailing_stop_loss_for_open_positions</code></td>
<td>Order execution helpers (<code>dry_run</code> supported)</td>
</tr>
<tr>
<td><code>MarginVolume</code>, <code>OrderLimits</code>, <code>OrderExecutionResult</code></td>
<td>Typed return contracts for order helpers</td>
</tr>
<tr>
<td><code>OrderSide</code>, <code>OrderFillingMode</code>, <code>OrderTimeMode</code>, <code>PositionSide</code>, <code>ExecutionStatus</code></td>
<td>Typed enums for order helpers</td>
</tr>
<tr>
<td><code>ProjectionMode</code></td>
<td>Literal type for <code>calculate_symbol_group_margin_ratio</code> projection</td>
</tr>
</tbody>
</table>
<p><code>calculate_symbol_group_margin_ratio</code> accepts an optional <code>projection_mode</code>
parameter (<code>"add"</code> by default). Pass <code>projection_mode="replace_symbol"</code> to
subtract current exposure for <code>new_symbol</code> before adding the candidate margin —
useful for reversal-style projections. mt5cli only calculates broker-facing
exposure; downstream applications own thresholds, risk guard actions, and
strategy policy.</p>
<p><code>MT5Client.order_send()</code> and CLI <code>order-send --yes</code> are live execution paths.</p>
<p>Order helpers validate broker stop-level distance in <code>determine_order_limits()</code> and
raise <code>Mt5OperationError</code> when computed SL/TP prices are too close to the entry
quote. Validation uses <code>trade_stops_level * point</code> from the current quote and
symbol metadata as a pre-check only; it does not guarantee live order acceptance
after price movement and does not inspect <code>trade_freeze_level</code>. Live
<code>place_market_order()</code> and SL/TP updates call
<code>ensure_symbol_selected()</code> so hidden symbols are added to Market Watch before
sending requests. Failed, malformed, or unknown broker retcodes are fail-closed
and returned as <code>status="failed"</code> with normalized <code>request</code> / <code>response</code> details;
<code>dry_run=True</code> never calls <code>ensure_symbol_selected()</code> or <code>order_send()</code>.</p>
<h3 id="grafana-observability-sqlite-read-model">Grafana observability (SQLite read model)<a class="headerlink" href="#grafana-observability-sqlite-read-model" title="Permanent link">&para;</a></h3>
<p>These helpers prepare a SQLite database as a Grafana datasource. All DDL is
idempotent (<code>CREATE TABLE IF NOT EXISTS</code>, <code>DROP VIEW IF EXISTS</code> + <code>CREATE
VIEW</code>, <code>CREATE INDEX IF NOT EXISTS</code>). Missing source tables are skipped with a
warning rather than raising an error.</p>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>update_observability</code></td>
<td>Append one timestamped snapshot row per data type; accepts an already-connected <code>Mt5DataClient</code></td>
</tr>
<tr>
<td><code>update_observability_with_config</code></td>
<td>Standalone wrapper: opens/closes MT5 connection automatically around <code>update_observability</code></td>
</tr>
</tbody>
</table>
<p>Both functions write to the SQLite path given by <code>output=</code>. The optional
<code>symbols</code> parameter filters <code>positions_get</code> / <code>orders_get</code> by symbol.
<code>with_grafana_schema=False</code> (default) skips Grafana view/index setup; run
<code>grafana-schema</code> once to set up the schema, then call <code>snapshot</code> repeatedly
without this flag.</p>
<p><strong>Snapshot tables</strong> (created by <code>create_snapshot_tables</code> in <code>mt5cli.grafana</code>):</p>
<table>
<thead>
<tr>
<th>Table</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>account_snapshots</code></td>
<td>Balance, equity, margin, free-margin, P&amp;L</td>
</tr>
<tr>
<td><code>position_snapshots</code></td>
<td>Open positions: symbol, volume, profit, …</td>
</tr>
<tr>
<td><code>order_snapshots</code></td>
<td>Active orders: symbol, type, price, …</td>
</tr>
<tr>
<td><code>terminal_snapshots</code></td>
<td>Terminal connectivity and build info</td>
</tr>
<tr>
<td><code>snapshot_runs</code></td>
<td>Per-run status (<code>ok</code> / <code>error</code>) timestamp</td>
</tr>
</tbody>
</table>
<p><strong>Grafana time-series views</strong> (integer epoch-second <code>time</code> column; snapshot views also expose <code>run_id</code>):</p>
<table>
<thead>
<tr>
<th>View</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>grafana_rates</code></td>
<td><code>rates</code> table</td>
</tr>
<tr>
<td><code>grafana_ticks</code></td>
<td><code>ticks</code> table</td>
</tr>
<tr>
<td><code>grafana_history_deals</code></td>
<td><code>history_deals</code></td>
</tr>
<tr>
<td><code>grafana_history_orders</code></td>
<td><code>history_orders</code></td>
</tr>
<tr>
<td><code>grafana_trade_deals</code></td>
<td><code>history_deals</code> trade types only</td>
</tr>
<tr>
<td><code>grafana_cash_events</code></td>
<td><code>history_deals</code> non-trade events</td>
</tr>
<tr>
<td><code>grafana_symbol_pnl</code></td>
<td>Per-close-deal P&amp;L per symbol</td>
</tr>
<tr>
<td><code>grafana_account_snapshots</code></td>
<td><code>account_snapshots</code></td>
</tr>
<tr>
<td><code>grafana_position_snapshots</code></td>
<td><code>position_snapshots</code></td>
</tr>
<tr>
<td><code>grafana_order_snapshots</code></td>
<td><code>order_snapshots</code></td>
</tr>
<tr>
<td><code>grafana_terminal_snapshots</code></td>
<td><code>terminal_snapshots</code></td>
</tr>
</tbody>
</table>
<p><strong>Grafana static summary views</strong> (no <code>time</code> column; use for table/stat panels, not time-series):</p>
<table>
<thead>
<tr>
<th>View</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>grafana_realized_pnl</code></td>
<td>Cumulative realized PnL per symbol</td>
</tr>
<tr>
<td><code>grafana_trade_stats</code></td>
<td>Win/loss counts and profit per symbol</td>
</tr>
</tbody>
</table>
<p>Lower-level helpers (<code>ensure_grafana_schema</code>, <code>create_grafana_views</code>,
<code>create_grafana_indexes</code>, <code>create_snapshot_tables</code>, <code>start_snapshot_run</code>,
<code>insert_account_snapshot</code>, <code>insert_position_snapshots</code>, <code>insert_order_snapshots</code>,
<code>insert_terminal_snapshot</code>, <code>record_snapshot_run</code>) are available directly from
<code>mt5cli.grafana</code> and are not part of the package-root stable surface.</p>
<h3 id="errors">Errors<a class="headerlink" href="#errors" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Mt5CliError</code>, <code>Mt5ConnectionError</code>, <code>Mt5OperationError</code>, <code>Mt5SchemaError</code></td>
<td>Stable mt5cli exception types</td>
</tr>
</tbody>
</table>
<h2 id="module-scoped-helpers">Module-scoped helpers<a class="headerlink" href="#module-scoped-helpers" title="Permanent link">&para;</a></h2>
<p>Lower-level helpers are available from their owning modules and are not part
of the package-root stable surface. Import them directly when needed:</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>mt5cli.grafana</code></td>
<td><code>ensure_grafana_schema</code>, <code>create_grafana_views</code>, <code>create_grafana_indexes</code>, <code>create_snapshot_tables</code>, <code>start_snapshot_run</code>, <code>insert_account_snapshot</code>, <code>record_snapshot_run</code></td>
</tr>
<tr>
<td><code>mt5cli.history</code></td>
<td><code>resolve_rate_view_name</code>, <code>resolve_rate_tables</code>, <code>load_rate_data</code>, <code>build_rate_view_name</code></td>
</tr>
<tr>
<td><code>mt5cli.sdk</code></td>
<td><code>copy_rates_from</code>, <code>copy_ticks_from</code>, <code>account_info</code>, <code>symbols</code>, <code>mt5_summary</code>, <code>latest_rates</code></td>
</tr>
<tr>
<td><code>mt5cli.schemas</code></td>
<td><code>DataKind</code>, <code>normalize_dataframe</code>, <code>validate_schema</code>, <code>DEDUP_KEYS</code></td>
</tr>
<tr>
<td><code>mt5cli.utils</code></td>
<td><code>Dataset</code>, <code>IfExists</code>, <code>detect_format</code>, <code>export_dataframe</code>, <code>export_dataframe_to_sqlite</code></td>
</tr>
<tr>
<td><code>mt5cli.converters</code></td>
<td><code>normalize_symbol</code>, <code>ensure_utc</code>, <code>parse_date_range</code>, <code>granularity_name</code></td>
</tr>
<tr>
<td><code>mt5cli.exceptions</code></td>
<td><code>normalize_mt5_exception</code>, <code>call_with_normalized_errors</code>, <code>is_recoverable_mt5_error</code></td>
</tr>
</tbody>
</table>
<h2 id="cli-commands">CLI commands<a class="headerlink" href="#cli-commands" title="Permanent link">&para;</a></h2>
<p>The Typer application in <code>mt5cli.cli</code> exposes file-export commands documented in
<a href="../cli/">CLI Module</a> and the project README. CLI commands:</p>
<ul>
<li>Require <code>-o/--output</code> and write CSV, JSON, Parquet, or SQLite.</li>
<li>Accept global MT5 connection options (<code>--login</code>, <code>--password</code>, <code>--server</code>,
<code>--path</code>, <code>--timeout</code>).</li>
<li>Resolve unset CLI connection options from <code>MT5_LOGIN</code>, <code>MT5_PASSWORD</code>,
<code>MT5_SERVER</code>, and <code>MT5_PATH</code>, and expand <code>${ENV_VAR}</code> placeholders in CLI
string fields before building the MT5 config.</li>
<li>Delegate to the same Python APIs described here; they are not duplicated
business logic.</li>
</ul>
<p><code>grafana-schema</code> initializes Grafana views, indexes, and snapshot tables in the
target SQLite database without connecting to MT5. It is idempotent and safe to
run repeatedly.</p>
<p><code>snapshot</code> appends one timestamped row per enabled data type
(<code>--with-account</code>, <code>--with-positions</code>, <code>--with-orders</code>, <code>--with-terminal</code>) and
never places orders or modifies trading state. Both commands require
<code>-o/--output</code> to point at a <code>.db</code> / SQLite file.</p>
<p><code>order-send</code> is the expert raw-request path; it requires <code>--yes</code> and a fully
constructed request payload. <code>close-positions</code> is the safer high-level helper
that closes open positions by <code>--symbol</code> or <code>--ticket</code> using
<code>close_open_positions()</code>. Both <code>order-send --yes</code> and <code>close-positions --yes</code>
are live execution paths. <code>close-positions --dry-run</code> previews close orders
without placing them and does not require <code>--yes</code>. <code>close-positions</code> also
accepts optional <code>--deviation</code>, <code>--comment</code>, and <code>--magic</code>; <code>--magic</code> scopes
the selected open positions fail-closed when position magic metadata is absent.</p>
<p><code>history-gaps</code> reads an existing SQLite history database and exports one row
per detected gap from managed rate compatibility views. It never initializes
MT5. Pass <code>--granularity-seconds</code> for custom tables or views whose bar spacing
cannot be inferred from the name.</p>
<h2 id="internal-helpers-not-stable">Internal helpers (not stable)<a class="headerlink" href="#internal-helpers-not-stable" title="Permanent link">&para;</a></h2>
<p>Do not import these for downstream contracts; they may change without a semver
notice:</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>mt5cli.sdk</code></td>
<td><code>connected_client</code>, <code>_run_with_client</code>, private coercion helpers</td>
</tr>
<tr>
<td><code>mt5cli.history</code></td>
<td><code>write_*_dataset</code>, <code>deduplicate_history_tables</code>, <code>parse_sqlite_timestamp</code></td>
</tr>
<tr>
<td><code>mt5cli.retry</code></td>
<td><code>retry_with_backoff</code></td>
</tr>
<tr>
<td><code>mt5cli.cli</code></td>
<td>Typer command handlers and Click parameter types</td>
</tr>
<tr>
<td>Leading-underscore names</td>
<td>Any <code>_</code>-prefixed function or method</td>
</tr>
</tbody>
</table>
<p>Use the package-root stable exports instead of reaching into submodule
internals.</p>
<h2 id="explicitly-out-of-scope">Explicitly out of scope<a class="headerlink" href="#explicitly-out-of-scope" title="Permanent link">&para;</a></h2>
<p>mt5cli must <strong>not</strong> implement downstream strategy or research responsibilities.
The following belong in consuming applications, not in mt5cli:</p>
<ul>
<li>Signal detection (for example AR-GARCH or other model-specific triggers)</li>
<li>Backtesting, walk-forward analysis, or parameter optimization</li>
<li>Strategy-specific risk policy, position sizing systems, or Kelly fractions</li>
<li>Entry/exit decision logic or YAML strategy semantics</li>
<li>Entry-deal classification, Kelly fractions, or betting-specific deal transformations
(use <code>fetch_recent_history_deals_for_trading_client</code> to retrieve raw deal data, then
apply downstream transformations in your own adapter layer)</li>
<li>Application-specific credential schema keys wired into mt5cli internals</li>
</ul>
<p>mt5cli provides connection lifecycle, normalized data access, SQLite history
machinery, closed-bar helpers, generic margin/volume/spread/SL/TP utilities, and
optional order primitives so downstream apps can focus on strategy code behind
their own adapter layer.</p>
<h2 id="contract-verification">Contract verification<a class="headerlink" href="#contract-verification" title="Permanent link">&para;</a></h2>
<p><code>tests/test_contracts.py</code> asserts that every name in <code>STABLE_SDK_EXPORTS</code> is
importable from <code>mt5cli</code>, that all package-root exports are covered by the
stable set, and documents key closed-bar, SQLite loading, account-resolution,
and trading-session behaviors.</p></div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="../../js/bootstrap.bundle.min.js"></script>
<script>
var base_url = "../..",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="../../js/base.js"></script>
<script src="../../search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
+9816
View File
File diff suppressed because it is too large Load Diff
+673
View File
@@ -0,0 +1,673 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="dceoy">
<link rel="canonical" href="https://github.com/dceoy/mt5cli/api/telemetry/">
<link rel="shortcut icon" href="../../img/favicon.ico">
<title>Telemetry - mt5cli API Documentation</title>
<link href="../../css/bootstrap.min.css" rel="stylesheet">
<link href="../../css/fontawesome.min.css" rel="stylesheet">
<link href="../../css/brands.min.css" rel="stylesheet">
<link href="../../css/solid.min.css" rel="stylesheet">
<link href="../../css/v4-font-face.min.css" rel="stylesheet">
<link href="../../css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="../../assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="../..">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href="../.." class="nav-link">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle active" aria-current="page" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="../" class="dropdown-item">Overview</a>
</li>
<li>
<a href="../public-contract/" class="dropdown-item">Public API Contract</a>
</li>
<li>
<a href="../client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="../schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="../converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="../exceptions/" class="dropdown-item">Exceptions</a>
</li>
<li>
<a href="../cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="../sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="../trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="../history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="./" class="dropdown-item active" aria-current="page">Telemetry</a>
</li>
<li>
<a href="../grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="../utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a rel="prev" href="../history/" class="nav-link">
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li class="nav-item">
<a rel="next" href="../grafana/" class="nav-link">
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli/edit/master/docs/api/telemetry.md" class="nav-link">Edit on dceoy/mt5cli
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"><div class="navbar-expand-md bs-sidebar hidden-print affix" role="complementary">
<div class="navbar-header">
<button type="button" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#toc-collapse" title="Table of Contents">
<span class="fa fa-angle-down"></span>
</button>
</div>
<div id="toc-collapse" class="navbar-collapse collapse card bg-body-tertiary">
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="1"><a href="#telemetry" class="nav-link">Telemetry</a>
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="2"><a href="#mt5cli.telemetry" class="nav-link">telemetry</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#enabling-opentelemetry-metrics" class="nav-link">Enabling OpenTelemetry metrics</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#emitted-metric-names" class="nav-link">Emitted metric names</a>
<ul class="nav flex-column">
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div></div>
<div class="col-md-9" role="main">
<h1 id="telemetry">Telemetry<a class="headerlink" href="#telemetry" title="Permanent link">&para;</a></h1>
<div class="doc doc-object doc-module">
<h2 id="mt5cli.telemetry" class="doc doc-heading">
<span class="doc doc-object-name doc-module-name">mt5cli.telemetry</span>
<a href="#mt5cli.telemetry" class="headerlink" title="Permanent link">&para;</a></h2>
<div class="doc doc-contents first">
<p>Optional OpenTelemetry metrics for MT5 history and snapshot observability.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-attribute">
<h3 id="mt5cli.telemetry.logger" class="doc doc-heading">
<span class="doc doc-object-name doc-attribute-name">logger</span>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-module-attribute"><code>module-attribute</code></small>
</span>
<a href="#mt5cli.telemetry.logger" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="n">logger</span> <span class="o">=</span> <span class="n"><span title="logging.getLogger">getLogger</span></span><span class="p">(</span><span class="n"><span title="__name__">__name__</span></span><span class="p">)</span>
</code></pre></div>
<div class="doc doc-contents ">
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.telemetry.configure_metrics" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">configure_metrics</span>
<a href="#mt5cli.telemetry.configure_metrics" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">configure_metrics</span><span class="p">(</span><span class="n">meter</span><span class="p">:</span> <span class="n"><span title="typing.Any">Any</span></span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Configure MT5 metrics using the provided meter.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>meter</code>
</td>
<td>
<code><span title="typing.Any">Any</span></code>
</td>
<td>
<div class="doc-md-description">
<p>An OpenTelemetry <code>Meter</code> or duck-typed compatible object.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/telemetry.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-291">291</a></span>
<span class="normal"><a href="#__codelineno-0-292">292</a></span>
<span class="normal"><a href="#__codelineno-0-293">293</a></span>
<span class="normal"><a href="#__codelineno-0-294">294</a></span>
<span class="normal"><a href="#__codelineno-0-295">295</a></span>
<span class="normal"><a href="#__codelineno-0-296">296</a></span>
<span class="normal"><a href="#__codelineno-0-297">297</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-291" name="__codelineno-0-291"></a><span class="k">def</span><span class="w"> </span><span class="nf">configure_metrics</span><span class="p">(</span><span class="n">meter</span><span class="p">:</span> <span class="n">Any</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span> <span class="c1"># noqa: ANN401</span>
<a id="__codelineno-0-292" name="__codelineno-0-292"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Configure MT5 metrics using the provided meter.</span>
<a id="__codelineno-0-293" name="__codelineno-0-293"></a>
<a id="__codelineno-0-294" name="__codelineno-0-294"></a><span class="sd"> Args:</span>
<a id="__codelineno-0-295" name="__codelineno-0-295"></a><span class="sd"> meter: An OpenTelemetry ``Meter`` or duck-typed compatible object.</span>
<a id="__codelineno-0-296" name="__codelineno-0-296"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-297" name="__codelineno-0-297"></a> <span class="n">_metrics</span><span class="o">.</span><span class="n">configure</span><span class="p">(</span><span class="n">meter</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.telemetry.enable_otel_metrics" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">enable_otel_metrics</span>
<a href="#mt5cli.telemetry.enable_otel_metrics" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">enable_otel_metrics</span><span class="p">(</span>
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a> <span class="n">service_name</span><span class="p">:</span> <span class="n"><span title="str">str</span></span> <span class="o">=</span> <span class="s2">&quot;mt5cli&quot;</span><span class="p">,</span>
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> <span class="n">readers</span><span class="p">:</span> <span class="n"><span title="list">list</span></span><span class="p">[</span><span class="n"><span title="typing.Any">Any</span></span><span class="p">]</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Enable OTel metrics by wiring up an SDK <code>MeterProvider</code> pipeline.</p>
<p>Requires the <code>otel</code> optional dependency group:
<code>pip install "mt5cli[otel]"</code>.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>service_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>OTel meter/service name used for the <code>Resource</code> and
the meter itself.</p>
</div>
</td>
<td>
<code>&#39;mt5cli&#39;</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>readers</code>
</td>
<td>
<code><span title="list">list</span>[<span title="typing.Any">Any</span>] | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Optional list of metric readers. When <em>None</em> (the default),
a :class:<code>~opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader</code>
backed by an OTLP HTTP exporter is created automatically
(reads the endpoint from <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>).
Pass a custom list (e.g. <code>InMemoryMetricReader</code> for tests)
to override.</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="ImportError">ImportError</span></code>
</td>
<td>
<div class="doc-md-description">
<p>If <code>opentelemetry-api</code> is not installed, or if
<code>readers</code> is <em>None</em> and
<code>opentelemetry-exporter-otlp-proto-http</code> is not installed.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/telemetry.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-300">300</a></span>
<span class="normal"><a href="#__codelineno-0-301">301</a></span>
<span class="normal"><a href="#__codelineno-0-302">302</a></span>
<span class="normal"><a href="#__codelineno-0-303">303</a></span>
<span class="normal"><a href="#__codelineno-0-304">304</a></span>
<span class="normal"><a href="#__codelineno-0-305">305</a></span>
<span class="normal"><a href="#__codelineno-0-306">306</a></span>
<span class="normal"><a href="#__codelineno-0-307">307</a></span>
<span class="normal"><a href="#__codelineno-0-308">308</a></span>
<span class="normal"><a href="#__codelineno-0-309">309</a></span>
<span class="normal"><a href="#__codelineno-0-310">310</a></span>
<span class="normal"><a href="#__codelineno-0-311">311</a></span>
<span class="normal"><a href="#__codelineno-0-312">312</a></span>
<span class="normal"><a href="#__codelineno-0-313">313</a></span>
<span class="normal"><a href="#__codelineno-0-314">314</a></span>
<span class="normal"><a href="#__codelineno-0-315">315</a></span>
<span class="normal"><a href="#__codelineno-0-316">316</a></span>
<span class="normal"><a href="#__codelineno-0-317">317</a></span>
<span class="normal"><a href="#__codelineno-0-318">318</a></span>
<span class="normal"><a href="#__codelineno-0-319">319</a></span>
<span class="normal"><a href="#__codelineno-0-320">320</a></span>
<span class="normal"><a href="#__codelineno-0-321">321</a></span>
<span class="normal"><a href="#__codelineno-0-322">322</a></span>
<span class="normal"><a href="#__codelineno-0-323">323</a></span>
<span class="normal"><a href="#__codelineno-0-324">324</a></span>
<span class="normal"><a href="#__codelineno-0-325">325</a></span>
<span class="normal"><a href="#__codelineno-0-326">326</a></span>
<span class="normal"><a href="#__codelineno-0-327">327</a></span>
<span class="normal"><a href="#__codelineno-0-328">328</a></span>
<span class="normal"><a href="#__codelineno-0-329">329</a></span>
<span class="normal"><a href="#__codelineno-0-330">330</a></span>
<span class="normal"><a href="#__codelineno-0-331">331</a></span>
<span class="normal"><a href="#__codelineno-0-332">332</a></span>
<span class="normal"><a href="#__codelineno-0-333">333</a></span>
<span class="normal"><a href="#__codelineno-0-334">334</a></span>
<span class="normal"><a href="#__codelineno-0-335">335</a></span>
<span class="normal"><a href="#__codelineno-0-336">336</a></span>
<span class="normal"><a href="#__codelineno-0-337">337</a></span>
<span class="normal"><a href="#__codelineno-0-338">338</a></span>
<span class="normal"><a href="#__codelineno-0-339">339</a></span>
<span class="normal"><a href="#__codelineno-0-340">340</a></span>
<span class="normal"><a href="#__codelineno-0-341">341</a></span>
<span class="normal"><a href="#__codelineno-0-342">342</a></span>
<span class="normal"><a href="#__codelineno-0-343">343</a></span>
<span class="normal"><a href="#__codelineno-0-344">344</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-300" name="__codelineno-0-300"></a><span class="k">def</span><span class="w"> </span><span class="nf">enable_otel_metrics</span><span class="p">(</span>
<a id="__codelineno-0-301" name="__codelineno-0-301"></a> <span class="n">service_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;mt5cli&quot;</span><span class="p">,</span>
<a id="__codelineno-0-302" name="__codelineno-0-302"></a> <span class="n">readers</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Any</span><span class="p">]</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<a id="__codelineno-0-303" name="__codelineno-0-303"></a><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-0-304" name="__codelineno-0-304"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Enable OTel metrics by wiring up an SDK ``MeterProvider`` pipeline.</span>
<a id="__codelineno-0-305" name="__codelineno-0-305"></a>
<a id="__codelineno-0-306" name="__codelineno-0-306"></a><span class="sd"> Requires the ``otel`` optional dependency group:</span>
<a id="__codelineno-0-307" name="__codelineno-0-307"></a><span class="sd"> ``pip install &quot;mt5cli[otel]&quot;``.</span>
<a id="__codelineno-0-308" name="__codelineno-0-308"></a>
<a id="__codelineno-0-309" name="__codelineno-0-309"></a><span class="sd"> Args:</span>
<a id="__codelineno-0-310" name="__codelineno-0-310"></a><span class="sd"> service_name: OTel meter/service name used for the ``Resource`` and</span>
<a id="__codelineno-0-311" name="__codelineno-0-311"></a><span class="sd"> the meter itself.</span>
<a id="__codelineno-0-312" name="__codelineno-0-312"></a><span class="sd"> readers: Optional list of metric readers. When *None* (the default),</span>
<a id="__codelineno-0-313" name="__codelineno-0-313"></a><span class="sd"> a :class:`~opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader`</span>
<a id="__codelineno-0-314" name="__codelineno-0-314"></a><span class="sd"> backed by an OTLP HTTP exporter is created automatically</span>
<a id="__codelineno-0-315" name="__codelineno-0-315"></a><span class="sd"> (reads the endpoint from ``OTEL_EXPORTER_OTLP_ENDPOINT``).</span>
<a id="__codelineno-0-316" name="__codelineno-0-316"></a><span class="sd"> Pass a custom list (e.g. ``InMemoryMetricReader`` for tests)</span>
<a id="__codelineno-0-317" name="__codelineno-0-317"></a><span class="sd"> to override.</span>
<a id="__codelineno-0-318" name="__codelineno-0-318"></a>
<a id="__codelineno-0-319" name="__codelineno-0-319"></a><span class="sd"> Raises:</span>
<a id="__codelineno-0-320" name="__codelineno-0-320"></a><span class="sd"> ImportError: If ``opentelemetry-api`` is not installed, or if</span>
<a id="__codelineno-0-321" name="__codelineno-0-321"></a><span class="sd"> ``readers`` is *None* and</span>
<a id="__codelineno-0-322" name="__codelineno-0-322"></a><span class="sd"> ``opentelemetry-exporter-otlp-proto-http`` is not installed.</span>
<a id="__codelineno-0-323" name="__codelineno-0-323"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-324" name="__codelineno-0-324"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">_OTEL_AVAILABLE</span><span class="p">:</span>
<a id="__codelineno-0-325" name="__codelineno-0-325"></a> <span class="n">msg</span> <span class="o">=</span> <span class="p">(</span>
<a id="__codelineno-0-326" name="__codelineno-0-326"></a> <span class="s2">&quot;opentelemetry-api is not installed. &quot;</span>
<a id="__codelineno-0-327" name="__codelineno-0-327"></a> <span class="s1">&#39;Install it with: pip install &quot;mt5cli[otel]&quot;&#39;</span>
<a id="__codelineno-0-328" name="__codelineno-0-328"></a> <span class="p">)</span>
<a id="__codelineno-0-329" name="__codelineno-0-329"></a> <span class="k">raise</span> <span class="ne">ImportError</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
<a id="__codelineno-0-330" name="__codelineno-0-330"></a> <span class="k">if</span> <span class="n">readers</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-0-331" name="__codelineno-0-331"></a> <span class="k">if</span> <span class="n">_OtelOTLPExporter</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-0-332" name="__codelineno-0-332"></a> <span class="n">msg</span> <span class="o">=</span> <span class="p">(</span>
<a id="__codelineno-0-333" name="__codelineno-0-333"></a> <span class="s2">&quot;opentelemetry-exporter-otlp-proto-http is required for the &quot;</span>
<a id="__codelineno-0-334" name="__codelineno-0-334"></a> <span class="s2">&quot;default OTLP export pipeline. &quot;</span>
<a id="__codelineno-0-335" name="__codelineno-0-335"></a> <span class="s1">&#39;Install it with: pip install &quot;mt5cli[otel]&quot; or pass a &#39;</span>
<a id="__codelineno-0-336" name="__codelineno-0-336"></a> <span class="s2">&quot;custom readers list.&quot;</span>
<a id="__codelineno-0-337" name="__codelineno-0-337"></a> <span class="p">)</span>
<a id="__codelineno-0-338" name="__codelineno-0-338"></a> <span class="k">raise</span> <span class="ne">ImportError</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
<a id="__codelineno-0-339" name="__codelineno-0-339"></a> <span class="n">readers</span> <span class="o">=</span> <span class="p">[</span><span class="n">_OtelPeriodicReader</span><span class="p">(</span><span class="n">_OtelOTLPExporter</span><span class="p">())]</span> <span class="c1"># type: ignore[misc]</span>
<a id="__codelineno-0-340" name="__codelineno-0-340"></a> <span class="n">resource</span> <span class="o">=</span> <span class="n">_OtelResource</span><span class="o">.</span><span class="n">create</span><span class="p">({</span><span class="s2">&quot;service.name&quot;</span><span class="p">:</span> <span class="n">service_name</span><span class="p">})</span> <span class="c1"># type: ignore[union-attr]</span>
<a id="__codelineno-0-341" name="__codelineno-0-341"></a> <span class="n">provider</span> <span class="o">=</span> <span class="n">_OtelMeterProvider</span><span class="p">(</span><span class="n">resource</span><span class="o">=</span><span class="n">resource</span><span class="p">,</span> <span class="n">metric_readers</span><span class="o">=</span><span class="n">readers</span><span class="p">)</span> <span class="c1"># type: ignore[misc]</span>
<a id="__codelineno-0-342" name="__codelineno-0-342"></a> <span class="n">_otel_metrics_mod</span><span class="o">.</span><span class="n">set_meter_provider</span><span class="p">(</span><span class="n">provider</span><span class="p">)</span> <span class="c1"># type: ignore[union-attr]</span>
<a id="__codelineno-0-343" name="__codelineno-0-343"></a> <span class="n">meter</span> <span class="o">=</span> <span class="n">provider</span><span class="o">.</span><span class="n">get_meter</span><span class="p">(</span><span class="n">service_name</span><span class="p">)</span>
<a id="__codelineno-0-344" name="__codelineno-0-344"></a> <span class="n">configure_metrics</span><span class="p">(</span><span class="n">meter</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="mt5cli.telemetry.get_metrics" class="doc doc-heading">
<span class="doc doc-object-name doc-function-name">get_metrics</span>
<a href="#mt5cli.telemetry.get_metrics" class="headerlink" title="Permanent link">&para;</a></h3>
<div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">get_metrics</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n"><span title="mt5cli.telemetry._Mt5Metrics">_Mt5Metrics</span></span>
</code></pre></div>
<div class="doc doc-contents ">
<p>Return the global :class:<code>_Mt5Metrics</code> instance.</p>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="mt5cli.telemetry._Mt5Metrics">_Mt5Metrics</span></code>
</td>
<td>
<div class="doc-md-description">
<p>The global metric registry (no-op until :func:<code>configure_metrics</code> is</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="mt5cli.telemetry._Mt5Metrics">_Mt5Metrics</span></code>
</td>
<td>
<div class="doc-md-description">
<p>called).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>mt5cli/telemetry.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-347">347</a></span>
<span class="normal"><a href="#__codelineno-0-348">348</a></span>
<span class="normal"><a href="#__codelineno-0-349">349</a></span>
<span class="normal"><a href="#__codelineno-0-350">350</a></span>
<span class="normal"><a href="#__codelineno-0-351">351</a></span>
<span class="normal"><a href="#__codelineno-0-352">352</a></span>
<span class="normal"><a href="#__codelineno-0-353">353</a></span>
<span class="normal"><a href="#__codelineno-0-354">354</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-347" name="__codelineno-0-347"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_metrics</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">_Mt5Metrics</span><span class="p">:</span>
<a id="__codelineno-0-348" name="__codelineno-0-348"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Return the global :class:`_Mt5Metrics` instance.</span>
<a id="__codelineno-0-349" name="__codelineno-0-349"></a>
<a id="__codelineno-0-350" name="__codelineno-0-350"></a><span class="sd"> Returns:</span>
<a id="__codelineno-0-351" name="__codelineno-0-351"></a><span class="sd"> The global metric registry (no-op until :func:`configure_metrics` is</span>
<a id="__codelineno-0-352" name="__codelineno-0-352"></a><span class="sd"> called).</span>
<a id="__codelineno-0-353" name="__codelineno-0-353"></a><span class="sd"> &quot;&quot;&quot;</span>
<a id="__codelineno-0-354" name="__codelineno-0-354"></a> <span class="k">return</span> <span class="n">_metrics</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="enabling-opentelemetry-metrics">Enabling OpenTelemetry metrics<a class="headerlink" href="#enabling-opentelemetry-metrics" title="Permanent link">&para;</a></h2>
<p>Install the optional exporter dependencies with:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>uv<span class="w"> </span>add<span class="w"> </span><span class="s1">&#39;mt5cli[otel]&#39;</span>
</code></pre></div>
<p>Then enable the default OTLP HTTP pipeline:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli.telemetry</span><span class="w"> </span><span class="kn">import</span> <span class="n">enable_otel_metrics</span>
<a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>
<a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="n">enable_otel_metrics</span><span class="p">(</span><span class="n">service_name</span><span class="o">=</span><span class="s2">&quot;mt5cli&quot;</span><span class="p">)</span>
</code></pre></div>
<p>When <code>readers=None</code>, <code>enable_otel_metrics()</code> builds a
<code>PeriodicExportingMetricReader</code> backed by the OTLP HTTP exporter and reads the
endpoint from <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>.</p>
<p>If your application already owns an OpenTelemetry <code>Meter</code>, wire mt5cli into it
directly with <code>configure_metrics(meter)</code>.</p>
<h2 id="emitted-metric-names">Emitted metric names<a class="headerlink" href="#emitted-metric-names" title="Permanent link">&para;</a></h2>
<p><code>enable_otel_metrics()</code> / <code>configure_metrics()</code> register these instruments:</p>
<ul>
<li><code>mt5_history_update_duration_seconds</code></li>
<li><code>mt5_history_update_rows_total</code></li>
<li><code>mt5_history_update_failures_total</code></li>
<li><code>mt5_snapshot_update_duration_seconds</code></li>
<li><code>mt5_snapshot_update_failures_total</code></li>
<li><code>mt5_account_balance</code></li>
<li><code>mt5_account_equity</code></li>
<li><code>mt5_account_margin</code></li>
<li><code>mt5_account_margin_free</code></li>
<li><code>mt5_account_margin_level</code></li>
<li><code>mt5_position_profit</code></li>
<li><code>mt5_position_volume</code></li>
<li><code>mt5_terminal_connected</code></li>
<li><code>mt5_terminal_trade_allowed</code></li>
<li><code>mt5_terminal_trade_expert</code></li>
<li><code>mt5_last_successful_update_timestamp</code></li>
</ul>
<p>The history metrics use a <code>dataset</code> attribute. Account and position gauges add
labels such as <code>login</code>, <code>server</code>, and <code>symbol</code> where applicable.</p></div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="../../js/bootstrap.bundle.min.js"></script>
<script>
var base_url = "../..",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="../../js/base.js"></script>
<script src="../../search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
+2275
View File
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
/* Avoid breaking parameter names, etc. in table cells. */
.doc-contents td code {
word-break: normal !important;
}
/* No line break before first paragraph of descriptions. */
.doc-md-description,
.doc-md-description>p:first-child {
display: inline;
}
/* No text transformation from Material for MkDocs for H5 headings. */
.md-typeset h5 .doc-object-name {
text-transform: none;
}
/* Max width for docstring sections tables. */
.doc .md-typeset__table,
.doc .md-typeset__table table {
display: table !important;
width: 100%;
}
.doc .md-typeset__table tr {
display: table-row;
}
/* Defaults in Spacy table style. */
.doc-param-default,
.doc-type_param-default {
float: right;
}
/* Parameter headings must be inline, not blocks. */
.doc-heading-parameter,
.doc-heading-type_parameter {
display: inline;
}
/* Default font size for parameter headings. */
.md-typeset .doc-heading-parameter {
font-size: inherit;
}
/* Prefer space on the right, not the left of parameter permalinks. */
.doc-heading-parameter .headerlink,
.doc-heading-type_parameter .headerlink {
margin-left: 0 !important;
margin-right: 0.2rem;
}
/* Backward-compatibility: docstring section titles in bold. */
.doc-section-title {
font-weight: bold;
}
/* Backlinks crumb separator. */
.doc-backlink-crumb {
display: inline-flex;
gap: .2rem;
white-space: nowrap;
align-items: center;
vertical-align: middle;
}
.doc-backlink-crumb:not(:first-child)::before {
background-color: var(--md-default-fg-color--lighter);
content: "";
display: inline;
height: 1rem;
--md-path-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6z"/></svg>');
-webkit-mask-image: var(--md-path-icon);
mask-image: var(--md-path-icon);
width: 1rem;
}
.doc-backlink-crumb.last {
font-weight: bold;
}
/* Symbols in Navigation and ToC. */
:root, :host,
[data-md-color-scheme="default"] {
--doc-symbol-parameter-fg-color: #df50af;
--doc-symbol-type_parameter-fg-color: #df50af;
--doc-symbol-attribute-fg-color: #953800;
--doc-symbol-function-fg-color: #8250df;
--doc-symbol-method-fg-color: #8250df;
--doc-symbol-class-fg-color: #0550ae;
--doc-symbol-type_alias-fg-color: #0550ae;
--doc-symbol-module-fg-color: #5cad0f;
--doc-symbol-parameter-bg-color: #df50af1a;
--doc-symbol-type_parameter-bg-color: #df50af1a;
--doc-symbol-attribute-bg-color: #9538001a;
--doc-symbol-function-bg-color: #8250df1a;
--doc-symbol-method-bg-color: #8250df1a;
--doc-symbol-class-bg-color: #0550ae1a;
--doc-symbol-type_alias-bg-color: #0550ae1a;
--doc-symbol-module-bg-color: #5cad0f1a;
}
[data-md-color-scheme="slate"] {
--doc-symbol-parameter-fg-color: #ffa8cc;
--doc-symbol-type_parameter-fg-color: #ffa8cc;
--doc-symbol-attribute-fg-color: #ffa657;
--doc-symbol-function-fg-color: #d2a8ff;
--doc-symbol-method-fg-color: #d2a8ff;
--doc-symbol-class-fg-color: #79c0ff;
--doc-symbol-type_alias-fg-color: #79c0ff;
--doc-symbol-module-fg-color: #baff79;
--doc-symbol-parameter-bg-color: #ffa8cc1a;
--doc-symbol-type_parameter-bg-color: #ffa8cc1a;
--doc-symbol-attribute-bg-color: #ffa6571a;
--doc-symbol-function-bg-color: #d2a8ff1a;
--doc-symbol-method-bg-color: #d2a8ff1a;
--doc-symbol-class-bg-color: #79c0ff1a;
--doc-symbol-type_alias-bg-color: #79c0ff1a;
--doc-symbol-module-bg-color: #baff791a;
}
code.doc-symbol {
border-radius: .1rem;
font-size: .85em;
padding: 0 .3em;
font-weight: bold;
}
code.doc-symbol-parameter,
a code.doc-symbol-parameter {
color: var(--doc-symbol-parameter-fg-color);
background-color: var(--doc-symbol-parameter-bg-color);
}
code.doc-symbol-parameter::after {
content: "param";
}
code.doc-symbol-type_parameter,
a code.doc-symbol-type_parameter {
color: var(--doc-symbol-type_parameter-fg-color);
background-color: var(--doc-symbol-type_parameter-bg-color);
}
code.doc-symbol-type_parameter::after {
content: "type-param";
}
code.doc-symbol-attribute,
a code.doc-symbol-attribute {
color: var(--doc-symbol-attribute-fg-color);
background-color: var(--doc-symbol-attribute-bg-color);
}
code.doc-symbol-attribute::after {
content: "attr";
}
code.doc-symbol-function,
a code.doc-symbol-function {
color: var(--doc-symbol-function-fg-color);
background-color: var(--doc-symbol-function-bg-color);
}
code.doc-symbol-function::after {
content: "func";
}
code.doc-symbol-method,
a code.doc-symbol-method {
color: var(--doc-symbol-method-fg-color);
background-color: var(--doc-symbol-method-bg-color);
}
code.doc-symbol-method::after {
content: "meth";
}
code.doc-symbol-class,
a code.doc-symbol-class {
color: var(--doc-symbol-class-fg-color);
background-color: var(--doc-symbol-class-bg-color);
}
code.doc-symbol-class::after {
content: "class";
}
code.doc-symbol-type_alias,
a code.doc-symbol-type_alias {
color: var(--doc-symbol-type_alias-fg-color);
background-color: var(--doc-symbol-type_alias-bg-color);
}
code.doc-symbol-type_alias::after {
content: "type";
}
code.doc-symbol-module,
a code.doc-symbol-module {
color: var(--doc-symbol-module-fg-color);
background-color: var(--doc-symbol-module-bg-color);
}
code.doc-symbol-module::after {
content: "mod";
}
.doc-signature .autorefs {
color: inherit;
border-bottom: 1px dotted currentcolor;
}
/* Source code blocks (admonitions). */
:root {
--md-admonition-icon--mkdocstrings-source: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.22 4.97a.75.75 0 0 1 1.06 0l6.5 6.5a.75.75 0 0 1 0 1.06l-6.5 6.5a.749.749 0 0 1-1.275-.326.75.75 0 0 1 .215-.734L21.19 12l-5.97-5.97a.75.75 0 0 1 0-1.06m-6.44 0a.75.75 0 0 1 0 1.06L2.81 12l5.97 5.97a.749.749 0 0 1-.326 1.275.75.75 0 0 1-.734-.215l-6.5-6.5a.75.75 0 0 1 0-1.06l6.5-6.5a.75.75 0 0 1 1.06 0"/></svg>')
}
.md-typeset .admonition.mkdocstrings-source,
.md-typeset details.mkdocstrings-source {
border: none;
padding: 0;
}
.md-typeset .admonition.mkdocstrings-source:focus-within,
.md-typeset details.mkdocstrings-source:focus-within {
box-shadow: none;
}
.md-typeset .mkdocstrings-source > .admonition-title,
.md-typeset .mkdocstrings-source > summary {
background-color: inherit;
}
.md-typeset .mkdocstrings-source > .admonition-title::before,
.md-typeset .mkdocstrings-source > summary::before {
background-color: var(--md-default-fg-color);
-webkit-mask-image: var(--md-admonition-icon--mkdocstrings-source);
mask-image: var(--md-admonition-icon--mkdocstrings-source);
}
+366
View File
@@ -0,0 +1,366 @@
html {
/* The nav header is 3.5rem high, plus 20px for the margin-top of the
main container. */
scroll-padding-top: calc(3.5rem + 20px);
}
/* Replacement for `body { background-attachment: fixed; }`, which has
performance issues when scrolling on large displays. See #1394. */
body::before {
content: ' ';
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: var(--bs-body-bg);
background: url(../img/grid.png) repeat-x;
will-change: transform;
z-index: -1;
}
body > .container {
margin-top: 20px;
min-height: 400px;
}
.navbar.fixed-top {
position: -webkit-sticky;
position: sticky;
}
.source-links {
float: right;
}
.col-md-9 img {
max-width: 100%;
display: inline-block;
padding: 4px;
line-height: 1.428571429;
background-color: var(--bs-secondary-bg-subtle);
border: 1px solid var(--bs-secondary-border-subtle);
border-radius: 4px;
margin: 20px auto 30px auto;
}
h1 {
color: inherit;
font-weight: 400;
font-size: 42px;
}
h2, h3, h4, h5, h6 {
color: inherit;
font-weight: 300;
}
hr {
border-top: 1px solid #aaa;
opacity: 1;
}
pre, .rst-content tt {
max-width: 100%;
background-color: var(--bs-body-bg);
border: solid 1px var(--bs-border-color);
color: var(--bs-body-color);
overflow-x: auto;
}
code.code-large, .rst-content tt.code-large {
font-size: 90%;
}
code {
padding: 2px 5px;
background-color: rgba(var(--bs-body-bg-rgb), 0.75);
border: solid 1px var(--bs-border-color);
color: var(--bs-body-color);
white-space: pre-wrap;
word-wrap: break-word;
}
pre code {
display: block;
border: none;
white-space: pre;
word-wrap: normal;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
}
kbd {
padding: 2px 4px;
font-size: 90%;
color: var(--bs-secondary-text-emphasis);
background-color: var(--bs-secondary-bg-subtle);
border-radius: 3px;
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);
box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);
}
a code {
color: inherit;
}
a:hover code, a:focus code {
color: inherit;
}
footer {
margin-top: 30px;
margin-bottom: 10px;
text-align: center;
font-weight: 200;
}
.modal-dialog {
margin-top: 60px;
}
/*
* Side navigation
*
* Scrollspy and affixed enhanced navigation to highlight sections and secondary
* sections of docs content.
*/
.bs-sidebar.affix {
position: -webkit-sticky;
position: sticky;
/* The nav header is 3.5rem high, plus 20px for the margin-top of the
main container. */
top: calc(3.5rem + 20px);
}
.bs-sidebar.card {
padding: 0;
max-height: 90%;
overflow-y: auto;
}
/* Toggle (vertically flip) sidebar collapse icon */
.bs-sidebar .navbar-toggler span {
-moz-transform: scale(1, -1);
-webkit-transform: scale(1, -1);
-o-transform: scale(1, -1);
-ms-transform: scale(1, -1);
transform: scale(1, -1);
}
.bs-sidebar .navbar-toggler.collapsed span {
-moz-transform: scale(1, 1);
-webkit-transform: scale(1, 1);
-o-transform: scale(1, 1);
-ms-transform: scale(1, 1);
transform: scale(1, 1);
}
/* First level of nav */
.bs-sidebar > .navbar-collapse > .nav {
padding-top: 10px;
padding-bottom: 10px;
border-radius: 5px;
width: 100%;
}
/* All levels of nav */
.bs-sidebar .nav > li > a {
display: block;
padding: 5px 20px;
z-index: 1;
}
.bs-sidebar .nav > li > a:hover,
.bs-sidebar .nav > li > a:focus {
text-decoration: none;
border-right: 1px solid;
}
.bs-sidebar .nav > li > a.active,
.bs-sidebar .nav > li > a.active:hover,
.bs-sidebar .nav > li > a.active:focus {
font-weight: bold;
background-color: transparent;
border-right: 1px solid;
}
.bs-sidebar .nav .nav .nav {
margin-left: 1em;
}
.bs-sidebar .nav > li > a {
font-weight: bold;
}
.bs-sidebar .nav .nav > li > a {
font-weight: normal;
}
.headerlink {
font-family: FontAwesome;
font-size: 14px;
display: none;
padding-left: .5em;
text-decoration: none;
vertical-align: middle;
}
h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink, h4:hover .headerlink, h5:hover .headerlink, h6:hover .headerlink {
display:inline-block;
}
blockquote {
padding-left: 10px;
border-left: 4px solid #e6e6e6;
}
.admonition, details {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
text-align: left;
}
.admonition.note, details.note {
color: var(--bs-primary-text-emphasis);
background-color: var(--bs-primary-bg-subtle);
border-color: var(--bs-primary-border-subtle);
}
.admonition.note h1, .admonition.note h2, .admonition.note h3,
.admonition.note h4, .admonition.note h5, .admonition.note h6,
details.note h1, details.note h2, details.note h3,
details.note h4, details.note h5, details.note h6 {
color: var(--bs-primary-text-emphasis);
}
.admonition.info, details.info {
color: var(--bs-info-text-emphasis);
background-color: var(--bs-info-bg-subtle);
border-color: var(--bs-info-border-subtle);
}
.admonition.info h1, .admonition.info h2, .admonition.info h3,
.admonition.info h4, .admonition.info h5, .admonition.info h6,
details.info h1, details.info h2, details.info h3,
details.info h4, details.info h5, details.info h6 {
color: var(--bs-info-text-emphasis);
}
.admonition.warning, details.warning {
color: var(--bs-warning-text-emphasis);
background-color: var(--bs-warning-bg-subtle);
border-color: var(--bs-warning-border-subtle);
}
.admonition.warning h1, .admonition.warning h2, .admonition.warning h3,
.admonition.warning h4, .admonition.warning h5, .admonition.warning h6,
details.warning h1, details.warning h2, details.warning h3,
details.warning h4, details.warning h5, details.warning h6 {
color: var(--bs-warning-text-emphasis);
}
.admonition.danger, details.danger {
color: var(--bs-danger-text-emphasis);
background-color: var(--bs-danger-bg-subtle);
border-color: var(--bs-danger-border-subtle);
}
.admonition.danger h1, .admonition.danger h2, .admonition.danger h3,
.admonition.danger h4, .admonition.danger h5, .admonition.danger h6,
details.danger h1, details.danger h2, details.danger h3,
details.danger h4, details.danger h5, details.danger h6 {
color: var(--bs-danger-text-emphasis);
}
.admonition, details {
color: var(--bs-light-text-emphasis);
background-color: var(--bs-light-bg-subtle);
border-color: var(--bs-light-border-subtle);
}
.admonition h1, .admonition h2, .admonition h3,
.admonition h4, .admonition h5, .admonition h6,
details h1, details h2, details h3,
details h4, details h5, details h6 {
color: var(--bs-light-text-emphasis);
}
.admonition-title, summary {
font-weight: bold;
text-align: left;
}
.admonition>p:last-child, details>p:last-child {
margin-bottom: 0;
}
@media (max-width: 991.98px) {
.navbar-collapse.show {
overflow-y: auto;
max-height: calc(100vh - 3.5rem);
}
}
.dropdown-item.open {
color: var(--bs-dropdown-link-active-color);
background-color: var(--bs-dropdown-link-active-bg);
}
.dropdown-submenu > .dropdown-menu {
margin: 0 0 0 1.5rem;
padding: 0;
border-width: 0;
}
.dropdown-submenu > a::after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 5px 0 5px 5px;
border-left-color: var(--bs-dropdown-link-active-color);
margin-top: 5px;
margin-right: -10px;
}
.dropdown-submenu:hover > a::after {
border-left-color: var(--bs-dropdown-link-active-color);
}
@media (min-width: 992px) {
.dropdown-menu {
overflow-y: auto;
max-height: calc(100vh - 3.5rem);
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
position: fixed !important;
margin-top: -9px;
margin-left: -2px;
border-width: 1px;
padding: 0.5rem 0;
}
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left > .dropdown-menu {
left: -100%;
margin-left: 10px;
}
}
@media print {
/* Remove sidebar when print */
.col-md-3 { display: none; }
}
+12
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
+6
View File
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
-3
View File
@@ -1,3 +0,0 @@
# CLI Module
::: mt5cli.cli
-229
View File
@@ -1,229 +0,0 @@
# History Collection (SQLite)
::: mt5cli.history
## `collect-history` schema
The `collect-history` command (and the matching `collect_history` SDK function) writes
selected MT5 datasets into one SQLite database. Each dataset becomes a table; column
names and types mirror the pdmt5 DataFrame schema for that export, with two additions:
- `symbol` is prepended on every table.
- `timeframe` is prepended on `rates` so appended runs at different bar sizes stay
distinguishable.
SQLite does not declare foreign keys. Rows are linked logically by `symbol`, time
windows, and (for deals) `position_id` / `order`. Duplicate rows are removed on
append using dataset-specific keys (for example `ticket` on history tables, or
`(symbol, timeframe, time)` on rates).
Optional views are created when `--with-views` is set and the `history-deals` dataset
was written.
### Entity-relationship diagram
Sample layout for a full collection with `--with-views`:
```mermaid
erDiagram
rates {
TEXT symbol "dedup key"
INTEGER timeframe "dedup key"
TEXT time "dedup key"
REAL open
REAL high
REAL low
REAL close
INTEGER tick_volume
INTEGER spread
INTEGER real_volume
}
ticks {
TEXT symbol "dedup key"
TEXT time "dedup key"
INTEGER time_msc "dedup key (preferred)"
REAL bid
REAL ask
REAL last
INTEGER volume
INTEGER flags
REAL volume_real
}
history_orders {
INTEGER ticket "dedup key"
TEXT symbol
TEXT time
INTEGER type
INTEGER state
REAL volume_initial
REAL price_open
REAL price_current
INTEGER magic
}
history_deals {
INTEGER ticket "dedup key"
INTEGER order
INTEGER position_id "groups position view"
TEXT symbol
TEXT time
INTEGER type "0/1 trade, else cash event"
INTEGER entry "0 IN, 1 OUT, 2 INOUT, 3 OUT_BY"
REAL volume
REAL price
REAL profit
REAL commission
REAL swap
REAL fee
}
cash_events {
INTEGER ticket
TEXT symbol
TEXT time
INTEGER type
REAL profit
}
positions_reconstructed {
INTEGER position_id
TEXT symbol
TEXT open_time
TEXT close_time
INTEGER direction
REAL volume_open
REAL volume_close
REAL volume_reversal
REAL open_price
REAL close_price
REAL total_profit
INTEGER reversal_count
INTEGER deals_count
}
rates ||--o{ history_deals : "symbol (logical)"
ticks ||--o{ history_deals : "symbol (logical)"
history_orders ||--o{ history_deals : "order ~ ticket (logical)"
history_deals ||--|| cash_events : "VIEW: type NOT IN (0,1)"
history_deals ||--o{ positions_reconstructed : "VIEW: GROUP BY position_id"
```
### Tables and views
| Object | Kind | Source | Notes |
| ------------------------- | ----- | -------------------- | ------------------------------------------------------------------------------------------- |
| `rates` | table | `copy_rates_range` | Indexed on `(symbol, timeframe, time)` when columns exist. |
| `ticks` | table | `copy_ticks_range` | Indexed on `(symbol, time)` when columns exist. |
| `history_orders` | table | `history_orders_get` | Fetched per `--symbol`, then concatenated. |
| `history_deals` | table | `history_deals_get` | Fetched per `--symbol`, then concatenated. Indexed on `(position_id, symbol)` when present. |
| `cash_events` | view | `history_deals` | Non-trade deal types (deposits, balance ops, etc.). Requires `type` column. |
| `positions_reconstructed` | view | `history_deals` | One row per closed `position_id`; volume-weighted prices and reversal stats. |
Column sets can vary with terminal and pdmt5 version. Views are skipped with a warning
when required columns are missing.
### Incremental collection
The `update_history` SDK path uses the same base tables and optional
`cash_events` / `positions_reconstructed` views. It additionally maintains
`rate_<symbol>__<timeframe>` compatibility views when `create_rate_views=True`.
### Rate view resolution
Downstream tools can resolve mt5cli-managed compatibility view names from an
existing SQLite history database without creating files or guessing naming
schemes:
```python
from pathlib import Path
from mt5cli.history import resolve_rate_view_name, resolve_rate_view_names
# Single symbol and granularity
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
# Batch resolution in row-major order
views = resolve_rate_view_names(
Path("history.db"),
["EURUSD", "GBPUSD"],
["M1", "H1"],
)
```
Resolution rules:
- Returns `rate_<symbol>__<timeframe>` when a symbol stores one timeframe.
- Returns `rate_<symbol>__<granularity>_<timeframe>` when multiple timeframes
are stored for the same symbol.
- When multiple naming candidates apply, prefers an existing managed
`rate_*__*` view from the candidate list.
- Falls back to single-timeframe naming when the database path is missing or
`rates` metadata is unavailable.
- Pass `require_existing=True` to raise `ValueError` instead of returning a
best-guess name when the database or view is missing.
- Accepts either a SQLite path or an open `sqlite3.Connection`.
### Rate data loading
Use `load_rate_data()` to load a table or view from a SQLite path, or
`load_rate_data_from_connection()` when you already have a connection:
```python
from pathlib import Path
from mt5cli import load_rate_data
from mt5cli.history import resolve_rate_view_name
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
rates = load_rate_data(Path("history.db"), view, count=1000)
```
The loader accepts close-based OHLC rate data or tick-like bid/ask data. It
validates that `time` exists, parses timestamps with pandas, and returns a
DataFrame indexed by ascending `DatetimeIndex` named `time`.
### Multi-series rate loading
For loading many rate series at once, build neutral `RateTarget` pairs and load
them from SQLite in one call. View names are resolved via the same
compatibility-view rules, or you can pass `explicit_tables` to bypass resolution:
```python
from pathlib import Path
from mt5cli import build_rate_targets, load_rate_series_from_sqlite
targets = build_rate_targets(["EURUSD", "GBPUSD"], ["M1", "H1"])
series = load_rate_series_from_sqlite(Path("history.db"), targets, count=1000)
frame = series["EURUSD", 1] # keyed by (symbol, integer timeframe)
```
- `build_rate_targets()` returns `RateTarget(symbol, timeframe)` pairs in
row-major order, normalizing timeframe names such as `"M1"` to their integer
values; set `allow_missing_symbol=True` to address series solely by
`explicit_tables` (targets carry `symbol=None`).
- `resolve_rate_tables()` maps targets to table or view names and validates that
any `explicit_tables` count matches the target count. Pass
`require_existing=True` to raise `ValueError` instead of returning a
best-guess name when the database or managed view is missing. When
`explicit_tables` is provided, names are returned as-is and
`require_existing` is ignored.
- `load_rate_series_from_sqlite()` returns a mapping keyed by
`(symbol, integer timeframe)`. Unless `explicit_tables` is supplied, it
requires existing managed `rate_*` compatibility views and raises
`ValueError` when they are missing. Duplicate `(symbol, timeframe)` targets
are rejected.
- `load_rate_series_by_granularity()` is a thin wrapper that builds the targets,
loads the series, and rekeys the result by granularity name to avoid
converting integer timeframes downstream:
```python
from mt5cli import load_rate_series_by_granularity
series = load_rate_series_by_granularity(
"history.db", ["EURUSD"], ["M1", "H1"], count=1000
)
frame = series["EURUSD", "M1"] # keyed by (symbol | None, granularity_name)
```
-125
View File
@@ -1,125 +0,0 @@
# API Reference
This section contains the complete API documentation for mt5cli.
## Modules
The mt5cli package consists of the following modules:
### [CLI](cli.md)
Command-line interface module providing typer-based commands for exporting MetaTrader 5 data to CSV, JSON, Parquet, and SQLite3 formats.
### [Utils](utils.md)
Utility module providing constants, enums, Click parameter types, and helper functions for parsing and exporting data.
### [SDK](sdk.md)
Programmatic SDK for read-only MetaTrader 5 data collection. Returns pandas DataFrames and provides `collect_history` for SQLite bulk collection.
### [Trading](trading.md)
Trading-capable session management and operational helpers built on `pdmt5.Mt5TradingClient`. Complements the read-only SDK without changing existing `Mt5CliClient` behavior.
### [History Collection (SQLite)](history.md)
SQLite storage helpers for the `collect-history` command schema, incremental updates, deduplication, indexes, and optional views.
## Architecture Overview
The package follows a simple architecture built on top of pdmt5:
1. **CLI Layer** (`cli.py`): Typer application with subcommands that delegate to the SDK and export results.
2. **SDK Layer** (`sdk.py`): Read-only data access functions, `Mt5CliClient`, and `collect_history` orchestration.
3. **Trading Layer** (`trading.py`): Trading-capable sessions and operational helpers on `Mt5TradingClient`.
4. **Utils Layer** (`utils.py`): Constants, enums, custom Click parameter types, parsing helpers, and format detection/export utilities.
5. **Data Layer** (via `pdmt5`): Uses `Mt5DataClient`, `Mt5TradingClient`, and `Mt5Config` from the pdmt5 package for MetaTrader 5 access.
## Usage Guidelines
All modules follow these conventions:
- **Type Safety**: All functions include comprehensive type hints
- **Error Handling**: User-friendly error messages via typer
- **Documentation**: Google-style docstrings with examples
- **Validation**: Custom Click parameter types for input validation
## Quick Start
```bash
# Export account information to CSV
mt5cli -o account.csv account-info
# Export EURUSD H1 rates to Parquet
mt5cli -o rates.parquet rates-from --symbol EURUSD --timeframe H1 \
--date-from 2024-01-01 --count 1000
# Export ticks to JSON
mt5cli -o ticks.json ticks-from --symbol EURUSD \
--date-from 2024-01-01 --count 500 --flags ALL
# Export to SQLite3 with custom table name
mt5cli -o data.db --table symbols symbols --group "*USD*"
```
## Python API
```python
from datetime import UTC, datetime
from pathlib import Path
from mt5cli import (
Dataset,
IfExists,
Mt5CliClient,
collect_history,
copy_rates_range,
detect_format,
export_dataframe,
export_dataframe_to_sqlite,
minimum_margins,
recent_ticks,
)
from mt5cli.history import resolve_rate_view_name
# Fetch rates programmatically
rates = copy_rates_range(
"EURUSD",
timeframe="H1",
date_from="2024-01-01",
date_to="2024-02-01",
)
# Detect output format from file extension
fmt = detect_format(Path("output.parquet")) # Returns "parquet"
# Export a DataFrame
export_dataframe(rates, Path("output.csv"), "csv")
# Append to SQLite with deduplication
export_dataframe_to_sqlite(
rates,
Path("history.db"),
"rates",
if_exists=IfExists.APPEND,
deduplicate_on=("symbol", "timeframe", "time"),
)
# Resolve rate compatibility views and fetch recent ticks
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
ticks = recent_ticks("EURUSD", seconds=300)
margins = minimum_margins("EURUSD")
# Collect history into SQLite
collect_history(
Path("history.db"),
symbols=["EURUSD"],
date_from=datetime(2024, 1, 1, tzinfo=UTC),
date_to=datetime(2024, 2, 1, tzinfo=UTC),
)
```
## Examples
See individual module pages for detailed usage examples and code samples.
-114
View File
@@ -1,114 +0,0 @@
# SDK Module
::: mt5cli.sdk
## Resilient multi-account orchestration
The SDK ships strategy-agnostic helpers for building long-running collectors on
top of the read-only client. None of them depend on a particular trading
application.
### Retrying transient rate collection
`collect_latest_rates_for_accounts_with_retries()` wraps
`collect_latest_rates_for_accounts()` with bounded exponential backoff. Only
`pdmt5.Mt5TradingError` and `pdmt5.Mt5RuntimeError` are retried; the final
failure is re-raised once `retry_count` is exhausted.
```python
from mt5cli import AccountSpec, collect_latest_rates_for_accounts_with_retries
accounts = [AccountSpec(symbols=["EURUSD"], login=12345)]
rates = collect_latest_rates_for_accounts_with_retries(
accounts,
["M1", "H1"],
count=500,
retry_count=3,
backoff_base=2, # sleeps 2s, 4s, 8s between attempts
)
```
### Latest closed rate bars
MetaTrader 5 `start_pos=0` includes the still-forming current bar as the last
row. `collect_latest_closed_rates_for_accounts()` fetches `count + 1` bars,
drops that row with `drop_forming_rate_bar()`, and validates each series is
non-empty. Use `collect_latest_closed_rates_by_granularity()` when callers
prefer keys such as `("EURUSD", "M1")` instead of integer timeframes.
```python
from mt5cli import AccountSpec, collect_latest_closed_rates_by_granularity
rates = collect_latest_closed_rates_by_granularity(
[AccountSpec(symbols=["EURUSD"], login=12345)],
["M1", "H1"],
count=500,
retry_count=3,
)
closed_m1 = rates["EURUSD", "M1"]
```
### Resolving credentials and `${ENV_VAR}` placeholders
`resolve_account_spec()` / `resolve_account_specs()` merge explicit override
values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders, keeping
secrets out of plan/config files. A missing environment variable raises
`ValueError`.
```python
import os
from mt5cli import AccountSpec, resolve_account_specs
os.environ["MT5_LOGIN"] = "12345"
os.environ["MT5_PASSWORD"] = "secret"
accounts = [
AccountSpec(symbols=["EURUSD"], login="${MT5_LOGIN}", password="${MT5_PASSWORD}")
]
resolved = resolve_account_specs(accounts, server="Broker-Demo")
# resolved[0].login == "12345", resolved[0].server == "Broker-Demo"
```
### Throttled incremental history updates
`ThrottledHistoryUpdater` wraps `update_history()` with a minimum interval
between successful runs (using a monotonic clock), so an application loop can
call it every iteration without over-fetching.
```python
from pdmt5 import Mt5Config, Mt5DataClient
from mt5cli import Dataset, ThrottledHistoryUpdater
updater = ThrottledHistoryUpdater(
output="history.db",
datasets={Dataset.rates},
timeframes=["M1"],
interval_seconds=60, # <= 0 updates on every call
)
client = Mt5DataClient(config=Mt5Config(login=12345))
client.initialize_and_login_mt5()
try:
while True:
updater.update(client, ["EURUSD", "GBPUSD"]) # no-op until 60s elapse
# ... do other work; break when shutting down ...
finally:
client.shutdown()
```
By default recoverable errors (`Mt5TradingError`, `Mt5RuntimeError`,
`sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability
`AttributeError` / `TypeError` for history API methods) propagate so the caller
controls logging; pass `suppress_errors=True` to swallow them and return
`False` without advancing the throttle. Other `AttributeError` / `TypeError`
values always propagate. Input validation (`_resolve_update_history_request`)
runs before any MT5 or SQLite calls, but when `suppress_errors=True` the
resulting `ValueError` is suppressed along with other recoverable errors.
## Trading-capable sessions
For order placement and trading calculations, use the dedicated
[Trading module](trading.md). The read-only `Mt5CliClient` and `mt5_session()`
helpers in this module are unchanged.
-70
View File
@@ -1,70 +0,0 @@
# Trading Module
::: mt5cli.trading
## Trading-capable MT5 sessions
`mt5_trading_session()` complements the read-only `mt5_session()` helper in
`sdk.py`. It yields a connected `pdmt5.Mt5TradingClient`, uses
`Mt5Config.path` to launch the terminal when configured, and always calls
`shutdown()` on exit.
```python
from pdmt5 import Mt5Config
from mt5cli import mt5_trading_session
with mt5_trading_session(
Mt5Config(path=r"C:\Program Files\MetaTrader 5\terminal64.exe", login=12345),
retry_count=2,
) as client:
positions = client.positions_get_as_df(symbol="EURUSD")
```
The read-only `Mt5CliClient` / `mt5_session()` API is unchanged.
## Operational trading helpers
These helpers are strategy-agnostic and do not depend on signal detection,
betting logic, or scheduling code in downstream applications.
```python
from mt5cli import (
calculate_margin_and_volume,
detect_position_side,
determine_order_limits,
)
side = detect_position_side(client, "EURUSD")
sizing = calculate_margin_and_volume(
client,
"EURUSD",
unit_margin_ratio=0.5,
preserved_margin_ratio=0.2,
)
limits = determine_order_limits(
client,
"EURUSD",
side="long",
stop_loss_limit_ratio=0.01,
take_profit_limit_ratio=0.02,
)
```
Protective ratios must satisfy `0 <= ratio < 1`; `0` omits that level.
`calculate_margin_and_volume()` clamps negative `margin_free` to `0.0`
before sizing.
## Migration from mteor-local helpers
| mteor-local concern | mt5cli replacement |
| -------------------------------------------------------- | ----------------------------------------------- |
| Manual terminal spawn/kill around trading code | `mt5_trading_session()` |
| Local position-side detection | `detect_position_side()` |
| Local margin/volume sizing | `calculate_margin_and_volume()` |
| Local SL/TP price derivation | `determine_order_limits()` |
| Throttled SQLite history loop with ad-hoc error handling | `ThrottledHistoryUpdater(suppress_errors=True)` |
Keep read-only data collection on `mt5_session()` / `Mt5CliClient`; use
`mt5_trading_session()` only where order placement or trading calculations are
required.
-3
View File
@@ -1,3 +0,0 @@
# Utils Module
::: mt5cli.utils
-231
View File
@@ -1,231 +0,0 @@
# mt5cli
Command-line tool for MetaTrader 5 data export.
## Overview
mt5cli is a CLI application that exports MetaTrader 5 trading data to multiple file formats. It is built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
## Architecture
- **pdmt5** — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (`TIMEFRAME_*`, `COPY_TICKS_*`, order types).
- **mt5cli** — CLI commands, CSV/JSON/Parquet/SQLite export, SQLite history collection, rate views, and local batch/automation SDK helpers built on pdmt5.
- **mt5api** — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.
## Features
- **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
- **Auto-detection**: Format detection from file extensions
- **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
- **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
- **Connection management**: Optional credentials, server, and timeout configuration
- **SQLite rate loading**: Load mt5cli-managed rate tables/views for offline workflows
## Installation
```bash
pip install mt5cli
```
## Programmatic usage / SDK usage
mt5cli can be used as a small Python SDK for read-only MetaTrader 5 data collection. SDK functions return pandas DataFrames without writing files. Use `export_dataframe` or `export_dataframe_to_sqlite` when you need to persist results.
```python
from datetime import UTC, datetime
from pathlib import Path
from mt5cli import (
Mt5CliClient,
collect_history,
copy_rates_range,
export_dataframe,
export_dataframe_to_sqlite,
load_rate_data,
minimum_margins,
recent_ticks,
)
from mt5cli.history import resolve_rate_view_name
# One-off fetch with module-level helpers
rates = copy_rates_range(
"EURUSD",
timeframe="H1",
date_from="2024-01-01",
date_to="2024-02-01",
)
export_dataframe(rates, Path("rates.csv"), "csv")
# Resolve SQLite rate compatibility views for downstream tools
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
offline_rates = load_rate_data(Path("history.db"), view, count=1000)
# Recent tick window and minimum margin summary
ticks = recent_ticks("EURUSD", seconds=300)
margins = minimum_margins("EURUSD")
# Reuse one MT5 connection for multiple calls
with Mt5CliClient(login=12345, password="secret", server="Broker-Demo") as client:
account = client.account_info()
positions = client.positions()
latest = client.latest_rates("EURUSD", "M1", count=100)
summary = client.mt5_summary()
summary_table = client.mt5_summary_as_df()
# Bulk SQLite collection (same behavior as the collect-history CLI command)
collect_history(
Path("history.db"),
symbols=["EURUSD", "GBPUSD"],
date_from=datetime(2024, 1, 1, tzinfo=UTC),
date_to=datetime(2024, 2, 1, tzinfo=UTC),
timeframe="M1",
flags="ALL",
with_views=True,
)
```
Timeframes, tick flags, and ISO 8601 date strings are accepted wherever noted in the SDK API.
`Mt5CliClient.mt5_summary()` returns the SDK structured form as plain nested Python values. Use `Mt5CliClient.mt5_summary_as_df()` when you need a one-row DataFrame for export. The `mt5-summary` CLI command uses this tabular form, so nested terminal/account fields are JSON-encoded strings that are safe for CSV, JSON, Parquet, and SQLite output.
## Quick Start
```bash
# Export account information to CSV
mt5cli -o account.csv account-info
# Export EURUSD M1 rates to Parquet
mt5cli -o rates.parquet rates-from --symbol EURUSD --timeframe M1 \
--date-from 2024-01-01 --count 1000
# Export ticks to JSON
mt5cli -o ticks.json ticks-from --symbol EURUSD \
--date-from 2024-01-01 --count 500 --flags ALL
# Export symbols to SQLite3 with custom table name
mt5cli -o data.db --table symbols symbols --group "*USD*"
# Export with connection credentials
mt5cli --login 12345 --password mypass --server MyBroker-Demo \
-o positions.csv positions
```
## Commands
### Rates
| Command | Description |
| ---------------- | ---------------------------------- |
| `rates-from` | Export rates from a start date |
| `rates-from-pos` | Export rates from a start position |
| `latest-rates` | Export latest rates |
| `rates-range` | Export rates for a date range |
### Ticks
| Command | Description |
| -------------- | ----------------------------------- |
| `ticks-from` | Export ticks from a start date |
| `ticks-range` | Export ticks for a date range |
| `ticks-recent` | Export ticks from a trailing window |
### Information
| Command | Description |
| ------------------ | --------------------------------------- |
| `account-info` | Export account information |
| `terminal-info` | Export terminal information |
| `version` | Export MetaTrader 5 version information |
| `last-error` | Export the last error information |
| `symbols` | Export symbol list |
| `symbol-info` | Export symbol details |
| `symbol-info-tick` | Export the last tick for a symbol |
| `minimum-margins` | Export minimum-volume margin summary |
| `market-book` | Export market depth (order book) |
### Trading
| Command | Description |
| ---------------------- | ----------------------------------------------------------- |
| `orders` | Export active orders |
| `positions` | Export open positions |
| `history-orders` | Export historical orders |
| `history-deals` | Export historical deals |
| `recent-history-deals` | Export historical deals from a trailing window |
| `mt5-summary` | Export terminal/account status summary |
| `order-check` | Check funds sufficiency for a trade request |
| `order-send` | Send a trade request to the trade server (`--yes` required) |
Use `order-check` to validate a request payload before running `order-send --yes`.
### Bulk Collection
| Command | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `collect-history` | Collect rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database (optional cash-event/position views) |
```bash
mt5cli -o history.db collect-history \
--symbol EURUSD --symbol GBPUSD \
--date-from 2024-01-01 --date-to 2024-02-01 \
--dataset rates --dataset history-deals \
--timeframe M1 --flags ALL --if-exists append --with-views
```
`collect-history` options:
| Option | Default | Description |
| -------------- | ---------- | --------------------------------------------------------------------------------------------- |
| `--symbol/-s` | _required_ | Symbol to collect (repeat for multiple). |
| `--date-from` | _required_ | Start date in ISO 8601. |
| `--date-to` | _required_ | End date in ISO 8601. |
| `--dataset` | all four | Repeatable: `rates`, `ticks`, `history-orders`, `history-deals`. |
| `--timeframe` | `M1` | Rates timeframe; recorded in a `timeframe` column on the `rates` table. |
| `--flags` | `ALL` | Tick copy flags forwarded to `copy_ticks_range`. |
| `--if-exists` | `fail` | `append`, `replace`, or `fail` when a target table already exists. |
| `--with-views` | off | Add `cash_events` and `positions_reconstructed` views (requires the `history-deals` dataset). |
History orders and deals are fetched per symbol and concatenated, so the symbol filter is applied consistently across all datasets. The `cash_events` view is derived from symbol-filtered `history_deals`, so account-level cash events with empty or non-matching symbols may be excluded. The `positions_reconstructed` view excludes positions with no closing deal, uses volume-weighted open/close prices, and reports reversal deals (`DEAL_ENTRY_INOUT`) via `volume_reversal` / `reversal_count`.
See the [History schema diagram](api/history.md#entity-relationship-diagram) for a sample ER layout of the resulting database.
## Global Options
| Option | Description |
| -------------- | ------------------------------------------------------- |
| `-o, --output` | Output file path (required) |
| `-f, --format` | Output format (auto-detected from extension if omitted) |
| `--table` | Table name for SQLite3 output (default: "data") |
| `--login` | Trading account login |
| `--password` | Trading account password |
| `--server` | Trading server name |
| `--path` | Path to MetaTrader5 terminal EXE file |
| `--timeout` | Connection timeout in milliseconds |
| `--log-level` | Logging level (DEBUG, INFO, WARNING, ERROR) |
## Requirements
- Python 3.11+
- Windows OS (MetaTrader 5 requirement)
- MetaTrader 5 platform
## API Reference
Browse the API documentation for detailed module information:
- [CLI Module](api/cli.md) - CLI application with export commands
- [SDK Module](api/sdk.md) - Programmatic read-only data collection API
- [Utils Module](api/utils.md) - Constants, parameter types, parsers, and export utilities
## Development
This project follows strict code quality standards:
- Type hints required (strict mode)
- Comprehensive linting with Ruff
- Test coverage tracking
- Google-style docstrings
## License
MIT License - see [LICENSE](https://github.com/dceoy/mt5cli/blob/main/LICENSE) file for details.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+686
View File
@@ -0,0 +1,686 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Generic MT5 data and execution infrastructure for Python">
<meta name="author" content="dceoy">
<link rel="canonical" href="https://github.com/dceoy/mt5cli/">
<link rel="shortcut icon" href="img/favicon.ico">
<title>mt5cli API Documentation</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/brands.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet">
<link href="css/v4-font-face.min.css" rel="stylesheet">
<link href="css/base.css" rel="stylesheet">
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" >
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" disabled>
<link href="assets/_mkdocstrings.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body class="homepage">
<div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href=".">mt5cli API Documentation</a>
<!-- Expander button -->
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Expanded navigation -->
<div id="navbar-collapse" class="navbar-collapse collapse">
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li class="nav-item">
<a href="." class="nav-link active" aria-current="page">Home</a>
</li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">API Reference</a>
<ul class="dropdown-menu">
<li>
<a href="api/" class="dropdown-item">Overview</a>
</li>
<li>
<a href="api/public-contract/" class="dropdown-item">Public API Contract</a>
</li>
<li>
<a href="api/client/" class="dropdown-item">Client</a>
</li>
<li>
<a href="api/schemas/" class="dropdown-item">Schemas</a>
</li>
<li>
<a href="api/converters/" class="dropdown-item">Converters</a>
</li>
<li>
<a href="api/exceptions/" class="dropdown-item">Exceptions</a>
</li>
<li>
<a href="api/cli/" class="dropdown-item">CLI</a>
</li>
<li>
<a href="api/sdk/" class="dropdown-item">SDK</a>
</li>
<li>
<a href="api/trading/" class="dropdown-item">Trading</a>
</li>
<li>
<a href="api/history/" class="dropdown-item">History Collection (SQLite)</a>
</li>
<li>
<a href="api/telemetry/" class="dropdown-item">Telemetry</a>
</li>
<li>
<a href="api/grafana/" class="dropdown-item">Grafana</a>
</li>
<li>
<a href="api/utils/" class="dropdown-item">Utils</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav ms-md-auto">
<li class="nav-item">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#mkdocs_search_modal">
<i class="fa fa-search"></i> Search
</a>
</li>
<li class="nav-item">
<a rel="prev" class="nav-link disabled">
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li class="nav-item">
<a rel="next" href="api/" class="nav-link">
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
<li class="nav-item">
<a href="https://github.com/dceoy/mt5cli/edit/master/docs/index.md" class="nav-link">Edit on dceoy/mt5cli
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"><div class="navbar-expand-md bs-sidebar hidden-print affix" role="complementary">
<div class="navbar-header">
<button type="button" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#toc-collapse" title="Table of Contents">
<span class="fa fa-angle-down"></span>
</button>
</div>
<div id="toc-collapse" class="navbar-collapse collapse card bg-body-tertiary">
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="1"><a href="#mt5cli" class="nav-link">mt5cli</a>
<ul class="nav flex-column">
<li class="nav-item" data-bs-level="2"><a href="#overview" class="nav-link">Overview</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#architecture" class="nav-link">Architecture</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#features" class="nav-link">Features</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#installation" class="nav-link">Installation</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#python-api-for-downstream-packages" class="nav-link">Python API for downstream packages</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#quick-start" class="nav-link">Quick Start</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#commands" class="nav-link">Commands</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#global-options" class="nav-link">Global Options</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#requirements" class="nav-link">Requirements</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#api-reference" class="nav-link">API Reference</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#development" class="nav-link">Development</a>
<ul class="nav flex-column">
</ul>
</li>
<li class="nav-item" data-bs-level="2"><a href="#license" class="nav-link">License</a>
<ul class="nav flex-column">
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div></div>
<div class="col-md-9" role="main">
<h1 id="mt5cli">mt5cli<a class="headerlink" href="#mt5cli" title="Permanent link">&para;</a></h1>
<p>Generic MT5 data and execution infrastructure for Python applications.</p>
<h2 id="overview">Overview<a class="headerlink" href="#overview" title="Permanent link">&para;</a></h2>
<p>mt5cli provides a stable <code>MT5Client</code> Python API, standardized dataset schemas, storage helpers, and a CLI for exporting MetaTrader 5 data. It is built on top of <a href="https://github.com/dceoy/pdmt5">pdmt5</a>, a pandas-based data handler for MetaTrader 5.</p>
<h2 id="architecture">Architecture<a class="headerlink" href="#architecture" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>pdmt5</strong> — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (<code>TIMEFRAME_*</code>, <code>COPY_TICKS_*</code>, order types).</li>
<li><strong>mt5cli</strong> — public <code>MT5Client</code> API, schema contracts, storage helpers, CLI commands, and SQLite history collection built on pdmt5.</li>
<li><strong>mt5api</strong> — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.</li>
</ul>
<h2 id="features">Features<a class="headerlink" href="#features" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Multi-format export</strong>: CSV, JSON, Parquet, and SQLite3 output formats</li>
<li><strong>Auto-detection</strong>: Format detection from file extensions</li>
<li><strong>Comprehensive data access</strong>: Rates, ticks, account info, symbols, orders, positions, and trading history</li>
<li><strong>Flexible timeframes</strong>: Named timeframes (M1, H1, D1, etc.) and numeric values</li>
<li><strong>Connection management</strong>: Optional credentials, server, and timeout configuration</li>
<li><strong>SQLite rate loading</strong>: Load mt5cli-managed rate tables/views for offline workflows</li>
</ul>
<h2 id="installation">Installation<a class="headerlink" href="#installation" title="Permanent link">&para;</a></h2>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>pip<span class="w"> </span>install<span class="w"> </span>mt5cli
</code></pre></div>
<p>Parquet export is not included by default. To enable it, install the <code>parquet</code> extra:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a>pip<span class="w"> </span>install<span class="w"> </span><span class="s2">&quot;mt5cli[parquet]&quot;</span>
</code></pre></div>
<h2 id="python-api-for-downstream-packages">Python API for downstream packages<a class="headerlink" href="#python-api-for-downstream-packages" title="Permanent link">&para;</a></h2>
<p>Import <code>MT5Client</code> for generic MT5 data access, schema normalization, and optional order primitives.</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">UTC</span><span class="p">,</span> <span class="n">datetime</span>
<a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">pathlib</span><span class="w"> </span><span class="kn">import</span> <span class="n">Path</span>
<a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a>
<a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
<a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a> <span class="n">MT5Client</span><span class="p">,</span>
<a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a> <span class="n">build_config</span><span class="p">,</span>
<a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a> <span class="n">collect_history</span><span class="p">,</span>
<a id="__codelineno-2-8" name="__codelineno-2-8" href="#__codelineno-2-8"></a> <span class="n">mt5_session</span><span class="p">,</span>
<a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a><span class="p">)</span>
<a id="__codelineno-2-10" name="__codelineno-2-10" href="#__codelineno-2-10"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli.history</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_rate_data</span><span class="p">,</span> <span class="n">resolve_rate_view_name</span>
<a id="__codelineno-2-11" name="__codelineno-2-11" href="#__codelineno-2-11"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli.schemas</span><span class="w"> </span><span class="kn">import</span> <span class="n">DataKind</span><span class="p">,</span> <span class="n">normalize_dataframe</span>
<a id="__codelineno-2-12" name="__codelineno-2-12" href="#__codelineno-2-12"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli.sdk</span><span class="w"> </span><span class="kn">import</span> <span class="n">minimum_margins</span><span class="p">,</span> <span class="n">recent_ticks</span>
<a id="__codelineno-2-13" name="__codelineno-2-13" href="#__codelineno-2-13"></a><span class="kn">from</span><span class="w"> </span><span class="nn">mt5cli.utils</span><span class="w"> </span><span class="kn">import</span> <span class="n">Dataset</span><span class="p">,</span> <span class="n">export_dataframe</span>
<a id="__codelineno-2-14" name="__codelineno-2-14" href="#__codelineno-2-14"></a>
<a id="__codelineno-2-15" name="__codelineno-2-15" href="#__codelineno-2-15"></a><span class="c1"># Persistent session for multiple calls</span>
<a id="__codelineno-2-16" name="__codelineno-2-16" href="#__codelineno-2-16"></a><span class="k">with</span> <span class="n">mt5_session</span><span class="p">(</span><span class="n">build_config</span><span class="p">(</span><span class="n">login</span><span class="o">=</span><span class="mi">12345</span><span class="p">,</span> <span class="n">server</span><span class="o">=</span><span class="s2">&quot;Broker-Demo&quot;</span><span class="p">))</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
<a id="__codelineno-2-17" name="__codelineno-2-17" href="#__codelineno-2-17"></a> <span class="n">rates</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">copy_rates_range</span><span class="p">(</span>
<a id="__codelineno-2-18" name="__codelineno-2-18" href="#__codelineno-2-18"></a> <span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span>
<a id="__codelineno-2-19" name="__codelineno-2-19" href="#__codelineno-2-19"></a> <span class="n">timeframe</span><span class="o">=</span><span class="s2">&quot;H1&quot;</span><span class="p">,</span>
<a id="__codelineno-2-20" name="__codelineno-2-20" href="#__codelineno-2-20"></a> <span class="n">date_from</span><span class="o">=</span><span class="s2">&quot;2024-01-01&quot;</span><span class="p">,</span>
<a id="__codelineno-2-21" name="__codelineno-2-21" href="#__codelineno-2-21"></a> <span class="n">date_to</span><span class="o">=</span><span class="s2">&quot;2024-02-01&quot;</span><span class="p">,</span>
<a id="__codelineno-2-22" name="__codelineno-2-22" href="#__codelineno-2-22"></a> <span class="p">)</span>
<a id="__codelineno-2-23" name="__codelineno-2-23" href="#__codelineno-2-23"></a> <span class="n">positions</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">positions</span><span class="p">()</span>
<a id="__codelineno-2-24" name="__codelineno-2-24" href="#__codelineno-2-24"></a> <span class="n">check</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">order_check</span><span class="p">({</span><span class="s2">&quot;action&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&quot;symbol&quot;</span><span class="p">:</span> <span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="s2">&quot;volume&quot;</span><span class="p">:</span> <span class="mf">0.1</span><span class="p">})</span>
<a id="__codelineno-2-25" name="__codelineno-2-25" href="#__codelineno-2-25"></a>
<a id="__codelineno-2-26" name="__codelineno-2-26" href="#__codelineno-2-26"></a><span class="c1"># Normalize MT5 frames to the public schema contract before storage</span>
<a id="__codelineno-2-27" name="__codelineno-2-27" href="#__codelineno-2-27"></a><span class="n">closed_rates</span> <span class="o">=</span> <span class="n">normalize_dataframe</span><span class="p">(</span>
<a id="__codelineno-2-28" name="__codelineno-2-28" href="#__codelineno-2-28"></a> <span class="n">rates</span><span class="p">,</span> <span class="n">DataKind</span><span class="o">.</span><span class="n">rates</span><span class="p">,</span> <span class="n">symbol</span><span class="o">=</span><span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="n">timeframe</span><span class="o">=</span><span class="s2">&quot;H1&quot;</span>
<a id="__codelineno-2-29" name="__codelineno-2-29" href="#__codelineno-2-29"></a><span class="p">)</span>
<a id="__codelineno-2-30" name="__codelineno-2-30" href="#__codelineno-2-30"></a><span class="n">export_dataframe</span><span class="p">(</span><span class="n">closed_rates</span><span class="p">,</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&quot;rates.csv&quot;</span><span class="p">),</span> <span class="s2">&quot;csv&quot;</span><span class="p">)</span>
<a id="__codelineno-2-31" name="__codelineno-2-31" href="#__codelineno-2-31"></a>
<a id="__codelineno-2-32" name="__codelineno-2-32" href="#__codelineno-2-32"></a><span class="c1"># Offline rate loading from mt5cli-managed SQLite history</span>
<a id="__codelineno-2-33" name="__codelineno-2-33" href="#__codelineno-2-33"></a><span class="n">view</span> <span class="o">=</span> <span class="n">resolve_rate_view_name</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="s2">&quot;history.db&quot;</span><span class="p">),</span> <span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="s2">&quot;M1&quot;</span><span class="p">,</span> <span class="n">require_existing</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<a id="__codelineno-2-34" name="__codelineno-2-34" href="#__codelineno-2-34"></a><span class="n">offline_rates</span> <span class="o">=</span> <span class="n">load_rate_data</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="s2">&quot;history.db&quot;</span><span class="p">),</span> <span class="n">view</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
<a id="__codelineno-2-35" name="__codelineno-2-35" href="#__codelineno-2-35"></a>
<a id="__codelineno-2-36" name="__codelineno-2-36" href="#__codelineno-2-36"></a><span class="c1"># One-off helpers still work without instantiating a client</span>
<a id="__codelineno-2-37" name="__codelineno-2-37" href="#__codelineno-2-37"></a><span class="n">ticks</span> <span class="o">=</span> <span class="n">recent_ticks</span><span class="p">(</span><span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="n">seconds</span><span class="o">=</span><span class="mi">300</span><span class="p">)</span>
<a id="__codelineno-2-38" name="__codelineno-2-38" href="#__codelineno-2-38"></a><span class="n">margins</span> <span class="o">=</span> <span class="n">minimum_margins</span><span class="p">(</span><span class="s2">&quot;EURUSD&quot;</span><span class="p">)</span>
<a id="__codelineno-2-39" name="__codelineno-2-39" href="#__codelineno-2-39"></a>
<a id="__codelineno-2-40" name="__codelineno-2-40" href="#__codelineno-2-40"></a><span class="n">collect_history</span><span class="p">(</span>
<a id="__codelineno-2-41" name="__codelineno-2-41" href="#__codelineno-2-41"></a> <span class="n">Path</span><span class="p">(</span><span class="s2">&quot;history.db&quot;</span><span class="p">),</span>
<a id="__codelineno-2-42" name="__codelineno-2-42" href="#__codelineno-2-42"></a> <span class="n">symbols</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;EURUSD&quot;</span><span class="p">,</span> <span class="s2">&quot;GBPUSD&quot;</span><span class="p">],</span>
<a id="__codelineno-2-43" name="__codelineno-2-43" href="#__codelineno-2-43"></a> <span class="n">date_from</span><span class="o">=</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2024</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">tzinfo</span><span class="o">=</span><span class="n">UTC</span><span class="p">),</span>
<a id="__codelineno-2-44" name="__codelineno-2-44" href="#__codelineno-2-44"></a> <span class="n">date_to</span><span class="o">=</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2024</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">tzinfo</span><span class="o">=</span><span class="n">UTC</span><span class="p">),</span>
<a id="__codelineno-2-45" name="__codelineno-2-45" href="#__codelineno-2-45"></a> <span class="n">datasets</span><span class="o">=</span><span class="p">{</span><span class="n">Dataset</span><span class="o">.</span><span class="n">rates</span><span class="p">,</span> <span class="n">Dataset</span><span class="o">.</span><span class="n">history_deals</span><span class="p">},</span>
<a id="__codelineno-2-46" name="__codelineno-2-46" href="#__codelineno-2-46"></a><span class="p">)</span>
</code></pre></div>
<p>Schema contracts live in <code>mt5cli.schemas</code> (<code>DataKind</code>, <code>validate_schema</code>, <code>normalize_dataframe</code>). Export and storage helpers are in <code>mt5cli.utils</code> (<code>Dataset</code>, <code>export_dataframe</code>) and <code>mt5cli.history</code>.</p>
<p><code>MT5Client.order_send()</code> is a live execution primitive: it can place real trades on the connected account. mt5cli does not implement strategy logic, signal generation, backtesting, or optimization — downstream applications must gate live execution explicitly (the CLI requires <code>--yes</code> for <code>order-send</code>).</p>
<p><code>MT5Client.mt5_summary()</code> returns structured nested Python values. Use <code>MT5Client.mt5_summary_as_df()</code> when you need a one-row DataFrame for export.</p>
<h2 id="quick-start">Quick Start<a class="headerlink" href="#quick-start" title="Permanent link">&para;</a></h2>
<div class="highlight"><pre><span></span><code><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="c1"># Export account information to CSV</span>
<a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>account.csv<span class="w"> </span>account-info
<a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a>
<a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a><span class="c1"># Export EURUSD M1 rates to Parquet</span>
<a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>rates.parquet<span class="w"> </span>rates-from<span class="w"> </span>--symbol<span class="w"> </span>EURUSD<span class="w"> </span>--timeframe<span class="w"> </span>M1<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a><span class="w"> </span>--date-from<span class="w"> </span><span class="m">2024</span>-01-01<span class="w"> </span>--count<span class="w"> </span><span class="m">1000</span>
<a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a>
<a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a><span class="c1"># Export ticks to JSON</span>
<a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>ticks.json<span class="w"> </span>ticks-from<span class="w"> </span>--symbol<span class="w"> </span>EURUSD<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="w"> </span>--date-from<span class="w"> </span><span class="m">2024</span>-01-01<span class="w"> </span>--count<span class="w"> </span><span class="m">500</span><span class="w"> </span>--flags<span class="w"> </span>ALL
<a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a>
<a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a><span class="c1"># Export symbols to SQLite3 with custom table name</span>
<a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>data.db<span class="w"> </span>--table<span class="w"> </span>symbols<span class="w"> </span>symbols<span class="w"> </span>--group<span class="w"> </span><span class="s2">&quot;*USD*&quot;</span>
<a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a>
<a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a><span class="c1"># Export with connection credentials from env or placeholders</span>
<a id="__codelineno-3-16" name="__codelineno-3-16" href="#__codelineno-3-16"></a><span class="nv">MT5_LOGIN</span><span class="o">=</span><span class="m">12345</span><span class="w"> </span><span class="nv">MT5_PASSWORD</span><span class="o">=</span>secret<span class="w"> </span><span class="nv">MT5_SERVER</span><span class="o">=</span>MyBroker-Demo<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-3-17" name="__codelineno-3-17" href="#__codelineno-3-17"></a><span class="w"> </span>mt5cli<span class="w"> </span>-o<span class="w"> </span>positions.csv<span class="w"> </span>positions
<a id="__codelineno-3-18" name="__codelineno-3-18" href="#__codelineno-3-18"></a>mt5cli<span class="w"> </span>--login<span class="w"> </span><span class="s1">&#39;${MT5_LOGIN}&#39;</span><span class="w"> </span>--password<span class="w"> </span><span class="s1">&#39;${MT5_PASSWORD}&#39;</span><span class="w"> </span>--server<span class="w"> </span><span class="s1">&#39;${MT5_SERVER}&#39;</span><span class="w"> </span><span class="se">\</span>
<a id="__codelineno-3-19" name="__codelineno-3-19" href="#__codelineno-3-19"></a><span class="w"> </span>-o<span class="w"> </span>positions.csv<span class="w"> </span>positions
</code></pre></div>
<h2 id="commands">Commands<a class="headerlink" href="#commands" title="Permanent link">&para;</a></h2>
<h3 id="rates">Rates<a class="headerlink" href="#rates" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rates-from</code></td>
<td>Export rates from a start date</td>
</tr>
<tr>
<td><code>rates-from-pos</code></td>
<td>Export rates from a start position</td>
</tr>
<tr>
<td><code>latest-rates</code></td>
<td>Export latest rates</td>
</tr>
<tr>
<td><code>rates-range</code></td>
<td>Export rates for a date range</td>
</tr>
</tbody>
</table>
<h3 id="ticks">Ticks<a class="headerlink" href="#ticks" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ticks-from</code></td>
<td>Export ticks from a start date</td>
</tr>
<tr>
<td><code>ticks-range</code></td>
<td>Export ticks for a date range</td>
</tr>
<tr>
<td><code>ticks-recent</code></td>
<td>Export ticks from a trailing window</td>
</tr>
</tbody>
</table>
<h3 id="information">Information<a class="headerlink" href="#information" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>account-info</code></td>
<td>Export account information</td>
</tr>
<tr>
<td><code>terminal-info</code></td>
<td>Export terminal information</td>
</tr>
<tr>
<td><code>version</code></td>
<td>Export MetaTrader 5 version information</td>
</tr>
<tr>
<td><code>last-error</code></td>
<td>Export the last error information</td>
</tr>
<tr>
<td><code>symbols</code></td>
<td>Export symbol list</td>
</tr>
<tr>
<td><code>symbol-info</code></td>
<td>Export symbol details</td>
</tr>
<tr>
<td><code>symbol-info-tick</code></td>
<td>Export the last tick for a symbol</td>
</tr>
<tr>
<td><code>minimum-margins</code></td>
<td>Export minimum-volume margin summary</td>
</tr>
<tr>
<td><code>market-book</code></td>
<td>Export market depth (order book)</td>
</tr>
</tbody>
</table>
<h3 id="trading-state">Trading State<a class="headerlink" href="#trading-state" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>orders</code></td>
<td>Export active orders</td>
</tr>
<tr>
<td><code>positions</code></td>
<td>Export open positions</td>
</tr>
<tr>
<td><code>history-orders</code></td>
<td>Export historical orders</td>
</tr>
<tr>
<td><code>history-deals</code></td>
<td>Export historical deals</td>
</tr>
<tr>
<td><code>recent-history-deals</code></td>
<td>Export historical deals from a trailing window</td>
</tr>
<tr>
<td><code>mt5-summary</code></td>
<td>Export terminal/account status summary</td>
</tr>
<tr>
<td><code>order-check</code></td>
<td>Check funds sufficiency for a trade request (read-only, no <code>--yes</code>)</td>
</tr>
</tbody>
</table>
<h3 id="execution-live-mutating">Execution (live / mutating)<a class="headerlink" href="#execution-live-mutating" title="Permanent link">&para;</a></h3>
<p>These commands send requests to the live trade server and can place or close
real trades. Both require <code>--yes</code> for live execution.</p>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>order-send</code></td>
<td>Send a <strong>raw</strong> trade request directly to MT5 (<code>--yes</code> required; expert path — no extra validation)</td>
</tr>
<tr>
<td><code>close-positions</code></td>
<td>Close open positions by <code>--symbol</code> or <code>--ticket</code> (<code>--yes</code> required for live; <code>--dry-run</code> to preview; optional <code>--deviation</code> / <code>--comment</code> / <code>--magic</code>)</td>
</tr>
</tbody>
</table>
<p>Use <code>order-check</code> (Trading State) to validate funds before running <code>order-send --yes</code>.
<code>close-positions</code> is the safer high-level alternative that builds correct close
requests automatically. <code>order-send</code> is the expert raw path — downstream
applications should prefer dedicated closing helpers or their own risk controls.</p>
<h3 id="bulk-collection">Bulk Collection<a class="headerlink" href="#bulk-collection" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>collect-history</code></td>
<td>Collect rates, history-orders, and history-deals (ticks opt-in via <code>--dataset ticks</code>) for one or more symbols into a single SQLite database (optional cash-event/position views)</td>
</tr>
<tr>
<td><code>history-gaps</code></td>
<td>Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5</td>
</tr>
</tbody>
</table>
<div class="highlight"><pre><span></span><code><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a>mt5cli<span class="w"> </span>-o<span class="w"> </span>history.db<span class="w"> </span>collect-history<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span>--symbol<span class="w"> </span>EURUSD<span class="w"> </span>--symbol<span class="w"> </span>GBPUSD<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span>--date-from<span class="w"> </span><span class="m">2024</span>-01-01<span class="w"> </span>--date-to<span class="w"> </span><span class="m">2024</span>-02-01<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span>--dataset<span class="w"> </span>rates<span class="w"> </span>--dataset<span class="w"> </span>history-deals<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span>--timeframe<span class="w"> </span>M1<span class="w"> </span>--flags<span class="w"> </span>ALL<span class="w"> </span>--if-exists<span class="w"> </span>append<span class="w"> </span>--with-views
</code></pre></div>
<p><code>collect-history</code> options:</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--symbol/-s</code></td>
<td><em>required</em></td>
<td>Symbol to collect (repeat for multiple).</td>
</tr>
<tr>
<td><code>--date-from</code></td>
<td><em>required</em></td>
<td>Start date in ISO 8601.</td>
</tr>
<tr>
<td><code>--date-to</code></td>
<td><em>required</em></td>
<td>End date in ISO 8601.</td>
</tr>
<tr>
<td><code>--dataset</code></td>
<td>rates, history-orders, history-deals</td>
<td>Repeatable: <code>rates</code>, <code>ticks</code>, <code>history-orders</code>, <code>history-deals</code>. Ticks are opt-in: pass <code>--dataset ticks</code> to include them.</td>
</tr>
<tr>
<td><code>--timeframe</code></td>
<td><code>M1</code></td>
<td>Rates timeframe; recorded in a <code>timeframe</code> column on the <code>rates</code> table.</td>
</tr>
<tr>
<td><code>--flags</code></td>
<td><code>ALL</code></td>
<td>Tick copy flags forwarded to <code>copy_ticks_range</code>.</td>
</tr>
<tr>
<td><code>--if-exists</code></td>
<td><code>fail</code></td>
<td><code>append</code>, <code>replace</code>, or <code>fail</code> when a target table already exists.</td>
</tr>
<tr>
<td><code>--with-views</code></td>
<td>off</td>
<td>Add <code>cash_events</code> and <code>positions_reconstructed</code> views (requires the <code>history-deals</code> dataset).</td>
</tr>
</tbody>
</table>
<p>History orders and deals are fetched per symbol and concatenated, so the symbol filter is applied consistently across all datasets. The <code>cash_events</code> view is derived from symbol-filtered <code>history_deals</code>, so account-level cash events with empty or non-matching symbols may be excluded. The <code>positions_reconstructed</code> view excludes positions with no closing deal, uses volume-weighted open/close prices, and reports reversal deals (<code>DEAL_ENTRY_INOUT</code>) via <code>volume_reversal</code> / <code>reversal_count</code>.</p>
<p>See the <a href="api/history/#entity-relationship-diagram">History schema diagram</a> for a sample ER layout of the resulting database.</p>
<h2 id="global-options">Global Options<a class="headerlink" href="#global-options" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-o, --output</code></td>
<td>Output file path (required)</td>
</tr>
<tr>
<td><code>-f, --format</code></td>
<td>Output format (auto-detected from extension if omitted)</td>
</tr>
<tr>
<td><code>--table</code></td>
<td>Table name for SQLite3 output (default: "data")</td>
</tr>
<tr>
<td><code>--login</code></td>
<td>Trading account login</td>
</tr>
<tr>
<td><code>--password</code></td>
<td>Trading account password (<code>MT5_PASSWORD</code>)</td>
</tr>
<tr>
<td><code>--server</code></td>
<td>Trading server name</td>
</tr>
<tr>
<td><code>--path</code></td>
<td>Path to MetaTrader5 terminal EXE file</td>
</tr>
<tr>
<td><code>--timeout</code></td>
<td>Connection timeout in milliseconds</td>
</tr>
<tr>
<td><code>--log-level</code></td>
<td>Logging level (DEBUG, INFO, WARNING, ERROR)</td>
</tr>
</tbody>
</table>
<h2 id="requirements">Requirements<a class="headerlink" href="#requirements" title="Permanent link">&para;</a></h2>
<ul>
<li>Python 3.11+</li>
<li>Windows OS (MetaTrader 5 requirement)</li>
<li>MetaTrader 5 platform</li>
</ul>
<h2 id="api-reference">API Reference<a class="headerlink" href="#api-reference" title="Permanent link">&para;</a></h2>
<p>Browse the API documentation for detailed module information:</p>
<ul>
<li><a href="api/cli/">CLI Module</a> - CLI application with data export and execution commands</li>
<li><a href="api/sdk/">SDK Module</a> - Programmatic read-only data collection API</li>
<li><a href="api/utils/">Utils Module</a> - Constants, parameter types, parsers, and export utilities</li>
</ul>
<h2 id="development">Development<a class="headerlink" href="#development" title="Permanent link">&para;</a></h2>
<p>This project follows strict code quality standards:</p>
<ul>
<li>Type hints required (strict mode)</li>
<li>Comprehensive linting with Ruff</li>
<li>Test coverage tracking</li>
<li>Google-style docstrings</li>
</ul>
<h2 id="license">License<a class="headerlink" href="#license" title="Permanent link">&para;</a></h2>
<p>MIT License - see <a href="https://github.com/dceoy/mt5cli/blob/main/LICENSE">LICENSE</a> file for details.</p></div>
</div>
</div>
<footer class="col-md-12">
<hr>
<p>Documentation built with <a href="https://www.mkdocs.org/">MkDocs</a>.</p>
</footer>
<script src="js/bootstrap.bundle.min.js"></script>
<script>
var base_url = ".",
shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83};
</script>
<script src="js/base.js"></script>
<script src="search/main.js"></script>
<div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">Search</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>From here you can search these documents. Enter your search terms below.</p>
<form>
<div class="form-group">
<input type="search" class="form-control" placeholder="Search..." id="mkdocs-search-query" title="Type search term here">
</div>
</form>
<div id="mkdocs-search-results" data-no-results-text="No results found"></div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="keyboardModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="keyboardModalLabel">Keyboard Shortcuts</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th style="width: 20%;">Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="help shortcut"><kbd>?</kbd></td>
<td>Open this help</td>
</tr>
<tr>
<td class="next shortcut"><kbd>n</kbd></td>
<td>Next page</td>
</tr>
<tr>
<td class="prev shortcut"><kbd>p</kbd></td>
<td>Previous page</td>
</tr>
<tr>
<td class="search shortcut"><kbd>s</kbd></td>
<td>Search</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</body>
</html>
<!--
MkDocs version : 1.6.1
Build Date UTC : 2026-07-04 05:22:51.759791+00:00
-->
+287
View File
@@ -0,0 +1,287 @@
function getSearchTerm() {
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++) {
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == 'q') {
return sParameterName[1];
}
}
}
function applyTopPadding() {
// Update various absolute positions to match where the main container
// starts. This is necessary for handling multi-line nav headers, since
// that pushes the main container down.
var container = document.querySelector('body > .container');
var offset = container.offsetTop;
document.documentElement.style.scrollPaddingTop = offset + 'px';
document.querySelectorAll('.bs-sidebar.affix').forEach(function(sidebar) {
sidebar.style.top = offset + 'px';
});
}
document.addEventListener("DOMContentLoaded", function () {
var search_term = getSearchTerm();
var search_modal = new bootstrap.Modal(document.getElementById('mkdocs_search_modal'));
var keyboard_modal = new bootstrap.Modal(document.getElementById('mkdocs_keyboard_modal'));
if (search_term) {
search_modal.show();
}
// make sure search input gets autofocus every time modal opens.
document.getElementById('mkdocs_search_modal').addEventListener('shown.bs.modal', function() {
document.getElementById('mkdocs-search-query').focus();
});
// Close search modal when result is selected
// The links get added later so listen to parent
document.getElementById('mkdocs-search-results').addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
search_modal.hide();
}
});
// Populate keyboard modal with proper Keys
document.querySelector('.help.shortcut kbd').innerHTML = keyCodes[shortcuts.help];
document.querySelector('.prev.shortcut kbd').innerHTML = keyCodes[shortcuts.previous];
document.querySelector('.next.shortcut kbd').innerHTML = keyCodes[shortcuts.next];
document.querySelector('.search.shortcut kbd').innerHTML = keyCodes[shortcuts.search];
// Keyboard navigation
document.addEventListener("keydown", function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return true;
var key = e.which || e.keyCode || window.event && window.event.keyCode;
var page;
switch (key) {
case shortcuts.next:
page = document.querySelector('.navbar a[rel="next"]');
break;
case shortcuts.previous:
page = document.querySelector('.navbar a[rel="prev"]');
break;
case shortcuts.search:
e.preventDefault();
keyboard_modal.hide();
search_modal.show();
document.getElementById('mkdocs-search-query').focus();
break;
case shortcuts.help:
search_modal.hide();
keyboard_modal.show();
break;
default: break;
}
if (page && page.hasAttribute('href')) {
keyboard_modal.hide();
window.location.href = page.getAttribute('href');
}
});
document.querySelectorAll('table').forEach(function(table) {
table.classList.add('table', 'table-striped', 'table-hover');
});
function showInnerDropdown(item) {
var popup = item.nextElementSibling;
popup.classList.add('show');
item.classList.add('open');
// First, close any sibling dropdowns.
var container = item.parentElement.parentElement;
container.querySelectorAll(':scope > .dropdown-submenu > a').forEach(function(el) {
if (el !== item) {
hideInnerDropdown(el);
}
});
var popupMargin = 10;
var maxBottom = window.innerHeight - popupMargin;
var bounds = item.getBoundingClientRect();
popup.style.left = bounds.right + 'px';
if (bounds.top + popup.clientHeight > maxBottom &&
bounds.top > window.innerHeight / 2) {
popup.style.top = (bounds.bottom - popup.clientHeight) + 'px';
popup.style.maxHeight = (bounds.bottom - popupMargin) + 'px';
} else {
popup.style.top = bounds.top + 'px';
popup.style.maxHeight = (maxBottom - bounds.top) + 'px';
}
}
function hideInnerDropdown(item) {
var popup = item.nextElementSibling;
popup.classList.remove('show');
item.classList.remove('open');
popup.scrollTop = 0;
var menu = popup.querySelector('.dropdown-menu');
if (menu) {
menu.scrollTop = 0;
}
var dropdown = popup.querySelector('.dropdown-submenu > a');
if (dropdown) {
dropdown.classList.remove('open');
}
}
document.querySelectorAll('.dropdown-submenu > a').forEach(function(item) {
item.addEventListener('click', function(e) {
if (item.nextElementSibling.classList.contains('show')) {
hideInnerDropdown(item);
} else {
showInnerDropdown(item);
}
e.stopPropagation();
e.preventDefault();
});
});
document.querySelectorAll('.dropdown-menu').forEach(function(menu) {
menu.parentElement.addEventListener('hide.bs.dropdown', function() {
menu.scrollTop = 0;
var dropdown = menu.querySelector('.dropdown-submenu > a');
if (dropdown) {
dropdown.classList.remove('open');
}
menu.querySelectorAll('.dropdown-menu .dropdown-menu').forEach(function(submenu) {
submenu.classList.remove('show');
});
});
});
applyTopPadding();
});
window.addEventListener('resize', applyTopPadding);
var scrollSpy = new bootstrap.ScrollSpy(document.body, {
target: '.bs-sidebar'
});
/* Prevent disabled links from causing a page reload */
document.querySelectorAll("li.disabled a").forEach(function(item) {
item.addEventListener("click", function(event) {
event.preventDefault();
});
});
// See https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
// We only list common keys below. Obscure keys are omitted and their use is discouraged.
var keyCodes = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause/break',
20: 'caps lock',
27: 'escape',
32: 'spacebar',
33: 'page up',
34: 'page down',
35: 'end',
36: 'home',
37: '&larr;',
38: '&uarr;',
39: '&rarr;',
40: '&darr;',
45: 'insert',
46: 'delete',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
65: 'a',
66: 'b',
67: 'c',
68: 'd',
69: 'e',
70: 'f',
71: 'g',
72: 'h',
73: 'i',
74: 'j',
75: 'k',
76: 'l',
77: 'm',
78: 'n',
79: 'o',
80: 'p',
81: 'q',
82: 'r',
83: 's',
84: 't',
85: 'u',
86: 'v',
87: 'w',
88: 'x',
89: 'y',
90: 'z',
91: 'Left Windows Key / Left ⌘',
92: 'Right Windows Key',
93: 'Windows Menu / Right ⌘',
96: 'numpad 0',
97: 'numpad 1',
98: 'numpad 2',
99: 'numpad 3',
100: 'numpad 4',
101: 'numpad 5',
102: 'numpad 6',
103: 'numpad 7',
104: 'numpad 8',
105: 'numpad 9',
106: 'multiply',
107: 'add',
109: 'subtract',
110: 'decimal point',
111: 'divide',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
124: 'f13',
125: 'f14',
126: 'f15',
127: 'f16',
128: 'f17',
129: 'f18',
130: 'f19',
131: 'f20',
132: 'f21',
133: 'f22',
134: 'f23',
135: 'f24',
144: 'num lock',
145: 'scroll lock',
186: '&semi;',
187: '&equals;',
188: '&comma;',
189: '&hyphen;',
190: '&period;',
191: '&quest;',
192: '&grave;',
219: '&lsqb;',
220: '&bsol;',
221: '&rsqb;',
222: '&apos;',
};
+7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+65
View File
@@ -0,0 +1,65 @@
function setColorMode(mode) {
// Switch between light/dark theme. `mode` is a string value of either 'dark' or 'light'.
var hljs_light = document.getElementById('hljs-light'),
hljs_dark = document.getElementById('hljs-dark');
document.documentElement.setAttribute('data-bs-theme', mode);
if (mode == 'dark') {
hljs_light.disabled = true;
hljs_dark.disabled = false;
} else {
hljs_dark.disabled = true;
hljs_light.disabled = false;
}
}
function updateModeToggle(mode) {
// Update icon and toggle checkmarks of color mode selector.
var menu = document.getElementById('theme-menu');
document.querySelectorAll('[data-bs-theme-value]')
.forEach(function(toggle) {
if (mode == toggle.getAttribute('data-bs-theme-value')) {
toggle.setAttribute('aria-pressed', 'true');
toggle.lastElementChild.classList.remove('d-none');
menu.firstElementChild.setAttribute('class', toggle.firstElementChild.getAttribute('class'));
} else {
toggle.setAttribute('aria-pressed', 'false');
toggle.lastElementChild.classList.add('d-none');
}
});
}
function onSystemColorSchemeChange(event) {
// Update site color mode to match system color mode.
setColorMode(event.matches ? 'dark' : 'light');
}
var mql = window.matchMedia('(prefers-color-scheme: dark)'),
defaultMode = document.documentElement.getAttribute('data-bs-theme'),
storedMode = localStorage.getItem('mkdocs-colormode');
if (storedMode && storedMode != 'auto') {
setColorMode(storedMode);
updateModeToggle(storedMode);
} else if (storedMode == 'auto' || defaultMode == 'auto') {
setColorMode(mql.matches ? 'dark' : 'light');
updateModeToggle('auto');
mql.addEventListener('change', onSystemColorSchemeChange);
} else {
setColorMode(defaultMode);
updateModeToggle(defaultMode);
}
document.querySelectorAll('[data-bs-theme-value]')
.forEach(function(toggle) {
toggle.addEventListener('click', function (e) {
var mode = e.currentTarget.getAttribute('data-bs-theme-value');
localStorage.setItem('mkdocs-colormode', mode);
if (mode == 'auto') {
setColorMode(mql.matches ? 'dark' : 'light');
mql.addEventListener('change', onSystemColorSchemeChange);
} else {
setColorMode(mode);
mql.removeEventListener('change', onSystemColorSchemeChange);
}
updateModeToggle(mode);
});
});
-76
View File
@@ -1,76 +0,0 @@
site_name: mt5cli API Documentation
site_description: Command-line tool for MetaTrader 5
site_author: dceoy
site_url: https://github.com/dceoy/mt5cli
repo_name: dceoy/mt5cli
repo_url: https://github.com/dceoy/mt5cli
theme:
name: material
palette:
- scheme: default
primary: blue
accent: blue
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: blue
accent: blue
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- content.code.annotate
- content.code.copy
- content.code.mermaid
- navigation.indexes
- navigation.sections
- navigation.tabs
- navigation.top
- search.highlight
- search.share
- search.suggest
- toc.follow
plugins:
- search
- mkdocstrings:
handlers:
python:
paths: [.]
options:
show_source: true
show_root_heading: true
show_root_toc_entry: true
docstring_style: google
docstring_section_style: table
separate_signature: true
show_signature_annotations: true
signature_crossrefs: true
merge_init_into_class: true
show_if_no_docstring: true
nav:
- Home: index.md
- API Reference:
- Overview: api/index.md
- CLI: api/cli.md
- SDK: api/sdk.md
- Trading: api/trading.md
- History Collection (SQLite): api/history.md
- Utils: api/utils.md
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true
-153
View File
@@ -1,153 +0,0 @@
"""mt5cli: Command-line tool and SDK for MetaTrader 5."""
from importlib.metadata import version
from .history import (
RateTarget,
build_rate_targets,
build_rate_view_name,
drop_forming_rate_bar,
load_rate_data,
load_rate_data_from_connection,
load_rate_series_by_granularity,
load_rate_series_from_sqlite,
resolve_history_datasets,
resolve_history_tick_flags,
resolve_history_timeframes,
resolve_rate_tables,
resolve_rate_view_name,
resolve_rate_view_names,
)
from .sdk import (
AccountSpec,
Mt5CliClient,
ThrottledHistoryUpdater,
account_info,
build_config,
collect_history,
collect_latest_closed_rates_by_granularity,
collect_latest_closed_rates_for_accounts,
collect_latest_rates,
collect_latest_rates_for_accounts,
collect_latest_rates_for_accounts_with_retries,
copy_rates_from,
copy_rates_from_pos,
copy_rates_range,
copy_ticks_from,
copy_ticks_range,
history_deals,
history_orders,
last_error,
latest_rates,
market_book,
minimum_margins,
mt5_session,
mt5_summary,
mt5_summary_as_df,
orders,
positions,
recent_history_deals,
recent_ticks,
resolve_account_spec,
resolve_account_specs,
substitute_env_placeholders,
symbol_info,
symbol_info_tick,
symbols,
terminal_info,
update_history,
update_history_with_config,
)
from .sdk import (
version as mt5_version,
)
from .trading import (
calculate_margin_and_volume,
detect_position_side,
determine_order_limits,
mt5_trading_session,
)
from .utils import (
TICK_FLAG_MAP,
TIMEFRAME_MAP,
Dataset,
IfExists,
detect_format,
export_dataframe,
export_dataframe_to_sqlite,
parse_datetime,
parse_tick_flags,
parse_timeframe,
)
__version__ = version(__package__) if __package__ else None
__all__ = [
"TICK_FLAG_MAP",
"TIMEFRAME_MAP",
"AccountSpec",
"Dataset",
"IfExists",
"Mt5CliClient",
"RateTarget",
"ThrottledHistoryUpdater",
"account_info",
"build_config",
"build_rate_targets",
"build_rate_view_name",
"calculate_margin_and_volume",
"collect_history",
"collect_latest_closed_rates_by_granularity",
"collect_latest_closed_rates_for_accounts",
"collect_latest_rates",
"collect_latest_rates_for_accounts",
"collect_latest_rates_for_accounts_with_retries",
"copy_rates_from",
"copy_rates_from_pos",
"copy_rates_range",
"copy_ticks_from",
"copy_ticks_range",
"detect_format",
"detect_position_side",
"determine_order_limits",
"drop_forming_rate_bar",
"export_dataframe",
"export_dataframe_to_sqlite",
"history_deals",
"history_orders",
"last_error",
"latest_rates",
"load_rate_data",
"load_rate_data_from_connection",
"load_rate_series_by_granularity",
"load_rate_series_from_sqlite",
"market_book",
"minimum_margins",
"mt5_session",
"mt5_summary",
"mt5_summary_as_df",
"mt5_trading_session",
"mt5_version",
"orders",
"parse_datetime",
"parse_tick_flags",
"parse_timeframe",
"positions",
"recent_history_deals",
"recent_ticks",
"resolve_account_spec",
"resolve_account_specs",
"resolve_history_datasets",
"resolve_history_tick_flags",
"resolve_history_timeframes",
"resolve_rate_tables",
"resolve_rate_view_name",
"resolve_rate_view_names",
"substitute_env_placeholders",
"symbol_info",
"symbol_info_tick",
"symbols",
"terminal_info",
"update_history",
"update_history_with_config",
]
-5
View File
@@ -1,5 +0,0 @@
"""Entry point for running mt5cli as a module via ``python -m mt5cli``."""
from mt5cli.cli import main
main()
-716
View File
@@ -1,716 +0,0 @@
"""Command-line interface for MetaTrader 5 data export."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from datetime import datetime # noqa: TC003
from pathlib import Path # noqa: TC003
from typing import TYPE_CHECKING, Annotated, Any, cast
import typer
from pdmt5 import Mt5Config
from . import sdk
from .utils import (
DATETIME_TYPE,
REQUEST_TYPE,
TICK_FLAGS_TYPE,
TIMEFRAME_TYPE,
Dataset,
IfExists,
LogLevel,
OutputFormat,
detect_format,
export_dataframe,
)
if TYPE_CHECKING:
from collections.abc import Callable
import pandas as pd
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Export context
# ---------------------------------------------------------------------------
@dataclass
class _ExportContext:
"""Shared context data passed from the callback to each subcommand."""
output: Path
output_format: str
table: str
config: Mt5Config
# ---------------------------------------------------------------------------
# Typer application
# ---------------------------------------------------------------------------
app = typer.Typer(
name="mt5cli",
help="Export MetaTrader5 data to CSV, JSON, Parquet, or SQLite3.",
)
_REQUEST_OPTION_HELP = (
"Order request as a JSON object string, or '@path' to load JSON from a file."
)
def _get_export_context(ctx: typer.Context) -> _ExportContext:
return cast("_ExportContext", ctx.obj)
def _execute_export(
ctx: typer.Context,
fetch_fn: Callable[[], pd.DataFrame],
) -> None:
"""Execute the common fetch-export workflow.
Args:
ctx: Typer context carrying shared options.
fetch_fn: Callable that returns a DataFrame via the SDK layer.
"""
export_ctx = _get_export_context(ctx)
df = fetch_fn()
export_dataframe(
df=df,
output_path=export_ctx.output,
output_format=export_ctx.output_format,
table_name=export_ctx.table,
)
logger.info(
"Exported %d rows to %s (%s)",
len(df),
export_ctx.output,
export_ctx.output_format,
)
def _sdk_client(ctx: typer.Context) -> sdk.Mt5CliClient:
export_ctx = _get_export_context(ctx)
return sdk.Mt5CliClient(config=export_ctx.config)
@app.callback()
def _callback( # pyright: ignore[reportUnusedFunction]
ctx: typer.Context,
output: Annotated[
Path,
typer.Option("--output", "-o", help="Output file path."),
],
fmt: Annotated[
OutputFormat | None,
typer.Option(
"--format",
"-f",
help="Output format (auto-detected from extension if omitted).",
),
] = None,
table: Annotated[
str,
typer.Option(help="Table name for SQLite3 output."),
] = "data",
login: Annotated[
int | None,
typer.Option(help="Trading account login."),
] = None,
password: Annotated[
str | None,
typer.Option(help="Trading account password."),
] = None,
server: Annotated[
str | None,
typer.Option(help="Trading server name."),
] = None,
path: Annotated[
str | None,
typer.Option(help="Path to MetaTrader5 terminal EXE file."),
] = None,
timeout: Annotated[
int | None,
typer.Option(help="Connection timeout in milliseconds."),
] = None,
log_level: Annotated[
LogLevel,
typer.Option("--log-level", help="Logging level."),
] = LogLevel.WARNING,
) -> None:
"""Configure shared options for all export commands.
Raises:
typer.BadParameter: If the output format cannot be determined.
"""
logging.basicConfig(level=getattr(logging, log_level.value))
try:
output_format = detect_format(
output,
explicit_format=fmt.value if fmt is not None else None,
)
except ValueError as exc:
raise typer.BadParameter(str(exc)) from exc
ctx.obj = _ExportContext(
output=output,
output_format=output_format,
table=table,
config=Mt5Config(
path=path,
login=login,
password=password,
server=server,
timeout=timeout,
),
)
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
@app.command()
def rates_from(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
timeframe: Annotated[
int,
typer.Option(
click_type=TIMEFRAME_TYPE,
help="Timeframe (e.g., M1, H1, D1, or integer).",
),
],
date_from: Annotated[
datetime,
typer.Option(
click_type=DATETIME_TYPE,
help="Start date in ISO 8601 format.",
),
],
count: Annotated[int, typer.Option(help="Number of records.")],
) -> None:
"""Export rates from a start date."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.copy_rates_from(symbol, timeframe, date_from, count),
)
@app.command()
def rates_from_pos(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
timeframe: Annotated[
int,
typer.Option(
click_type=TIMEFRAME_TYPE,
help="Timeframe.",
),
],
start_pos: Annotated[int, typer.Option(help="Start position (0 = current bar).")],
count: Annotated[int, typer.Option(help="Number of records.")],
) -> None:
"""Export rates from a start position."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.copy_rates_from_pos(symbol, timeframe, start_pos, count),
)
@app.command()
def latest_rates(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
timeframe: Annotated[
int,
typer.Option(
click_type=TIMEFRAME_TYPE,
help="Timeframe.",
),
],
count: Annotated[int, typer.Option(help="Number of records.")],
start_pos: Annotated[
int,
typer.Option(help="Start position (0 = current bar)."),
] = 0,
) -> None:
"""Export latest rates from a start position."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.latest_rates(symbol, timeframe, count, start_pos=start_pos),
)
@app.command()
def rates_range(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
timeframe: Annotated[
int,
typer.Option(
click_type=TIMEFRAME_TYPE,
help="Timeframe.",
),
],
date_from: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
],
date_to: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="End date."),
],
) -> None:
"""Export rates for a date range."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.copy_rates_range(symbol, timeframe, date_from, date_to),
)
@app.command()
def ticks_from(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
date_from: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
],
count: Annotated[int, typer.Option(help="Number of ticks.")],
flags: Annotated[
int,
typer.Option(
click_type=TICK_FLAGS_TYPE,
help="Tick flags (ALL, INFO, TRADE, or integer).",
),
],
) -> None:
"""Export ticks from a start date."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.copy_ticks_from(symbol, date_from, count, flags),
)
@app.command()
def ticks_range(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
date_from: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
],
date_to: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="End date."),
],
flags: Annotated[
int,
typer.Option(click_type=TICK_FLAGS_TYPE, help="Tick flags."),
],
) -> None:
"""Export ticks for a date range."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.copy_ticks_range(symbol, date_from, date_to, flags),
)
@app.command()
def ticks_recent(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
seconds: Annotated[
float,
typer.Option(help="Lookback window in seconds."),
],
date_to: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
] = None,
count: Annotated[
int,
typer.Option(help="Maximum number of ticks to return."),
] = 10000,
flags: Annotated[
int,
typer.Option(
click_type=TICK_FLAGS_TYPE,
help="Tick flags (ALL, INFO, TRADE, or integer).",
),
] = "ALL", # pyright: ignore[reportArgumentType]
) -> None:
"""Export ticks from a recent time window."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.recent_ticks(
symbol,
seconds,
date_to=date_to,
count=count,
flags=flags,
),
)
@app.command()
def account_info(ctx: typer.Context) -> None:
"""Export account information."""
_execute_export(ctx, _sdk_client(ctx).account_info)
@app.command()
def terminal_info(ctx: typer.Context) -> None:
"""Export terminal information."""
_execute_export(ctx, _sdk_client(ctx).terminal_info)
@app.command()
def symbols(
ctx: typer.Context,
group: Annotated[
str | None,
typer.Option(help="Symbol group filter (e.g., *USD*)."),
] = None,
) -> None:
"""Export symbol list."""
client = _sdk_client(ctx)
_execute_export(ctx, lambda: client.symbols(group=group))
@app.command()
def symbol_info(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
) -> None:
"""Export symbol details."""
client = _sdk_client(ctx)
_execute_export(ctx, lambda: client.symbol_info(symbol))
@app.command()
def minimum_margins(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
) -> None:
"""Export minimum-volume buy and sell margin requirements."""
client = _sdk_client(ctx)
_execute_export(ctx, lambda: client.minimum_margins(symbol))
@app.command()
def orders(
ctx: typer.Context,
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
) -> None:
"""Export active orders."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.orders(symbol=symbol, group=group, ticket=ticket),
)
@app.command()
def positions(
ctx: typer.Context,
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
) -> None:
"""Export open positions."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.positions(symbol=symbol, group=group, ticket=ticket),
)
@app.command()
def history_orders(
ctx: typer.Context,
date_from: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
] = None,
date_to: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="End date."),
] = None,
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
) -> None:
"""Export historical orders."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.history_orders(
date_from=date_from,
date_to=date_to,
group=group,
symbol=symbol,
ticket=ticket,
position=position,
),
)
@app.command()
def history_deals(
ctx: typer.Context,
date_from: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
] = None,
date_to: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="End date."),
] = None,
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
) -> None:
"""Export historical deals."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.history_deals(
date_from=date_from,
date_to=date_to,
group=group,
symbol=symbol,
ticket=ticket,
position=position,
),
)
@app.command()
def recent_history_deals(
ctx: typer.Context,
hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
date_to: Annotated[
datetime | None,
typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
] = None,
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
) -> None:
"""Export historical deals from a recent trailing window."""
client = _sdk_client(ctx)
_execute_export(
ctx,
lambda: client.recent_history_deals(
hours,
date_to=date_to,
group=group,
symbol=symbol,
),
)
@app.command()
def mt5_summary(ctx: typer.Context) -> None:
"""Export a compact terminal/account status summary."""
client = _sdk_client(ctx)
_execute_export(ctx, client.mt5_summary_as_df)
@app.command()
def version(ctx: typer.Context) -> None:
"""Export MetaTrader5 version information."""
_execute_export(ctx, _sdk_client(ctx).version)
@app.command()
def last_error(ctx: typer.Context) -> None:
"""Export the last error information."""
_execute_export(ctx, _sdk_client(ctx).last_error)
@app.command()
def symbol_info_tick(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
) -> None:
"""Export the last tick for a symbol."""
client = _sdk_client(ctx)
_execute_export(ctx, lambda: client.symbol_info_tick(symbol))
@app.command()
def market_book(
ctx: typer.Context,
symbol: Annotated[str, typer.Option(help="Symbol name.")],
) -> None:
"""Export market depth (order book) for a symbol."""
client = _sdk_client(ctx)
_execute_export(ctx, lambda: client.market_book(symbol))
@app.command()
def order_check(
ctx: typer.Context,
request: Annotated[
dict[str, Any],
typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
],
) -> None:
"""Check funds sufficiency for a trading operation."""
export_ctx = _get_export_context(ctx)
def _fetch() -> pd.DataFrame:
return sdk._run_with_client( # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
export_ctx.config,
lambda c: c.order_check_as_df(request=request),
)
_execute_export(ctx, _fetch)
@app.command()
def order_send(
ctx: typer.Context,
request: Annotated[
dict[str, Any],
typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
],
yes: Annotated[
bool,
typer.Option("--yes", help="Confirm the live trade request."),
] = False,
) -> None:
"""Send a trading operation request to the trade server.
Raises:
typer.BadParameter: If --yes is not provided.
"""
if not yes:
msg = "Pass --yes to send a live trade request."
raise typer.BadParameter(msg, param_hint="--yes")
export_ctx = _get_export_context(ctx)
def _fetch() -> pd.DataFrame:
return sdk._run_with_client( # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
export_ctx.config,
lambda c: c.order_send_as_df(request=request),
)
_execute_export(ctx, _fetch)
@app.command()
def collect_history(
ctx: typer.Context,
symbol: Annotated[
list[str],
typer.Option(
"--symbol",
"-s",
help="Symbol to collect (repeat for multiple symbols).",
),
],
date_from: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="Start date."),
],
date_to: Annotated[
datetime,
typer.Option(click_type=DATETIME_TYPE, help="End date."),
],
dataset: Annotated[
list[Dataset] | None,
typer.Option(
"--dataset",
help=(
"Dataset to include (repeat for multiple)."
" Defaults to all: rates, ticks, history-orders, history-deals."
),
),
] = None,
timeframe: Annotated[
int,
typer.Option(
click_type=TIMEFRAME_TYPE,
help="Rates timeframe (e.g., M1, H1, D1).",
),
] = 1,
flags: Annotated[
int,
typer.Option(
click_type=TICK_FLAGS_TYPE,
help="Tick copy flags (ALL, INFO, TRADE, or integer).",
),
] = "ALL", # pyright: ignore[reportArgumentType]
if_exists: Annotated[
IfExists,
typer.Option(
"--if-exists",
help="Behavior when a target table already exists.",
),
] = IfExists.FAIL,
with_views: Annotated[
bool,
typer.Option(
"--with-views",
help=(
"Add cash_events and positions_reconstructed SQLite views"
" derived from history_deals."
),
),
] = False,
) -> None:
"""Collect historical datasets into a single SQLite database.
Tables written depend on ``--dataset``: ``rates``, ``ticks``,
``history_orders``, ``history_deals``. History datasets are fetched per
symbol and concatenated. Rates rows carry the requested ``timeframe`` so
appended runs at different timeframes remain distinguishable.
With ``--with-views`` (requires the ``history-deals`` dataset), optional
views ``cash_events`` and ``positions_reconstructed`` are derived from
``history_deals`` when the required columns are present.
Raises:
typer.BadParameter: If the output format is not SQLite3.
"""
export_ctx = _get_export_context(ctx)
if export_ctx.output_format != "sqlite3":
msg = (
"collect-history requires SQLite3 output."
" Use a .db/.sqlite/.sqlite3 extension or --format sqlite3."
)
raise typer.BadParameter(msg)
datasets = set(dataset) if dataset else set(Dataset)
sdk.collect_history(
output=export_ctx.output,
symbols=symbol,
date_from=date_from,
date_to=date_to,
datasets=datasets,
timeframe=timeframe,
flags=flags,
if_exists=if_exists,
with_views=with_views,
config=export_ctx.config,
)
def main() -> None:
"""Run the mt5cli CLI."""
app()
-1858
View File
File diff suppressed because it is too large Load Diff
-1964
View File
File diff suppressed because it is too large Load Diff
-210
View File
@@ -1,210 +0,0 @@
"""Trading-capable MetaTrader 5 session helpers and operational utilities."""
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING, Literal
from pdmt5 import Mt5Config, Mt5TradingClient
from .sdk import build_config
if TYPE_CHECKING:
from collections.abc import Iterator
import pandas as pd
PositionSide = Literal["long", "short"]
OrderSide = Literal["long", "short"]
__all__ = [
"OrderSide",
"PositionSide",
"calculate_margin_and_volume",
"detect_position_side",
"determine_order_limits",
"mt5_trading_session",
]
def _require_unit_ratio(value: float, name: str) -> None:
if not 0.0 <= value <= 1.0:
msg = f"{name} must be between 0 and 1 inclusive."
raise ValueError(msg)
def _require_protective_ratio(value: float, name: str) -> None:
if not 0.0 <= value < 1.0:
msg = f"{name} must be at least 0 and less than 1."
raise ValueError(msg)
def _sum_position_volume(positions: pd.DataFrame, position_type: object) -> float:
matched = positions.loc[positions["type"] == position_type, "volume"]
if matched.empty:
return 0.0
return float(matched.to_numpy(dtype=float).sum())
def _normalize_order_side(side: str) -> OrderSide:
normalized = side.lower()
if normalized in {"long", "buy"}:
return "long"
if normalized in {"short", "sell"}:
return "short"
msg = (
f"Unsupported order side: {side!r}. Expected 'long', 'short', 'buy', or 'sell'."
)
raise ValueError(msg)
def detect_position_side(
client: Mt5TradingClient,
symbol: str,
) -> PositionSide | None:
"""Detect the net open position side for a symbol.
Args:
client: Connected ``Mt5TradingClient`` instance.
symbol: Symbol to inspect.
Returns:
``"long"`` when net buy volume exceeds sell volume, ``"short"`` when
net sell volume exceeds buy volume, or ``None`` when no positions exist
or buy/sell volumes are exactly balanced.
"""
positions = client.positions_get_as_df(symbol=symbol)
if positions.empty:
return None
buy_type = client.mt5.POSITION_TYPE_BUY
sell_type = client.mt5.POSITION_TYPE_SELL
buy_volume = _sum_position_volume(positions, buy_type)
sell_volume = _sum_position_volume(positions, sell_type)
net_volume = buy_volume - sell_volume
if net_volume > 0:
return "long"
if net_volume < 0:
return "short"
return None
def calculate_margin_and_volume(
client: Mt5TradingClient,
symbol: str,
unit_margin_ratio: float,
preserved_margin_ratio: float,
) -> dict[str, float]:
"""Calculate tradable margin and volumes from account free margin.
Applies ``preserved_margin_ratio`` to keep a reserve off ``margin_free``,
then allocates ``unit_margin_ratio`` of the remainder as the margin budget
for volume sizing on both buy and sell sides.
Args:
client: Connected ``Mt5TradingClient`` instance.
symbol: Symbol used for minimum-lot margin and volume calculations.
unit_margin_ratio: Fraction of post-reserve margin to allocate per unit.
preserved_margin_ratio: Fraction of ``margin_free`` to preserve.
Returns:
Dictionary with ``margin_free``, ``available_margin``, ``trade_margin``,
``buy_volume``, and ``sell_volume``. Negative ``margin_free`` values are
clamped to ``0.0`` before sizing.
"""
_require_unit_ratio(unit_margin_ratio, "unit_margin_ratio")
_require_unit_ratio(preserved_margin_ratio, "preserved_margin_ratio")
account = client.account_info_as_dict()
margin_free = max(0.0, float(account.get("margin_free") or 0.0))
available_margin = margin_free * (1.0 - preserved_margin_ratio)
trade_margin = available_margin * unit_margin_ratio
buy_volume = client.calculate_volume_by_margin(symbol, trade_margin, "BUY")
sell_volume = client.calculate_volume_by_margin(symbol, trade_margin, "SELL")
return {
"margin_free": margin_free,
"available_margin": available_margin,
"trade_margin": trade_margin,
"buy_volume": buy_volume,
"sell_volume": sell_volume,
}
def determine_order_limits(
client: Mt5TradingClient,
symbol: str,
side: OrderSide | str,
stop_loss_limit_ratio: float,
take_profit_limit_ratio: float,
) -> dict[str, float | None]:
"""Derive entry and protective order prices from current market quotes.
Args:
client: Connected ``Mt5TradingClient`` instance.
symbol: Symbol used for the quote lookup.
side: Position side as ``"long"``/``"short"`` (``"buy"``/``"sell"``
aliases are accepted).
stop_loss_limit_ratio: Relative distance from entry for stop loss in
``[0, 1)``. A value of ``0`` omits the stop loss.
take_profit_limit_ratio: Relative distance from entry for take profit in
``[0, 1)``. A value of ``0`` omits the take profit.
Returns:
Dictionary with ``entry``, ``stop_loss``, and ``take_profit`` keys.
Omitted protective levels are returned as ``None``.
"""
_require_protective_ratio(stop_loss_limit_ratio, "stop_loss_limit_ratio")
_require_protective_ratio(take_profit_limit_ratio, "take_profit_limit_ratio")
normalized_side = _normalize_order_side(side)
tick = client.symbol_info_tick_as_dict(symbol=symbol)
entry = float(tick["ask"] if normalized_side == "long" else tick["bid"])
stop_loss: float | None = None
if stop_loss_limit_ratio > 0:
if normalized_side == "long":
stop_loss = entry * (1.0 - stop_loss_limit_ratio)
else:
stop_loss = entry * (1.0 + stop_loss_limit_ratio)
take_profit: float | None = None
if take_profit_limit_ratio > 0:
if normalized_side == "long":
take_profit = entry * (1.0 + take_profit_limit_ratio)
else:
take_profit = entry * (1.0 - take_profit_limit_ratio)
return {
"entry": entry,
"stop_loss": stop_loss,
"take_profit": take_profit,
}
@contextmanager
def mt5_trading_session(
config: Mt5Config | None = None,
retry_count: int = 0,
) -> Iterator[Mt5TradingClient]:
"""Open a trading-capable MT5 session and always shut down safely.
Launches the MetaTrader 5 terminal using ``Mt5Config.path`` when set,
initializes and logs in via ``initialize_and_login_mt5()``, yields a
connected :class:`~pdmt5.Mt5TradingClient`, and calls ``shutdown()`` on
exit even when an error is raised inside the context.
Args:
config: MT5 connection configuration. Defaults to an empty config that
attaches to a running terminal.
retry_count: Number of initialization retries passed to
``Mt5TradingClient``.
Yields:
Connected ``Mt5TradingClient`` bound to the session.
"""
mt5_config = config or build_config()
client = Mt5TradingClient(config=mt5_config, retry_count=retry_count)
try:
client.initialize_and_login_mt5()
yield client
finally:
client.shutdown()
-434
View File
@@ -1,434 +0,0 @@
"""Utility constants, types, and functions for the mt5cli package."""
from __future__ import annotations
import json
import sqlite3
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeGuard
import click
from pdmt5 import COPY_TICKS_MAP, TIMEFRAME_MAP
from pdmt5 import parse_copy_ticks as _parse_copy_ticks
from pdmt5 import parse_timeframe as _parse_timeframe
if TYPE_CHECKING:
from collections.abc import Sequence
import pandas as pd
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Backward-compatible snapshot; prefer ``COPY_TICKS_MAP`` from pdmt5 directly.
TICK_FLAG_MAP: dict[str, int] = dict(COPY_TICKS_MAP)
TIMEFRAME_NAMES: tuple[str, ...] = tuple(
name for name in TIMEFRAME_MAP if not name.startswith("TIMEFRAME_")
)
_TICK_FLAG_NAMES: tuple[str, ...] = tuple(
name for name in COPY_TICKS_MAP if not name.startswith("COPY_TICKS_")
)
_FORMAT_EXTENSIONS: dict[str, str] = {
".csv": "csv",
".json": "json",
".parquet": "parquet",
".pq": "parquet",
".db": "sqlite3",
".sqlite": "sqlite3",
".sqlite3": "sqlite3",
}
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class OutputFormat(StrEnum):
"""Supported output file formats."""
csv = "csv"
json = "json"
parquet = "parquet"
sqlite3 = "sqlite3"
class LogLevel(StrEnum):
"""Logging verbosity levels."""
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class Dataset(StrEnum):
"""Datasets supported by the ``collect-history`` command."""
rates = "rates"
ticks = "ticks"
history_orders = "history-orders"
history_deals = "history-deals"
@property
def table_name(self) -> str:
"""Return the SQLite table name for this dataset."""
return self.value.replace("-", "_")
class IfExists(StrEnum):
"""SQLite table conflict behavior for the ``collect-history`` command."""
APPEND = "append"
REPLACE = "replace"
FAIL = "fail"
# ---------------------------------------------------------------------------
# Click parameter types
# ---------------------------------------------------------------------------
class _DateTimeType(click.ParamType):
"""Click parameter type for ISO 8601 datetime strings."""
name = "DATETIME"
def convert(
self,
value: object,
param: click.Parameter | None,
ctx: click.Context | None,
) -> datetime:
"""Convert a string value to a timezone-aware datetime.
Args:
value: Raw value from the command line.
param: Click parameter instance.
ctx: Click context.
Returns:
Parsed datetime.
"""
if isinstance(value, datetime):
return value
try:
return parse_datetime(str(value))
except ValueError as exc:
self.fail(str(exc), param, ctx)
class _TimeframeType(click.ParamType):
"""Click parameter type for MT5 timeframe values."""
name = "TIMEFRAME"
def convert(
self,
value: object,
param: click.Parameter | None,
ctx: click.Context | None,
) -> int:
"""Convert a string or integer value to a timeframe integer.
Args:
value: Raw value from the command line.
param: Click parameter instance.
ctx: Click context.
Returns:
Integer timeframe value.
"""
try:
return parse_timeframe(value)
except ValueError as exc:
self.fail(str(exc), param, ctx)
class _TickFlagsType(click.ParamType):
"""Click parameter type for MT5 tick copy flags."""
name = "FLAGS"
def convert(
self,
value: object,
param: click.Parameter | None,
ctx: click.Context | None,
) -> int:
"""Convert a string or integer value to a tick flags integer.
Args:
value: Raw value from the command line.
param: Click parameter instance.
ctx: Click context.
Returns:
Integer tick flag value.
"""
try:
return parse_tick_flags(value)
except ValueError as exc:
self.fail(str(exc), param, ctx)
class _RequestType(click.ParamType):
"""Click parameter type for JSON order requests."""
name = "REQUEST"
def convert(
self,
value: object,
param: click.Parameter | None,
ctx: click.Context | None,
) -> dict[str, Any]:
"""Convert a raw CLI value to an order request dictionary.
Args:
value: Raw value from the command line.
param: Click parameter instance.
ctx: Click context.
Returns:
Parsed request dictionary.
"""
try:
return parse_request(str(value))
except ValueError as exc:
self.fail(str(exc), param, ctx)
DATETIME_TYPE = _DateTimeType()
TIMEFRAME_TYPE = _TimeframeType()
TICK_FLAGS_TYPE = _TickFlagsType()
REQUEST_TYPE = _RequestType()
# ---------------------------------------------------------------------------
# Public utility functions
# ---------------------------------------------------------------------------
def detect_format(
output_path: Path,
explicit_format: str | None = None,
) -> str:
"""Detect the output format from a file extension or explicit format string.
Args:
output_path: Path to the output file.
explicit_format: Explicitly specified format, if any.
Returns:
The detected format string.
Raises:
ValueError: If the format cannot be determined.
"""
if explicit_format is not None:
return explicit_format
suffix = output_path.suffix.lower()
if suffix in _FORMAT_EXTENSIONS:
return _FORMAT_EXTENSIONS[suffix]
msg = (
f"Cannot detect format from extension '{suffix}'."
" Use --format to specify the output format."
)
raise ValueError(msg)
def export_dataframe_to_sqlite(
df: pd.DataFrame,
output_path: Path,
table_name: str = "data",
*,
if_exists: IfExists = IfExists.APPEND,
index: bool = False,
index_label: str | None = None,
deduplicate_on: Sequence[str] | None = None,
) -> None:
"""Write a DataFrame to SQLite with configurable append and deduplication.
Args:
df: DataFrame to export.
output_path: SQLite database path.
table_name: Target table name.
if_exists: Conflict behavior when the table already exists.
index: Whether to write the DataFrame index as a column.
index_label: Column name for the index when ``index=True``.
deduplicate_on: Optional key columns to deduplicate after writing,
keeping the latest ``ROWID`` per key group. Deduplication scans the
full table, so repeated appends cost O(table size); index the key
columns when appending frequently.
"""
with sqlite3.connect(output_path) as conn:
df.to_sql( # type: ignore[reportUnknownMemberType]
table_name,
conn,
if_exists=if_exists.value,
index=index,
index_label=index_label,
)
if deduplicate_on:
from .history import drop_duplicates_in_table # noqa: PLC0415
drop_duplicates_in_table(
conn.cursor(),
table_name,
list(deduplicate_on),
keep="last",
)
conn.commit()
def export_dataframe(
df: pd.DataFrame,
output_path: Path,
output_format: str,
table_name: str = "data",
) -> None:
"""Export a pandas DataFrame to the specified file format.
Args:
df: DataFrame to export.
output_path: Path to the output file.
output_format: Output format (csv, json, parquet, or sqlite3).
table_name: Table name for SQLite3 output.
Raises:
ValueError: If the output format is not supported.
"""
if output_format == "csv":
df.to_csv(output_path, index=False)
elif output_format == "json":
df.to_json(
output_path,
orient="records",
date_format="iso",
indent=2,
)
elif output_format == "parquet":
df.to_parquet(output_path, index=False)
elif output_format == "sqlite3":
export_dataframe_to_sqlite(
df,
output_path,
table_name,
if_exists=IfExists.REPLACE,
index=False,
)
else:
msg = f"Unsupported output format: {output_format}"
raise ValueError(msg)
def parse_datetime(value: str) -> datetime:
"""Parse an ISO 8601 datetime string to a timezone-aware datetime.
Args:
value: ISO 8601 datetime string (e.g., '2024-01-01' or
'2024-01-01T12:00:00+00:00').
Returns:
Parsed datetime with UTC timezone if no timezone is specified.
Raises:
ValueError: If the string cannot be parsed.
"""
try:
dt = datetime.fromisoformat(value)
except ValueError:
msg = f"Invalid datetime format: '{value}'. Use ISO 8601 format."
raise ValueError(msg) from None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
return dt
def parse_timeframe(value: object) -> int:
"""Parse a timeframe string or integer value.
Args:
value: Timeframe name (e.g., 'M1', 'H1', 'D1') or integer value.
Returns:
Integer timeframe value.
Raises:
ValueError: If the timeframe is invalid.
"""
try:
return _parse_timeframe(value)
except ValueError:
display = value if isinstance(value, str) else repr(value)
valid = ", ".join(TIMEFRAME_NAMES)
msg = (
f"Invalid timeframe: '{display}'. "
f"Use one of: {valid}, or a supported integer."
)
raise ValueError(msg) from None
def parse_tick_flags(value: object) -> int:
"""Parse tick flags string or integer value.
Args:
value: Tick flag name (ALL, INFO, TRADE, COPY_TICKS_*) or integer value.
Returns:
Integer tick flag value compatible with MetaTrader 5 ``COPY_TICKS_*``.
Raises:
ValueError: If the flag is invalid.
"""
try:
return _parse_copy_ticks(value)
except ValueError:
display = value if isinstance(value, str) else repr(value)
valid = ", ".join(_TICK_FLAG_NAMES)
msg = (
f"Invalid tick flags: '{display}'. "
f"Use one of: {valid}, or a supported integer."
)
raise ValueError(msg) from None
def _is_request_dict(value: object) -> TypeGuard[dict[str, Any]]:
return isinstance(value, dict)
def parse_request(value: str) -> dict[str, Any]:
"""Parse a JSON-formatted order request string or file reference.
Args:
value: JSON object string, or '@path' to read JSON from a file.
Returns:
Parsed request dictionary.
Raises:
ValueError: If the request file cannot be read or the value is not a
JSON object.
"""
if value.startswith("@"):
path = Path(value[1:])
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
msg = f"Failed to read JSON request file '{path}': {exc}"
raise ValueError(msg) from exc
else:
text = value
try:
parsed: object = json.loads(text)
except json.JSONDecodeError as exc:
msg = f"Invalid JSON request: {exc}"
raise ValueError(msg) from exc
if not _is_request_dict(parsed):
msg = "Order request must be a JSON object."
raise ValueError(msg)
return parsed
BIN
View File
Binary file not shown.
-183
View File
@@ -1,183 +0,0 @@
[project]
name = "mt5cli"
version = "0.7.0"
description = "Command-line tool for MetaTrader 5"
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
license = "MIT"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">= 3.11, < 3.14"
dependencies = [
"pdmt5>=0.3.0",
"click >= 8.1.0",
"pyarrow >= 19.0.0",
"typer >= 0.15.0",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"License :: OSI Approved :: MIT License",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Intended Audience :: Financial and Insurance Industry",
"Topic :: Office/Business :: Financial :: Investment",
]
[project.scripts]
mt5cli = "mt5cli.cli:main"
[project.urls]
Repository = "https://github.com/dceoy/mt5cli.git"
[tool.uv]
required-environments = ["platform_system == 'Windows'"]
[dependency-groups]
dev = [
"ruff >= 0.11.0",
"pyright >= 1.1.407",
"pytest>=9.0.3",
"pytest-mock >= 3.12.0",
"pytest-cov >= 5.0.0",
"pandas-stubs >= 2.2.3.250527",
"mkdocs >= 1.6.1",
"mkdocs-material >= 9.7.6",
"mkdocstrings[python] >= 1.0.4",
"pymdown-extensions >= 10.21.2",
]
[tool.ruff]
line-length = 88
exclude = ["build", ".venv"]
preview = true
[tool.ruff.lint]
select = [
"F", # Pyflakes (F)
"E", # pycodestyle error (E)
"W", # pycodestyle warning (W)
"C90", # mccabe (C90)
"I", # isort (I)
"N", # pep8-naming (N)
"D", # pydocstyle (D)
"UP", # pyupgrade (UP)
"S", # flake8-bandit (S)
"B", # flake8-bugbear (B)
"C4", # flake8-comprehensions (C4)
"SIM", # flake8-simplify (SIM)
"ARG", # flake8-unused-arguments (ARG)
"PD", # pandas-vet (PD)
"PLC", # Pylint convention (PLC)
"PLE", # Pylint error (PLE)
"PLR", # Pylint refactor (PLR)
"PLW", # Pylint warning (PLW)
"FLY", # flynt (FLY)
"NPY", # NumPy-specific rules (NPY)
"PERF", # Perflint (PERF)
"FURB", # refurb (FURB)
"RUF", # Ruff-specific rules (RUF)
"YTT", # flake8-2020 (YTT)
"ANN", # flake8-annotations (ANN)
"ASYNC", # flake8-async (ASYNC)
"BLE", # flake8-blind-except (BLE)
"FBT", # flake8-boolean-trap (FBT)
"A", # flake8-builtins (A)
"COM", # flake8-commas (COM)
"DTZ", # flake8-datetimez (DTZ)
"T10", # flake8-debugger (T10)
"DJ", # flake8-django (DJ)
"EM", # flake8-errmsg (EM)
"EXE", # flake8-executable (EXE)
"FA", # flake8-future-annotations (FA)
"ISC", # flake8-implicit-str-concat (ISC)
"ICN", # flake8-import-conventions (ICN)
"LOG", # flake8-logging (LOG)
"G", # flake8-logging-format (G)
"INP", # flake8-no-pep420 (INP)
"PIE", # flake8-pie (PIE)
"T20", # flake8-print (T20)
"PYI", # flake8-pyi (PYI)
"PT", # flake8-pytest-style (PT)
"Q", # flake8-quotes (Q)
"RSE", # flake8-raise (RSE)
"SLF", # flake8-self (SLF)
"SLOT", # flake8-slots (SLOT)
"TID", # flake8-tidy-imports (TID)
"TCH", # flake8-type-checking (TCH)
"INT", # flake8-gettext (INT)
"PTH", # flake8-use-pathlib (PTH)
"TD", # flake8-todos (TD)
"FIX", # flake8-fixme (FIX)
"ERA", # eradicate (ERA)
"PGH", # pygrep-hooks (PGH)
"TRY", # tryceratops (TRY)
"FAST", # FastAPI (FAST)
"AIR", # Airflow (AIR)
"DOC", # pydoclint (DOC)
]
ignore = [
"COM812", # missing-trailing-comma
"FBT001", # boolean-type-hint-positional-argument
"FBT002", # boolean-default-value-positional-argument
]
[tool.ruff.lint.per-file-ignores]
"mt5cli/history.py" = ["TC003"]
"tests/**/*.py" = [
"DOC201", # Missing return documentation
"DOC501", # Raised exception missing from docstring
"PLC2701", # Private name import
"PLR0904", # Too many public methods
"PLR2004", # Magic value used in comparison
"PLR6301", # Method could be function/static/classmethod
"S101", # Use of assert (acceptable in tests)
"S106", # Possible hardcoded password
"SLF001", # Private member accessed
]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.pylint]
max-args = 10
max-public-methods = 40
[tool.pyright]
exclude = ["build", ".venv"]
venvPath = "."
venv = ".venv"
typeCheckingMode = "strict"
reportMissingTypeStubs = false
[tool.pytest.ini_options]
addopts = [
"--cov=mt5cli",
"--cov-branch",
"--doctest-modules",
"--capture=no",
]
pythonpath = ["."]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
minversion = "6.0"
[tool.coverage.run]
source = ["mt5cli"]
omit = [
"**/__init__.py",
"**/__main__.py",
"tests/**",
]
[tool.coverage.report]
show_missing = true
fail_under = 100
exclude_lines = ["if TYPE_CHECKING:"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+3475
View File
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
function getSearchTermFromLocation() {
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++) {
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == 'q') {
return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20'));
}
}
}
function joinUrl (base, path) {
if (path.substring(0, 1) === "/") {
// path starts with `/`. Thus it is absolute.
return path;
}
if (base.substring(base.length-1) === "/") {
// base ends with `/`
return base + path;
}
return base + "/" + path;
}
function escapeHtml (value) {
return value.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function formatResult (location, title, summary) {
return '<article><h3><a href="' + joinUrl(base_url, location) + '">'+ escapeHtml(title) + '</a></h3><p>' + escapeHtml(summary) +'</p></article>';
}
function displayResults (results) {
var search_results = document.getElementById("mkdocs-search-results");
while (search_results.firstChild) {
search_results.removeChild(search_results.firstChild);
}
if (results.length > 0){
for (var i=0; i < results.length; i++){
var result = results[i];
var html = formatResult(result.location, result.title, result.summary);
search_results.insertAdjacentHTML('beforeend', html);
}
} else {
var noResultsText = search_results.getAttribute('data-no-results-text');
if (!noResultsText) {
noResultsText = "No results found";
}
search_results.insertAdjacentHTML('beforeend', '<p>' + noResultsText + '</p>');
}
}
function doSearch () {
var query = document.getElementById('mkdocs-search-query').value;
if (query.length > min_search_length) {
if (!window.Worker) {
displayResults(search(query));
} else {
searchWorker.postMessage({query: query});
}
} else {
// Clear results for short queries
displayResults([]);
}
}
function initSearch () {
var search_input = document.getElementById('mkdocs-search-query');
if (search_input) {
search_input.addEventListener("keyup", doSearch);
}
var term = getSearchTermFromLocation();
if (term) {
search_input.value = term;
doSearch();
}
}
function onWorkerMessage (e) {
if (e.data.allowSearch) {
initSearch();
} else if (e.data.results) {
var results = e.data.results;
displayResults(results);
} else if (e.data.config) {
min_search_length = e.data.config.min_search_length-1;
}
}
if (!window.Worker) {
console.log('Web Worker API not supported');
// load index in main thread
$.getScript(joinUrl(base_url, "search/worker.js")).done(function () {
console.log('Loaded worker');
init();
window.postMessage = function (msg) {
onWorkerMessage({data: msg});
};
}).fail(function (jqxhr, settings, exception) {
console.error('Could not load worker.js');
});
} else {
// Wrap search in a web worker
var searchWorker = new Worker(joinUrl(base_url, "search/worker.js"));
searchWorker.postMessage({init: true});
searchWorker.onmessage = onWorkerMessage;
}
File diff suppressed because one or more lines are too long
+133
View File
@@ -0,0 +1,133 @@
var base_path = 'function' === typeof importScripts ? '.' : '/search/';
var allowSearch = false;
var index;
var documents = {};
var lang = ['en'];
var data;
function getScript(script, callback) {
console.log('Loading script: ' + script);
$.getScript(base_path + script).done(function () {
callback();
}).fail(function (jqxhr, settings, exception) {
console.log('Error: ' + exception);
});
}
function getScriptsInOrder(scripts, callback) {
if (scripts.length === 0) {
callback();
return;
}
getScript(scripts[0], function() {
getScriptsInOrder(scripts.slice(1), callback);
});
}
function loadScripts(urls, callback) {
if( 'function' === typeof importScripts ) {
importScripts.apply(null, urls);
callback();
} else {
getScriptsInOrder(urls, callback);
}
}
function onJSONLoaded () {
data = JSON.parse(this.responseText);
var scriptsToLoad = ['lunr.js'];
if (data.config && data.config.lang && data.config.lang.length) {
lang = data.config.lang;
}
if (lang.length > 1 || lang[0] !== "en") {
scriptsToLoad.push('lunr.stemmer.support.js');
if (lang.length > 1) {
scriptsToLoad.push('lunr.multi.js');
}
if (lang.includes("ja") || lang.includes("jp")) {
scriptsToLoad.push('tinyseg.js');
}
for (var i=0; i < lang.length; i++) {
if (lang[i] != 'en') {
scriptsToLoad.push(['lunr', lang[i], 'js'].join('.'));
}
}
}
loadScripts(scriptsToLoad, onScriptsLoaded);
}
function onScriptsLoaded () {
console.log('All search scripts loaded, building Lunr index...');
if (data.config && data.config.separator && data.config.separator.length) {
lunr.tokenizer.separator = new RegExp(data.config.separator);
}
if (data.index) {
index = lunr.Index.load(data.index);
data.docs.forEach(function (doc) {
documents[doc.location] = doc;
});
console.log('Lunr pre-built index loaded, search ready');
} else {
index = lunr(function () {
if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) {
this.use(lunr[lang[0]]);
} else if (lang.length > 1) {
this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility
}
this.field('title');
this.field('text');
this.ref('location');
for (var i=0; i < data.docs.length; i++) {
var doc = data.docs[i];
this.add(doc);
documents[doc.location] = doc;
}
});
console.log('Lunr index built, search ready');
}
allowSearch = true;
postMessage({config: data.config});
postMessage({allowSearch: allowSearch});
}
function init () {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", onJSONLoaded);
var index_path = base_path + '/search_index.json';
if( 'function' === typeof importScripts ){
index_path = 'search_index.json';
}
oReq.open("GET", index_path);
oReq.send();
}
function search (query) {
if (!allowSearch) {
console.error('Assets for search still loading');
return;
}
var resultDocuments = [];
var results = index.search(query);
for (var i=0; i < results.length; i++){
var result = results[i];
doc = documents[result.ref];
doc.summary = doc.text.substring(0, 200);
resultDocuments.push(doc);
}
return resultDocuments;
}
if( 'function' === typeof importScripts ) {
onmessage = function (e) {
if (e.data.init) {
init();
} else if (e.data.query) {
postMessage({ results: search(e.data.query) });
} else {
console.error("Worker - Unrecognized message: " + e);
}
};
}
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://github.com/dceoy/mt5cli/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/cli/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/client/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/converters/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/exceptions/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/grafana/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/history/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/public-contract/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/schemas/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/sdk/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/telemetry/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/trading/</loc>
<lastmod>2026-07-04</lastmod>
</url>
<url>
<loc>https://github.com/dceoy/mt5cli/api/utils/</loc>
<lastmod>2026-07-04</lastmod>
</url>
</urlset>
BIN
View File
Binary file not shown.
-113
View File
@@ -1,113 +0,0 @@
---
name: mt5cli
description: Use the `mt5cli` CLI to export MetaTrader 5 data (rates, ticks, account, symbols, orders, positions, history) to CSV, JSON, Parquet, or SQLite3. Invoke when the user asks to export, dump, download, or fetch MT5 market data or account data to a file.
---
# mt5cli
Export MetaTrader 5 data to CSV, JSON, Parquet, or SQLite3 via the `mt5cli`
command. Output format is auto-detected from the file extension (`.csv`,
`.json`, `.parquet`/`.pq`, `.db`/`.sqlite`/`.sqlite3`) or overridden with
`--format/-f`.
## Requirements
- Python 3.11+ on Windows with MetaTrader 5 installed (pdmt5 requires the
MT5 terminal).
- Install: `pip install -U mt5cli MetaTrader5`.
- In this repo, run via `uv run mt5cli ...` or `uv run python -m mt5cli ...`.
## Invocation shape
```
mt5cli [GLOBAL OPTIONS] -o OUTPUT COMMAND [COMMAND OPTIONS]
```
Global options MUST precede the subcommand.
### Global options (apply to every subcommand)
| Option | Purpose |
| --------------------- | ------------------------------------------------------------- |
| `-o, --output PATH` | Output file path (required). |
| `-f, --format FORMAT` | `csv`, `json`, `parquet`, or `sqlite3` (auto from extension). |
| `--table NAME` | Table name for SQLite3 output (default: `data`). |
| `--login INT` | MT5 trading account login. |
| `--password TEXT` | MT5 trading account password. |
| `--server TEXT` | MT5 trading server name. |
| `--path TEXT` | Path to MetaTrader 5 terminal EXE. |
| `--timeout INT` | Connection timeout in milliseconds. |
| `--log-level LEVEL` | `DEBUG`, `INFO`, `WARNING` (default), `ERROR`. |
### Parameter value formats
- **Datetimes** (`--date-from`, `--date-to`): ISO 8601 (`2024-01-01` or
`2024-01-01T12:00:00+00:00`). Naive values are treated as UTC.
- **Timeframe** (`--timeframe`): `M1`, `M2`, `M3`, `M4`, `M5`, `M6`, `M10`,
`M12`, `M15`, `M20`, `M30`, `H1`, `H2`, `H3`, `H4`, `H6`, `H8`, `H12`,
`D1`, `W1`, `MN1`, or the raw integer.
- **Tick flags** (`--flags`): `ALL`, `INFO`, `TRADE`, or the raw integer.
## Commands
| Command | Required options | Optional options |
| ----------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rates-from` | `--symbol`, `--timeframe`, `--date-from`, `--count` | — |
| `rates-from-pos` | `--symbol`, `--timeframe`, `--start-pos`, `--count` | — |
| `rates-range` | `--symbol`, `--timeframe`, `--date-from`, `--date-to` | — |
| `ticks-from` | `--symbol`, `--date-from`, `--count`, `--flags` | — |
| `ticks-range` | `--symbol`, `--date-from`, `--date-to`, `--flags` | — |
| `account-info` | — | — |
| `terminal-info` | — | — |
| `symbols` | — | `--group` (e.g., `*USD*`) |
| `symbol-info` | `--symbol` | — |
| `orders` | — | `--symbol`, `--group`, `--ticket` |
| `positions` | — | `--symbol`, `--group`, `--ticket` |
| `history-orders` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position` |
| `history-deals` | — | `--date-from`, `--date-to`, `--group`, `--symbol`, `--ticket`, `--position` |
| `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) |
## Examples
```bash
# Account snapshot as CSV.
mt5cli -o account.csv account-info
# EURUSD M1 bars (1000 rows) from a start date to Parquet.
mt5cli -o rates.parquet rates-from \
--symbol EURUSD --timeframe M1 --date-from 2024-01-01 --count 1000
# EURUSD tick stream for a date range to JSON.
mt5cli -o ticks.json ticks-range \
--symbol EURUSD --date-from 2024-01-01 --date-to 2024-01-02 --flags ALL
# USD symbols into a named table in SQLite3.
mt5cli -o data.db --table symbols symbols --group "*USD*"
# Historical deals filtered by symbol (using an already-logged-in MT5 terminal).
mt5cli -o deals.csv history-deals --symbol EURUSD --date-from 2024-01-01
# Bundle selected historical datasets into one SQLite db, appending to any
# existing tables, plus cash_events and positions_reconstructed views.
mt5cli -o history.db collect-history \
--symbol EURUSD --symbol GBPUSD \
--date-from 2024-01-01 --date-to 2024-02-01 \
--dataset rates --dataset history-deals \
--timeframe M1 --flags ALL --if-exists append --with-views
```
## Guidelines
- Pick the output extension to avoid passing `--format`.
- Use `--table` only with SQLite3 outputs; it is otherwise ignored.
- `--count` is required for `rates-from`, `rates-from-pos`, and `ticks-from`.
Prefer `rates-range` / `ticks-range` when a fixed window is known.
- Credentials (`--login`, `--password`, `--server`) are optional when the
local MT5 terminal is already logged in.
- Avoid passing `--password` on the command line in shared or logged
environments — it is visible in `ps`, shell history, and CI logs. Prefer
logging in through the MT5 terminal first, then omit credentials here.
- Reach for `--log-level DEBUG` when a command fails silently — MT5
connection errors surface there.
- If the user asks to run from source in this repo, prefix with `uv run`
(e.g., `uv run mt5cli -o out.csv account-info`).
-1
View File
@@ -1 +0,0 @@
"""Test suite for mt5cli."""
-1500
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-2071
View File
File diff suppressed because it is too large Load Diff
-356
View File
@@ -1,356 +0,0 @@
"""Tests for trading session helpers and operational utilities."""
from __future__ import annotations
from unittest.mock import MagicMock
import pandas as pd
import pytest
from pdmt5 import Mt5RuntimeError
from pytest_mock import MockerFixture # noqa: TC002
from mt5cli.sdk import build_config
from mt5cli.trading import (
calculate_margin_and_volume,
detect_position_side,
determine_order_limits,
mt5_trading_session,
)
class TestDetectPositionSide:
"""Tests for detect_position_side."""
def test_returns_none_when_no_positions(self) -> None:
"""Test None is returned when no open positions exist."""
client = MagicMock()
client.positions_get_as_df.return_value = pd.DataFrame()
assert detect_position_side(client, "EURUSD") is None
def test_returns_long_for_net_buy_volume(self) -> None:
"""Test long is returned when buy volume exceeds sell volume."""
client = MagicMock()
client.mt5.POSITION_TYPE_BUY = 0
client.mt5.POSITION_TYPE_SELL = 1
client.positions_get_as_df.return_value = pd.DataFrame(
{
"type": [0, 0, 1],
"volume": [0.2, 0.1, 0.05],
},
)
assert detect_position_side(client, "EURUSD") == "long"
def test_returns_short_for_net_sell_volume(self) -> None:
"""Test short is returned when sell volume exceeds buy volume."""
client = MagicMock()
client.mt5.POSITION_TYPE_BUY = 0
client.mt5.POSITION_TYPE_SELL = 1
client.positions_get_as_df.return_value = pd.DataFrame(
{
"type": [1, 1],
"volume": [0.3, 0.1],
},
)
assert detect_position_side(client, "EURUSD") == "short"
def test_returns_none_for_balanced_hedged_positions(self) -> None:
"""Test None is returned when buy and sell volumes net to zero."""
client = MagicMock()
client.mt5.POSITION_TYPE_BUY = 0
client.mt5.POSITION_TYPE_SELL = 1
client.positions_get_as_df.return_value = pd.DataFrame(
{
"type": [0, 1],
"volume": [0.2, 0.2],
},
)
assert detect_position_side(client, "EURUSD") is None
class TestCalculateMarginAndVolume:
"""Tests for calculate_margin_and_volume."""
def test_calculates_margin_budget_and_volumes(self) -> None:
"""Test margin budget and buy/sell volumes are derived from ratios."""
client = MagicMock()
client.account_info_as_dict.return_value = {"margin_free": 1000.0}
client.calculate_volume_by_margin.side_effect = [0.3, 0.2]
result = calculate_margin_and_volume(
client,
"EURUSD",
unit_margin_ratio=0.5,
preserved_margin_ratio=0.2,
)
assert result == {
"margin_free": 1000.0,
"available_margin": 800.0,
"trade_margin": 400.0,
"buy_volume": 0.3,
"sell_volume": 0.2,
}
client.calculate_volume_by_margin.assert_any_call("EURUSD", 400.0, "BUY")
client.calculate_volume_by_margin.assert_any_call("EURUSD", 400.0, "SELL")
@pytest.mark.parametrize(
("account_dict", "expected_margin_free"),
[
({"margin_free": 0.0}, 0.0),
({}, 0.0),
({"margin_free": None}, 0.0),
],
)
def test_zero_or_missing_margin_free(
self,
account_dict: dict[str, float | None],
expected_margin_free: float,
) -> None:
"""Test missing or zero margin_free yields zero trade margin."""
client = MagicMock()
client.account_info_as_dict.return_value = account_dict
client.calculate_volume_by_margin.return_value = 0.0
result = calculate_margin_and_volume(
client,
"EURUSD",
unit_margin_ratio=0.5,
preserved_margin_ratio=0.2,
)
assert result["margin_free"] == expected_margin_free
client.calculate_volume_by_margin.assert_any_call("EURUSD", 0.0, "BUY")
client.calculate_volume_by_margin.assert_any_call("EURUSD", 0.0, "SELL")
def test_clamps_negative_margin_free_to_zero(self) -> None:
"""Test negative margin_free is clamped to zero before sizing."""
client = MagicMock()
client.account_info_as_dict.return_value = {"margin_free": -500.0}
client.calculate_volume_by_margin.return_value = 0.0
result = calculate_margin_and_volume(
client,
"EURUSD",
unit_margin_ratio=0.5,
preserved_margin_ratio=0.2,
)
expected_margin_free = 0.0
assert result["margin_free"] == expected_margin_free
client.calculate_volume_by_margin.assert_any_call("EURUSD", 0.0, "BUY")
client.calculate_volume_by_margin.assert_any_call("EURUSD", 0.0, "SELL")
@pytest.mark.parametrize(
("unit_ratio", "preserved_ratio"),
[
(-0.1, 0.0),
(1.1, 0.0),
(0.5, -0.1),
(0.5, 1.1),
],
)
def test_rejects_invalid_ratios(
self,
unit_ratio: float,
preserved_ratio: float,
) -> None:
"""Test invalid ratio values raise ValueError."""
with pytest.raises(ValueError, match="must be between 0 and 1"):
calculate_margin_and_volume(
MagicMock(),
"EURUSD",
unit_margin_ratio=unit_ratio,
preserved_margin_ratio=preserved_ratio,
)
class TestDetermineOrderLimits:
"""Tests for determine_order_limits."""
@pytest.mark.parametrize(
("side", "expected_entry_key"),
[
("long", "ask"),
("short", "bid"),
("buy", "ask"),
("sell", "bid"),
],
)
def test_uses_expected_quote_for_entry(
self,
side: str,
expected_entry_key: str,
) -> None:
"""Test entry price is taken from ask for long/buy and bid for short/sell."""
client = MagicMock()
client.symbol_info_tick_as_dict.return_value = {"ask": 1.1010, "bid": 1.1000}
result = determine_order_limits(
client,
"EURUSD",
side,
stop_loss_limit_ratio=0.0,
take_profit_limit_ratio=0.0,
)
assert (
result["entry"]
== client.symbol_info_tick_as_dict.return_value[expected_entry_key]
)
assert result["stop_loss"] is None
assert result["take_profit"] is None
def test_calculates_long_protective_levels(self) -> None:
"""Test long stop loss and take profit are placed below/above entry."""
client = MagicMock()
client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
result = determine_order_limits(
client,
"EURUSD",
"long",
stop_loss_limit_ratio=0.02,
take_profit_limit_ratio=0.03,
)
assert result == {
"entry": 100.0,
"stop_loss": 98.0,
"take_profit": 103.0,
}
def test_calculates_short_protective_levels(self) -> None:
"""Test short stop loss and take profit are placed above/below entry."""
client = MagicMock()
client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
result = determine_order_limits(
client,
"EURUSD",
"short",
stop_loss_limit_ratio=0.02,
take_profit_limit_ratio=0.03,
)
assert result == {
"entry": 99.0,
"stop_loss": 100.98,
"take_profit": 96.03,
}
def test_rejects_unknown_side(self) -> None:
"""Test unsupported side values raise ValueError."""
with pytest.raises(ValueError, match="Unsupported order side"):
determine_order_limits(
MagicMock(),
"EURUSD",
"flat",
stop_loss_limit_ratio=0.01,
take_profit_limit_ratio=0.01,
)
@pytest.mark.parametrize(
("stop_loss_ratio", "take_profit_ratio"),
[
(-0.05, 0.01),
(0.01, 2.0),
],
)
def test_rejects_invalid_protective_ratios(
self,
stop_loss_ratio: float,
take_profit_ratio: float,
) -> None:
"""Test out-of-range protective ratios raise ValueError."""
with pytest.raises(ValueError, match="must be at least 0 and less than 1"):
determine_order_limits(
MagicMock(),
"EURUSD",
"long",
stop_loss_limit_ratio=stop_loss_ratio,
take_profit_limit_ratio=take_profit_ratio,
)
@pytest.mark.parametrize(
("field", "ratio"),
[
("stop_loss_limit_ratio", 1.0),
("take_profit_limit_ratio", 1.0),
],
)
def test_rejects_unit_boundary_protective_ratios(
self,
field: str,
ratio: float,
) -> None:
"""Test protective ratios of exactly 1.0 are rejected."""
kwargs = {
"stop_loss_limit_ratio": 0.01,
"take_profit_limit_ratio": 0.01,
field: ratio,
}
with pytest.raises(ValueError, match="must be at least 0 and less than 1"):
determine_order_limits(
MagicMock(),
"EURUSD",
"long",
**kwargs,
)
class TestMt5TradingSession:
"""Tests for the mt5_trading_session context manager."""
def test_yields_connected_client_and_shuts_down(
self,
mocker: MockerFixture,
) -> None:
"""Test mt5_trading_session connects, yields a client, and shuts down."""
mock_client = MagicMock()
trading_client = mocker.patch(
"mt5cli.trading.Mt5TradingClient",
return_value=mock_client,
)
with mt5_trading_session(
build_config(path="/opt/mt5/terminal64.exe"),
retry_count=2,
) as client:
mock_client.initialize_and_login_mt5.assert_called_once()
assert client is mock_client
trading_client.assert_called_once()
assert trading_client.call_args.kwargs["retry_count"] == 2
assert (
trading_client.call_args.kwargs["config"].path == "/opt/mt5/terminal64.exe"
)
mock_client.shutdown.assert_called_once()
def test_shuts_down_when_initialize_raises(
self,
mocker: MockerFixture,
) -> None:
"""Test shutdown is called when initialization fails."""
mock_client = MagicMock()
mock_client.initialize_and_login_mt5.side_effect = Mt5RuntimeError("boom")
mocker.patch("mt5cli.trading.Mt5TradingClient", return_value=mock_client)
with pytest.raises(Mt5RuntimeError, match="boom"), mt5_trading_session():
pass
mock_client.shutdown.assert_called_once()
def test_shuts_down_when_body_raises(self, mocker: MockerFixture) -> None:
"""Test shutdown is called when the context body raises."""
mock_client = MagicMock()
mocker.patch("mt5cli.trading.Mt5TradingClient", return_value=mock_client)
body_error = "body error"
with pytest.raises(RuntimeError, match=body_error), mt5_trading_session():
raise RuntimeError(body_error)
mock_client.shutdown.assert_called_once()
-480
View File
@@ -1,480 +0,0 @@
"""Tests for mt5cli.utils module."""
from __future__ import annotations
import json
import sqlite3
from datetime import UTC, datetime
from typing import TYPE_CHECKING
import pandas as pd
import pytest
if TYPE_CHECKING:
from pathlib import Path
from mt5cli.utils import (
DATETIME_TYPE,
REQUEST_TYPE,
TICK_FLAG_MAP,
TICK_FLAGS_TYPE,
TIMEFRAME_MAP,
TIMEFRAME_TYPE,
Dataset,
IfExists,
detect_format,
export_dataframe,
export_dataframe_to_sqlite,
parse_datetime,
parse_request,
parse_tick_flags,
parse_timeframe,
)
# ---------------------------------------------------------------------------
# detect_format
# ---------------------------------------------------------------------------
class TestDetectFormat:
"""Tests for detect_format."""
def test_explicit_format_returned(self, tmp_path: Path) -> None:
"""Test that explicit format overrides extension."""
result = detect_format(tmp_path / "data.txt", explicit_format="csv")
assert result == "csv"
@pytest.mark.parametrize(
("filename", "expected"),
[
("data.csv", "csv"),
("data.json", "json"),
("data.parquet", "parquet"),
("data.pq", "parquet"),
("data.db", "sqlite3"),
("data.sqlite", "sqlite3"),
("data.sqlite3", "sqlite3"),
("DATA.CSV", "csv"),
("DATA.JSON", "json"),
("DATA.PARQUET", "parquet"),
],
)
def test_auto_detect_from_extension(
self,
tmp_path: Path,
filename: str,
expected: str,
) -> None:
"""Test format auto-detection from file extension."""
result = detect_format(tmp_path / filename)
assert result == expected
def test_unknown_extension_raises(self, tmp_path: Path) -> None:
"""Test that unknown extension raises ValueError."""
with pytest.raises(ValueError, match="Cannot detect format"):
detect_format(tmp_path / "data.xyz")
# ---------------------------------------------------------------------------
# export_dataframe
# ---------------------------------------------------------------------------
class TestExportDataframe:
"""Tests for export_dataframe."""
@pytest.fixture
def sample_df(self) -> pd.DataFrame:
"""Create a sample DataFrame for testing."""
return pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
def test_export_csv(self, tmp_path: Path, sample_df: pd.DataFrame) -> None:
"""Test CSV export."""
output = tmp_path / "out.csv"
export_dataframe(sample_df, output, "csv")
result = pd.read_csv(output)
pd.testing.assert_frame_equal(result, sample_df)
def test_export_json(self, tmp_path: Path, sample_df: pd.DataFrame) -> None:
"""Test JSON export."""
output = tmp_path / "out.json"
export_dataframe(sample_df, output, "json")
with output.open() as f:
records = json.load(f)
assert len(records) == 3
assert records[0]["a"] == 1
def test_export_parquet(self, tmp_path: Path, sample_df: pd.DataFrame) -> None:
"""Test Parquet export."""
output = tmp_path / "out.parquet"
export_dataframe(sample_df, output, "parquet")
result = pd.read_parquet(output)
pd.testing.assert_frame_equal(result, sample_df)
def test_export_sqlite3(self, tmp_path: Path, sample_df: pd.DataFrame) -> None:
"""Test SQLite3 export."""
output = tmp_path / "out.db"
export_dataframe(sample_df, output, "sqlite3", table_name="test_table")
with sqlite3.connect(output) as conn:
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
"SELECT * FROM test_table",
conn,
)
pd.testing.assert_frame_equal(result, sample_df)
def test_unsupported_format_raises(
self,
tmp_path: Path,
sample_df: pd.DataFrame,
) -> None:
"""Test that unsupported format raises ValueError."""
with pytest.raises(ValueError, match="Unsupported output format"):
export_dataframe(sample_df, tmp_path / "out.txt", "xml")
class TestExportDataframeToSqlite:
"""Tests for export_dataframe_to_sqlite."""
def test_append_preserves_existing_rows(self, tmp_path: Path) -> None:
"""Test append mode keeps prior rows in the SQLite table."""
output = tmp_path / "append.db"
first = pd.DataFrame({"id": [1], "value": ["a"]})
second = pd.DataFrame({"id": [2], "value": ["b"]})
export_dataframe_to_sqlite(first, output, "items", if_exists=IfExists.REPLACE)
export_dataframe_to_sqlite(second, output, "items", if_exists=IfExists.APPEND)
with sqlite3.connect(output) as conn:
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
"SELECT id, value FROM items ORDER BY id",
conn,
)
pd.testing.assert_frame_equal(
result,
pd.DataFrame({"id": [1, 2], "value": ["a", "b"]}),
)
def test_deduplicate_keeps_latest_row(self, tmp_path: Path) -> None:
"""Test deduplication keeps the latest ROWID for key columns."""
output = tmp_path / "dedup.db"
first = pd.DataFrame({
"symbol": ["EURUSD", "EURUSD"],
"time": ["2024-01-01", "2024-01-01"],
"bid": [1.0, 1.1],
})
second = pd.DataFrame({
"symbol": ["EURUSD"],
"time": ["2024-01-01"],
"bid": [1.2],
})
export_dataframe_to_sqlite(
first,
output,
"ticks",
if_exists=IfExists.REPLACE,
deduplicate_on=("symbol", "time"),
)
export_dataframe_to_sqlite(
second,
output,
"ticks",
if_exists=IfExists.APPEND,
deduplicate_on=("symbol", "time"),
)
with sqlite3.connect(output) as conn:
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
"SELECT symbol, time, bid FROM ticks",
conn,
)
pd.testing.assert_frame_equal(
result.reset_index(drop=True),
pd.DataFrame({
"symbol": ["EURUSD"],
"time": ["2024-01-01"],
"bid": [1.2],
}),
)
def test_default_if_exists_appends_without_dropping_rows(
self,
tmp_path: Path,
) -> None:
"""Test the default append mode keeps prior rows."""
output = tmp_path / "default-append.db"
first = pd.DataFrame({"id": [1], "value": ["a"]})
second = pd.DataFrame({"id": [2], "value": ["b"]})
export_dataframe_to_sqlite(first, output, "items")
export_dataframe_to_sqlite(second, output, "items")
with sqlite3.connect(output) as conn:
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
"SELECT id, value FROM items ORDER BY id",
conn,
)
pd.testing.assert_frame_equal(
result,
pd.DataFrame({"id": [1, 2], "value": ["a", "b"]}),
)
def test_writes_index_with_label(self, tmp_path: Path) -> None:
"""Test optional index export with a custom label."""
output = tmp_path / "index.db"
frame = pd.DataFrame(
{"value": [1.0]}, index=pd.Index(["EURUSD"], name="symbol")
)
export_dataframe_to_sqlite(
frame,
output,
"margins",
if_exists=IfExists.REPLACE,
index=True,
index_label="symbol",
)
with sqlite3.connect(output) as conn:
result = pd.read_sql( # type: ignore[reportUnknownMemberType]
"SELECT symbol, value FROM margins",
conn,
)
pd.testing.assert_frame_equal(
result,
pd.DataFrame({"symbol": ["EURUSD"], "value": [1.0]}),
)
# ---------------------------------------------------------------------------
# Parse helpers
# ---------------------------------------------------------------------------
class TestParseDatetime:
"""Tests for parse_datetime."""
def test_valid_date(self) -> None:
"""Test parsing a date string."""
result = parse_datetime("2024-01-15")
assert result == datetime(2024, 1, 15, tzinfo=UTC)
def test_valid_datetime_with_tz(self) -> None:
"""Test parsing a datetime with timezone."""
result = parse_datetime("2024-01-15T12:00:00+00:00")
assert result == datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
def test_invalid_format_raises(self) -> None:
"""Test that invalid format raises ValueError."""
with pytest.raises(ValueError, match="Invalid datetime"):
parse_datetime("not-a-date")
class TestParseTimeframe:
"""Tests for parse_timeframe."""
@pytest.mark.parametrize(
("value", "expected"),
[("M1", 1), ("h1", 16385), ("D1", 16408), ("MN1", 49153)],
)
def test_named_timeframe(self, value: str, expected: int) -> None:
"""Test parsing named timeframes."""
assert parse_timeframe(value) == expected
def test_integer_timeframe(self) -> None:
"""Test parsing supported integer timeframes."""
assert parse_timeframe("1") == 1
assert parse_timeframe(16385) == 16385
def test_unsupported_integer_timeframe_raises(self) -> None:
"""Test that unsupported integer timeframes raise ValueError."""
with pytest.raises(ValueError, match="Invalid timeframe"):
parse_timeframe("42")
def test_invalid_timeframe_raises(self) -> None:
"""Test that invalid timeframe raises ValueError."""
with pytest.raises(ValueError, match="Invalid timeframe"):
parse_timeframe("INVALID")
class TestParseTickFlags:
"""Tests for parse_tick_flags."""
@pytest.mark.parametrize(
("value", "expected"),
[("ALL", -1), ("info", 1), ("TRADE", 2), ("COPY_TICKS_ALL", -1)],
)
def test_named_flag(self, value: str, expected: int) -> None:
"""Test parsing named tick flags."""
assert parse_tick_flags(value) == expected
def test_integer_flag(self) -> None:
"""Test parsing supported integer tick flags."""
assert parse_tick_flags("-1") == -1
assert parse_tick_flags(2) == 2
def test_unsupported_integer_flag_raises(self) -> None:
"""Test that unsupported integer tick flags raise ValueError."""
with pytest.raises(ValueError, match="Invalid tick flags"):
parse_tick_flags("7")
def test_invalid_flag_raises(self) -> None:
"""Test that invalid flag raises ValueError."""
with pytest.raises(ValueError, match="Invalid tick flags"):
parse_tick_flags("INVALID")
# ---------------------------------------------------------------------------
# parse_request
# ---------------------------------------------------------------------------
class TestParseRequest:
"""Tests for parse_request."""
def test_inline_json(self) -> None:
"""Test parsing an inline JSON object string."""
result = parse_request('{"action": 1, "symbol": "EURUSD"}')
assert result == {"action": 1, "symbol": "EURUSD"}
def test_file_reference(self, tmp_path: Path) -> None:
"""Test parsing JSON from a file via the @path syntax."""
path = tmp_path / "req.json"
path.write_text('{"action": 2}', encoding="utf-8")
result = parse_request(f"@{path}")
assert result == {"action": 2}
def test_invalid_json_raises(self) -> None:
"""Test that invalid JSON raises ValueError."""
with pytest.raises(ValueError, match="Invalid JSON request"):
parse_request("not json")
def test_non_object_raises(self) -> None:
"""Test that a non-object JSON raises ValueError."""
with pytest.raises(ValueError, match="must be a JSON object"):
parse_request("[1, 2, 3]")
def test_missing_file_raises(self, tmp_path: Path) -> None:
"""Test that a missing request file raises ValueError."""
path = tmp_path / "missing.json"
with pytest.raises(ValueError, match="Failed to read JSON request file"):
parse_request(f"@{path}")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
class TestConstants:
"""Tests for module constants."""
def test_timeframe_map_has_expected_keys(self) -> None:
"""Test that TIMEFRAME_MAP contains standard timeframes."""
for key in ("M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1", "MN1"):
assert key in TIMEFRAME_MAP
def test_tick_flag_map_has_expected_keys(self) -> None:
"""Test that TICK_FLAG_MAP contains standard flags with MT5 values."""
assert {"ALL", "INFO", "TRADE"} <= set(TICK_FLAG_MAP)
assert TICK_FLAG_MAP["ALL"] == -1
assert TICK_FLAG_MAP["INFO"] == 1
assert TICK_FLAG_MAP["TRADE"] == 2
@pytest.mark.parametrize(
("dataset", "expected"),
[
(Dataset.rates, "rates"),
(Dataset.ticks, "ticks"),
(Dataset.history_orders, "history_orders"),
(Dataset.history_deals, "history_deals"),
],
)
def test_dataset_table_name(self, dataset: Dataset, expected: str) -> None:
"""Test dataset SQLite table names."""
assert dataset.table_name == expected
# ---------------------------------------------------------------------------
# Click ParamTypes
# ---------------------------------------------------------------------------
class TestDateTimeType:
"""Tests for _DateTimeType."""
def test_convert_string(self) -> None:
"""Test converting a string to datetime."""
result = DATETIME_TYPE.convert("2024-06-15", None, None)
assert result == datetime(2024, 6, 15, tzinfo=UTC)
def test_convert_datetime_passthrough(self) -> None:
"""Test that datetime values pass through unchanged."""
dt = datetime(2024, 1, 1, tzinfo=UTC)
assert DATETIME_TYPE.convert(dt, None, None) is dt
def test_convert_invalid(self) -> None:
"""Test that invalid values raise BadParameter."""
with pytest.raises(Exception, match="Invalid datetime"):
DATETIME_TYPE.convert("bad", None, None)
class TestTimeframeType:
"""Tests for _TimeframeType."""
def test_convert_string(self) -> None:
"""Test converting a string to timeframe integer."""
assert TIMEFRAME_TYPE.convert("H1", None, None) == 16385
def test_convert_int(self) -> None:
"""Test converting supported integer timeframe values."""
assert TIMEFRAME_TYPE.convert(16385, None, None) == 16385
def test_convert_unsupported_int(self) -> None:
"""Test that unsupported integer values raise BadParameter."""
with pytest.raises(Exception, match="Invalid timeframe"):
TIMEFRAME_TYPE.convert(42, None, None)
def test_convert_invalid(self) -> None:
"""Test that invalid values raise BadParameter."""
with pytest.raises(Exception, match="Invalid timeframe"):
TIMEFRAME_TYPE.convert("bad", None, None)
@pytest.mark.parametrize("value", [True, False, None, 1.5])
def test_convert_invalid_types(self, value: object) -> None:
"""Test that bool, float, and None values raise BadParameter."""
with pytest.raises(Exception, match="Invalid timeframe"):
TIMEFRAME_TYPE.convert(value, None, None)
class TestTickFlagsType:
"""Tests for _TickFlagsType."""
def test_convert_string(self) -> None:
"""Test converting a string to tick flags integer."""
assert TICK_FLAGS_TYPE.convert("ALL", None, None) == -1
def test_convert_int(self) -> None:
"""Test converting supported integer tick flag values."""
assert TICK_FLAGS_TYPE.convert(2, None, None) == 2
def test_convert_unsupported_int(self) -> None:
"""Test that unsupported integer values raise BadParameter."""
with pytest.raises(Exception, match="Invalid tick flags"):
TICK_FLAGS_TYPE.convert(7, None, None)
@pytest.mark.parametrize("value", [True, False, None, 1.5])
def test_convert_invalid_types(self, value: object) -> None:
"""Test that bool, float, and None values raise BadParameter."""
with pytest.raises(Exception, match="Invalid tick flags"):
TICK_FLAGS_TYPE.convert(value, None, None)
def test_convert_invalid(self) -> None:
"""Test that invalid values raise BadParameter."""
with pytest.raises(Exception, match="Invalid tick flags"):
TICK_FLAGS_TYPE.convert("bad", None, None)
class TestRequestType:
"""Tests for _RequestType."""
def test_convert_string(self) -> None:
"""Test converting a JSON string to a request dictionary."""
assert REQUEST_TYPE.convert('{"action": 1}', None, None) == {"action": 1}
def test_convert_invalid(self) -> None:
"""Test that invalid values raise BadParameter."""
with pytest.raises(Exception, match="Invalid JSON request"):
REQUEST_TYPE.convert("bad", None, None)
Generated
-1161
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.