Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a95ade1800 | |||
| f11eded3c1 | |||
| c3b527fe5e | |||
| 29a3ddd8b7 | |||
| ef7e9ef5d2 | |||
| 569abbb5d0 | |||
| e33c0a9e63 | |||
| ed76a412a1 | |||
| d77cf5211b | |||
| 2caed332ff | |||
| 2614ef8f2c | |||
| f27febb6f5 | |||
| bba1008be8 | |||
| 7dc097cdf6 | |||
| 938b7eda5e | |||
| d3dabad610 | |||
| b2354b0429 | |||
| d3b21df369 | |||
| 40ab7ee96f | |||
| f909f5d05f | |||
| cc016044ca | |||
| a228b52245 | |||
| da5afc496b | |||
| 3f804a3064 | |||
| 62063bbf22 | |||
| 2ec68691e8 | |||
| 6e5c775f68 | |||
| e171fe2864 | |||
| 39a883c279 | |||
| e034d43720 | |||
| 3ee7140302 | |||
| 8eeac933a7 | |||
| e962a6adbf | |||
| 35ac854ab0 | |||
| 74a6dbf9dc | |||
| e5235a4f7a | |||
| 5bf54fa0dc | |||
| 35c79d7fbb | |||
| ed45281274 | |||
| 206f5fd771 | |||
| 57a53f9c8c | |||
| ea951c615c | |||
| 947bc45fe3 | |||
| 788b22a85b | |||
| dbf11de061 | |||
| 0d9226d875 | |||
| a70021a72e | |||
| e7c6c0b25f | |||
| 8cfcb983fe | |||
| 3eb51a1a12 | |||
| 6fe33bae25 | |||
| 45026a9e61 | |||
| 03b8599ae3 | |||
| d4787abe3d | |||
| 4eb2fcd07e | |||
| e9180ad565 | |||
| 42f16af1aa | |||
| 019c16a261 | |||
| b0be48c9ba | |||
| 2722447ae6 | |||
| 8994954964 | |||
| 83d6643f1e | |||
| 55940af10b | |||
| f10e14d775 | |||
| d68a2ac7c6 | |||
| b2e216edd1 | |||
| fad800632a | |||
| 1de77405de | |||
| 19df5ea4dc | |||
| 12d776c8ff | |||
| a58f209556 | |||
| 9e808ad915 | |||
| cc4895b72a | |||
| b6cab3da7c | |||
| a67a2c732e | |||
| c1bfabacb2 | |||
| 626bdad318 |
@@ -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.
|
||||
@@ -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 +0,0 @@
|
||||
../../skills/mt5cli
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
name: pr-feedback-triage
|
||||
description: Triage pull request review comments into fixes, replies, clarification requests, or open follow-ups while respecting safe execution modes.
|
||||
---
|
||||
|
||||
# PR Feedback Triage
|
||||
|
||||
Triage pull request review feedback, decide what action each thread needs, make focused fixes when allowed, and report or resolve only what is actually handled.
|
||||
|
||||
## When to Use
|
||||
|
||||
- A PR has review comments, requested changes, unresolved review threads, or bot review findings.
|
||||
- The user asks to address, respond to, or resolve PR feedback.
|
||||
- The user provides a PR URL/number, a branch with an associated PR, or copied comments.
|
||||
|
||||
Do not use this skill for a first-pass code review with no existing feedback; use a code review skill instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Pull request URL or number, or a current branch that has an associated pull request.
|
||||
- Repository checkout or platform access sufficient to inspect the PR diff and review feedback.
|
||||
- Optional reviewer priorities from the user, such as "only address blocking comments" or "do not reply on the PR platform".
|
||||
- Optional operating mode flags: `dry_run`, `no_push`, and `no_reply`.
|
||||
|
||||
If no PR or review comments are identifiable, ask for the target PR or the copied comments before proceeding.
|
||||
|
||||
## Modes
|
||||
|
||||
- `dry_run`: inspect review feedback and report the triage only. Do not edit files, run write-mode formatters, commit, push, post replies, or resolve review threads.
|
||||
- `no_push`: local edits and verification are allowed, but do not push commits or otherwise update the remote branch. Report the local diff or local commits that still need to be pushed. Do not resolve threads whose resolution depends on unpushed local edits.
|
||||
- `no_reply`: do not post replies, submit reviews, or resolve review threads. Provide suggested replies and resolution actions in the final report instead.
|
||||
|
||||
When a mode disables an action, skip that destructive or externally visible action even if normal workflow text would otherwise allow it.
|
||||
|
||||
## Preflight
|
||||
|
||||
1. Identify the current branch and target PR.
|
||||
2. Check tracked local changes with `git diff --name-only` and `git diff --cached --name-only`. Ignore untracked files unless the review feedback explicitly concerns them.
|
||||
3. Check unpushed commits before relying on remote review feedback.
|
||||
4. If tracked local changes or unpushed commits exist, warn that existing PR comments may not cover the latest local state. In `normal` mode, push only when the user request or repository workflow allows it; otherwise continue with a clearly reported limitation.
|
||||
|
||||
## Feedback Collection
|
||||
|
||||
Gather the complete feedback set before editing:
|
||||
|
||||
- Fetch unresolved review threads, requested-change reviews, PR-level summary comments, and copied comments.
|
||||
- Use platform-native APIs/CLI when available. Paginate results; do not inspect only the first page of threads or comments.
|
||||
- For bot reviewers that post both summary comments and inline comments, collect both. Summary comments often contain severity, rationale, and fix instructions; inline comments contain the exact file and line context.
|
||||
- Preserve every thread/comment identifier needed to reply or resolve later.
|
||||
- Compare each comment with the current diff and file contents because review lines can become outdated.
|
||||
|
||||
## Deduplication and Ordering
|
||||
|
||||
Build one triage record per distinct finding:
|
||||
|
||||
- Prefer exact review-thread identity when available.
|
||||
- For duplicate bot findings appearing in both summary and inline comments, merge by exact issue title first, then by file path plus line range as a fallback.
|
||||
- Prefer inline comments for location and current code context.
|
||||
- Prefer summary comments for severity, category, rationale, and detailed agent prompts.
|
||||
- Preserve the reviewer’s exact issue title and original wording where practical. Do not rename findings in a way that would make replies hard to map back to comments.
|
||||
- Preserve the reviewer’s original ordering unless the user asks for priority reordering. Many review bots already order findings by severity.
|
||||
|
||||
Each triage record should track: original title, reviewer, source IDs, location, current applicability, severity/priority if available, disposition, planned action, verification, reply text if any, resolution decision, platform action attempted, and final platform state.
|
||||
|
||||
## Resolution Policy
|
||||
|
||||
In normal mode, `Resolve conversation` is the default action for any review thread that has been fully handled. A thread is handled when the requested change is implemented and verified, the current code already satisfies the comment, the comment is outdated and no longer applies, or a deliberate deferral/won't-fix response has been posted with a clear reason.
|
||||
|
||||
Keep a thread open only when it still needs reviewer, maintainer, or product input, the fix is local-only and not pushed, verification is missing for a material change, or the user explicitly requested `dry_run`, `no_push`, or `no_reply` behavior that prevents resolution.
|
||||
|
||||
When resolving a thread, add a concise reply first only if it provides useful context, such as what changed, why no code change was needed, why a finding was intentionally deferred, or why the original comment is now outdated. Do not add noisy replies for self-evident fixes unless project norms require them.
|
||||
|
||||
## Platform Action Contract
|
||||
|
||||
Do not treat triage as complete until every collected source ID reaches an explicit terminal state:
|
||||
|
||||
- `resolved`: a platform resolve action succeeded, or a re-check shows the thread is already resolved.
|
||||
- `replied_left_open`: a reply or question was posted and the thread is intentionally left unresolved.
|
||||
- `not_resolvable`: the source is a PR-level summary comment or copied comment that has no platform-level resolve action; reply or post a PR summary when useful.
|
||||
- `skipped_by_mode`: `dry_run`, `no_push`, or `no_reply` prevented the external action.
|
||||
- `failed_action`: a reply or resolve action was attempted and failed; include the attempted action and failure in the final summary.
|
||||
|
||||
In normal mode, build and execute a platform action queue after fixes are verified and pushed when needed:
|
||||
|
||||
- `reply_then_resolve`: use for handled threads where the reviewer needs context before resolution.
|
||||
- `resolve_only`: use for self-evident fixes and already-addressed or outdated threads where an extra reply would add noise.
|
||||
- `reply_leave_open`: use only for clarification requests, blocked work, or intentionally open follow-ups.
|
||||
- `reply_only`: use for PR-level comments or summaries that cannot be resolved as review threads.
|
||||
|
||||
For duplicate findings, execute the terminal action for every source thread ID, not only the primary triage record. If one finding is represented by three unresolved inline threads, all three must be resolved or explicitly left open.
|
||||
|
||||
## GitHub Action Guidance
|
||||
|
||||
Prefer platform-native APIs or `gh` commands that expose review-thread resolution state. For GitHub inline review threads, use the thread node ID and the GraphQL `resolveReviewThread` mutation rather than assuming that a reply resolves the conversation.
|
||||
|
||||
A reliable pattern is:
|
||||
|
||||
1. Re-fetch review threads and comments immediately before acting.
|
||||
2. Reply to the thread when the action queue says a reply is needed.
|
||||
3. Resolve the review thread by node ID when the terminal state should be `resolved`.
|
||||
4. Re-fetch unresolved review threads after the action queue completes.
|
||||
5. Retry any expected-to-be-resolved thread that is still unresolved once; if it still remains unresolved, mark it `failed_action` instead of claiming completion.
|
||||
|
||||
Example GraphQL mutation shape:
|
||||
|
||||
```graphql
|
||||
mutation ($threadId: ID!) {
|
||||
resolveReviewThread(input: { threadId: $threadId }) {
|
||||
thread {
|
||||
id
|
||||
isResolved
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A posted reply alone is sufficient only for `reply_leave_open`, `reply_only`, or `not_resolvable` sources. For handled inline review threads, reply and resolve are separate actions.
|
||||
|
||||
## Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Identify PR and branch state] --> B[Collect all review feedback]
|
||||
B --> C[Deduplicate and preserve source IDs]
|
||||
C --> D[Inspect current diff and code]
|
||||
D --> E{Classify each triage record}
|
||||
E -->|Fix| F[Implement minimal change]
|
||||
E -->|Answer| G[Prepare concise reply]
|
||||
E -->|Clarify| H[Prepare question and leave open]
|
||||
E -->|Already addressed or Outdated| I[Prepare evidence]
|
||||
E -->|Defer or Won't fix| J[Document reason]
|
||||
F --> K[Verify]
|
||||
G --> L{Mode}
|
||||
H --> L
|
||||
I --> L
|
||||
J --> L
|
||||
K --> L
|
||||
L -->|dry_run| M[Report triage only]
|
||||
L -->|no_push| N[Report local diff or commits]
|
||||
L -->|no_reply| O[Report suggested replies/actions]
|
||||
L -->|normal| P[Commit/push if changed]
|
||||
P --> R[Execute reply/resolve action queue]
|
||||
R --> S[Re-fetch threads and retry unresolved handled threads once]
|
||||
M --> Q[Final summary]
|
||||
N --> Q
|
||||
O --> Q
|
||||
S --> Q
|
||||
```
|
||||
|
||||
## Compact Workflow
|
||||
|
||||
1. **Collect all relevant feedback**
|
||||
- Identify the PR and gather unresolved review threads, requested-change reviews, PR-level summaries, inline comments, and copied comments.
|
||||
- Paginate all platform calls and keep comment/thread IDs for later replies and resolution.
|
||||
- For bot reviews, collect both summary and inline comments, then merge duplicates rather than fixing the same finding twice.
|
||||
|
||||
2. **Classify each triage record**
|
||||
- **Fix**: Valid requested change; make the smallest focused edit when not in `dry_run`.
|
||||
- **Answer**: No code change needed; prepare a concise explanation.
|
||||
- **Clarify**: Ambiguous, conflicting, or missing context; reply with the question and leave unresolved.
|
||||
- **Already addressed**: Current code already satisfies it; prepare evidence.
|
||||
- **Outdated**: Commented code or issue no longer exists; prepare evidence.
|
||||
- **Defer / Won't fix**: Valid concern intentionally not changed now; document a specific reason.
|
||||
|
||||
3. **Act according to the classification and mode**
|
||||
- Keep edits scoped to the review feedback.
|
||||
- Follow reviewer-provided fix instructions literally when they are still applicable; deviate only when the current code proves the instruction is stale or unsafe.
|
||||
- In `dry_run`, stop at triage, proposed fixes, suggested replies, and verification plan.
|
||||
- In `no_push`, local edits are allowed, but do not push or resolve threads whose fix is only local. Reply or resolve non-code, already-addressed, or outdated threads only when the action does not depend on unpushed work and `no_reply` is not set.
|
||||
- In `no_reply`, do not post replies or resolve threads; report suggested replies/actions instead.
|
||||
- In normal mode, commit and push changed code when appropriate, then execute the platform action queue for every collected source ID.
|
||||
|
||||
4. **Verify before claiming completion**
|
||||
- For fixes, run appropriate checks or explain why they could not run.
|
||||
- Re-inspect the updated diff and comment context to confirm the concern is resolved.
|
||||
- Re-fetch review threads after reply/resolve actions and confirm all expected-to-be-resolved thread IDs are resolved.
|
||||
- Do not mark a thread resolved if it still needs reviewer, maintainer, or product input.
|
||||
- If a resolve or reply operation fails, retry once when safe; then report `failed_action` with the affected source ID and reason.
|
||||
|
||||
5. **Finish**
|
||||
- Normal mode: commit/push changes when appropriate, post useful replies or a summary, resolve all handled threads by default, and reconcile the final unresolved set.
|
||||
- Safe modes: report the local state and the exact replies/resolution actions a human could take.
|
||||
|
||||
## Reply Guidance
|
||||
|
||||
- Keep inline replies short and tied to the original title or concern.
|
||||
- For fixed findings, mention the concrete change or commit if useful.
|
||||
- For already-addressed or outdated findings, cite the current code path or behavior that makes the finding no longer applicable.
|
||||
- For deferred or won't-fix findings, provide the reason and any follow-up issue or owner if known.
|
||||
- If a reply or resolve operation fails, continue with the remaining threads and report the failure in the final summary.
|
||||
|
||||
## Final Summary Checklist
|
||||
|
||||
- Mode used: `normal`, `dry_run`, `no_push`, or `no_reply`
|
||||
- Counts by disposition: fixed, answered, clarified/left open, already addressed, outdated, deferred/won't-fix
|
||||
- Counts by platform terminal state: resolved, replied-left-open, not-resolvable, skipped-by-mode, failed-action
|
||||
- Threads resolved, intentionally left open, already resolved, or resolution actions skipped by mode
|
||||
- Any expected-to-be-resolved thread that remained unresolved after retry
|
||||
- Verification run or planned
|
||||
- Commits pushed, local diff/commits, or "none"
|
||||
- Remaining open items and who needs to respond
|
||||
@@ -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.
|
||||
@@ -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 +0,0 @@
|
||||
../.agents/skills
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
github:
|
||||
- dceoy
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,92 +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 }}
|
||||
github-codeql-analysis:
|
||||
if: >
|
||||
github.event_name == 'push'
|
||||
|| github.event_name == 'pull_request'
|
||||
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
uses: dceoy/gh-actions-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
language: >
|
||||
["python"]
|
||||
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
|
||||
checks: read
|
||||
statuses: read
|
||||
with:
|
||||
unconditional: true
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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__/
|
||||
@@ -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>
|
||||
@@ -1,37 +0,0 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
`mt5cli/` contains the package source. Important modules include `cli.py` for the Typer command-line app, `client.py` and `sdk.py` for public MT5 client/session APIs, `history.py` for SQLite history collection, `storage.py` and `converters.py` for export behavior, and `schemas.py` for normalized dataset contracts. `tests/` holds pytest coverage for CLI behavior, SDK contracts, trading helpers, history, and utilities. `docs/` and `mkdocs.yml` define the MkDocs site and API reference. `skills/mt5cli/SKILL.md` documents the mt5cli agent skill.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `uv sync` installs runtime and development dependencies from `pyproject.toml` and `uv.lock`.
|
||||
- `uv run mt5cli --help` runs the local CLI entry point.
|
||||
- `uv run ruff format .` formats Python files.
|
||||
- `uv run ruff check --fix .` lints and applies safe fixes.
|
||||
- `uv run pyright .` runs strict type checking.
|
||||
- `uv run pytest` runs doctests, branch coverage, and the test suite.
|
||||
- `uv run mkdocs serve` previews documentation locally; `uv run mkdocs build` validates the docs build.
|
||||
|
||||
Use `.agents/skills/local-qa/SKILL.md` for pre-handoff QA. It runs `.agents/skills/local-qa/scripts/qa.sh`, which formats, lints, type-checks, tests, formats Markdown, and checks GitHub workflows.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Target Python `>=3.11,<3.14`. Use Ruff’s configured 88-character line length and Google-style docstrings. Pyright is strict, so prefer explicit public type annotations and narrow exception handling. Keep module, function, and variable names in `snake_case`; classes and enums use `PascalCase`. Preserve the package’s small, typed helper style rather than adding broad abstractions.
|
||||
|
||||
## Design Principles
|
||||
|
||||
Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest implementation that satisfies the current CLI/API contract. Remove duplication when shared behavior is already proven by at least two concrete call sites, but avoid generic helpers for speculative reuse. Do not add configuration flags, extension hooks, or alternate backends until a real repository use case requires them.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Tests use pytest, pytest-mock, doctests, and pytest-cov. Test files should match `tests/test_*.py`, classes `Test*`, and functions `test_*`. Coverage is configured with `fail_under = 100`, so add focused tests for every behavior change. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent history uses concise imperative commits, sometimes with conventional prefixes such as `feat:` or `chore:` and PR numbers appended by GitHub. Keep commits scoped to one logical change. Pull requests should describe behavior changes, note tests run, link related issues, and call out MT5/live-trading risk where relevant.
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
Never commit account credentials, broker passwords, exported private data, or local `.venv` contents. Treat `order_send` and CLI `order-send --yes` as live execution paths; gate examples and tests so they cannot place real trades accidentally.
|
||||
@@ -1,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.
|
||||
@@ -1,346 +0,0 @@
|
||||
# mt5cli
|
||||
|
||||
[](https://github.com/dceoy/mt5cli/actions/workflows/ci.yml)
|
||||
|
||||
Generic MT5 data and execution infrastructure for Python applications. Export from the CLI or import a small, stable Python API in downstream packages.
|
||||
|
||||
The [Public API Contract](docs/api/public-contract.md) lists stable SDK exports (`mt5cli.STABLE_SDK_EXPORTS`), CLI commands, internal helpers, and responsibilities that remain out of scope (strategy logic, backtests, optimization).
|
||||
|
||||
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** — public `MT5Client` API, standardized dataset schemas, storage helpers, CLI commands, and SQLite history collection 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
|
||||
```
|
||||
|
||||
Parquet export is not included by default. To enable it, install the `parquet` extra:
|
||||
|
||||
```bash
|
||||
pip install -U "mt5cli[parquet]" MetaTrader5
|
||||
```
|
||||
|
||||
## Python API (downstream packages)
|
||||
|
||||
Import `MT5Client` for generic MT5 data access, schema normalization, and optional order primitives.
|
||||
|
||||
```python
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from mt5cli import (
|
||||
DataKind,
|
||||
Dataset,
|
||||
MT5Client,
|
||||
build_config,
|
||||
collect_history,
|
||||
export_dataframe,
|
||||
mt5_session,
|
||||
normalize_dataframe,
|
||||
update_history_with_config,
|
||||
)
|
||||
|
||||
# Persistent session for multiple calls
|
||||
with mt5_session(build_config(login=12345, server="Broker-Demo")) as client:
|
||||
rates = client.copy_rates_range(
|
||||
"EURUSD",
|
||||
timeframe="H1",
|
||||
date_from="2024-01-01",
|
||||
date_to="2024-02-01",
|
||||
)
|
||||
positions = client.positions()
|
||||
check = client.order_check({"action": 1, "symbol": "EURUSD", "volume": 0.1})
|
||||
|
||||
# Normalize MT5 frames to the public schema contract before storage
|
||||
closed_rates = normalize_dataframe(
|
||||
rates, DataKind.rates, symbol="EURUSD", timeframe="H1"
|
||||
)
|
||||
export_dataframe(closed_rates, Path("rates.csv"), "csv")
|
||||
|
||||
# Bulk SQLite history (same behavior as collect-history CLI command)
|
||||
collect_history(
|
||||
Path("history.db"),
|
||||
symbols=["EURUSD"],
|
||||
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
||||
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
||||
datasets={Dataset.rates, Dataset.history_deals},
|
||||
)
|
||||
|
||||
# Incremental append for automated pipelines
|
||||
update_history_with_config(
|
||||
output="history.db",
|
||||
symbols=["EURUSD"],
|
||||
config=build_config(login=12345),
|
||||
)
|
||||
```
|
||||
|
||||
Schema contracts live in `mt5cli.schemas` (`DataKind`, `validate_schema`, `normalize_dataframe`). Storage helpers are re-exported from `mt5cli.storage` and the package root.
|
||||
|
||||
`MT5Client.order_send()` 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.
|
||||
|
||||
### Trading lifecycle and state helpers
|
||||
|
||||
Trading applications can depend on `mt5cli` imports only; terminal path,
|
||||
credentials, server, and timeout are forwarded to `pdmt5.Mt5Config`, numeric
|
||||
login strings are coerced to integers, and empty login strings are treated as
|
||||
unset. Pass `allow_whole_dollar_env=True` to expand `${ENV_VAR}` and bare
|
||||
`$ENV_NAME` placeholders in connection string parameters before coercion.
|
||||
|
||||
```python
|
||||
from mt5cli import (
|
||||
build_config,
|
||||
calculate_spread_ratio,
|
||||
create_trading_client,
|
||||
get_account_snapshot,
|
||||
mt5_trading_session,
|
||||
)
|
||||
|
||||
# Login from environment — numeric string is coerced to int automatically
|
||||
config = build_config(login="$MT5_LOGIN", allow_whole_dollar_env=True)
|
||||
|
||||
with mt5_trading_session(
|
||||
path=r"C:\Program Files\MetaTrader 5\terminal64.exe",
|
||||
login="12345",
|
||||
password="from-env-or-secret-store",
|
||||
server="Broker-Demo",
|
||||
) as client:
|
||||
account = get_account_snapshot(client)
|
||||
spread = calculate_spread_ratio(client, "EURUSD")
|
||||
|
||||
client = create_trading_client(login=12345, server="Broker-Demo")
|
||||
try:
|
||||
positions = client.positions_get_as_df(symbol="EURUSD")
|
||||
finally:
|
||||
client.shutdown()
|
||||
```
|
||||
|
||||
## CLI 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 raw trade request to the trade server (`--yes` required; expert path) |
|
||||
| `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available) |
|
||||
| `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`.
|
||||
`close-positions` is the safer high-level alternative that builds correct close
|
||||
requests automatically. At least one `--symbol` or `--ticket` must be provided.
|
||||
|
||||
### `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 downstream tools.
|
||||
- **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. For config dicts or nested structures loaded from YAML/TOML, use `substitute_mapping_values(data, keys={"login", "password"})` to expand placeholders only for caller-specified keys — key names are never hard-coded in mt5cli.
|
||||
- **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). Pass `update_backend` to inject a custom history update callable (same keyword arguments as `update_history`) instead of monkey-patching `mt5cli.sdk.update_history`.
|
||||
- **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. Keep read-only collection on `mt5_session()` / `MT5Client`.
|
||||
- **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 `MT5Client` 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 downstream trading apps
|
||||
|
||||
Replace local MT5 lifecycle and trading helper code with mt5cli imports:
|
||||
|
||||
```python
|
||||
# Before (local application 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 `MT5Client`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dceoy/mt5cli.git
|
||||
cd mt5cli
|
||||
uv sync
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
+3240
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
@@ -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">¶</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">¶</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">¶</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">'T'</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">¶</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">"Mt5CliError"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> <span class="s2">"Mt5ConnectionError"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> <span class="s2">"Mt5OperationError"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a> <span class="s2">"Mt5SchemaError"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a> <span class="s2">"call_with_normalized_errors"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a> <span class="s2">"is_recoverable_mt5_error"</span><span class="p">,</span>
|
||||
<a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a> <span class="s2">"normalize_mt5_exception"</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">¶</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">¶</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">¶</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">¶</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">¶</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">-></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">-></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">"""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"> """</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">¶</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">-></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">-></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">"""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"> """</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">¶</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">-></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">-></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">"""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"> """</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
@@ -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">¶</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">¶</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">¶</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["Downstream application"] --> Client["MT5Client"]
|
||||
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> CLI["mt5cli CLI"] --> Client
|
||||
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> Client --> SDK["sdk / pdmt5"]
|
||||
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a> Client --> Schemas["schemas"]
|
||||
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a> History["history SQLite"] --> Utils["utils export"]
|
||||
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a> SDK --> PDMT5["pdmt5.Mt5DataClient"]
|
||||
</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">¶</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">"EURUSD"</span><span class="p">,</span> <span class="s2">"H1"</span><span class="p">,</span> <span class="s2">"2024-01-01"</span><span class="p">,</span> <span class="s2">"2024-02-01"</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>
|
||||
@@ -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">¶</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 -> mt5cli -> pdmt5 -> MetaTrader 5
|
||||
</code></pre></div>
|
||||
<h2 id="responsibility-boundary">Responsibility boundary<a class="headerlink" href="#responsibility-boundary" title="Permanent link">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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&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&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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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
File diff suppressed because it is too large
Load Diff
@@ -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">¶</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">¶</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">¶</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">¶</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">-></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">-></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">"""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"> """</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">¶</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">"mt5cli"</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">-></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>'mt5cli'</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">"mt5cli"</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">-></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">"""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 "mt5cli[otel]"``.</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"> """</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">"opentelemetry-api is not installed. "</span>
|
||||
<a id="__codelineno-0-327" name="__codelineno-0-327"></a> <span class="s1">'Install it with: pip install "mt5cli[otel]"'</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">"opentelemetry-exporter-otlp-proto-http is required for the "</span>
|
||||
<a id="__codelineno-0-334" name="__codelineno-0-334"></a> <span class="s2">"default OTLP export pipeline. "</span>
|
||||
<a id="__codelineno-0-335" name="__codelineno-0-335"></a> <span class="s1">'Install it with: pip install "mt5cli[otel]" or pass a '</span>
|
||||
<a id="__codelineno-0-336" name="__codelineno-0-336"></a> <span class="s2">"custom readers list."</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">"service.name"</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">¶</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">-></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">-></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">"""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"> """</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">¶</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">'mt5cli[otel]'</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">"mt5cli"</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">¶</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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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; }
|
||||
}
|
||||
Vendored
+12
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -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}
|
||||
Vendored
+6
@@ -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}
|
||||
@@ -1,3 +0,0 @@
|
||||
# CLI Module
|
||||
|
||||
::: mt5cli.cli
|
||||
@@ -1,3 +0,0 @@
|
||||
# Client
|
||||
|
||||
::: mt5cli.client
|
||||
@@ -1,3 +0,0 @@
|
||||
# Converters
|
||||
|
||||
::: mt5cli.converters
|
||||
@@ -1,3 +0,0 @@
|
||||
# Exceptions
|
||||
|
||||
::: mt5cli.exceptions
|
||||
@@ -1,255 +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
|
||||
|
||||
The canonical normalized rate table is `rates`; compatibility views are named
|
||||
with `rate_<symbol>__<timeframe>` for single-timeframe symbols or
|
||||
`rate_<symbol>__<granularity>_<timeframe>` when a symbol has multiple stored
|
||||
timeframes. `resolve_rate_table_name()` returns `rates`, while
|
||||
`resolve_rate_view_name()` returns the per-symbol compatibility view name.
|
||||
|
||||
Use `load_rate_data()` or `load_rate_series_from_sqlite(..., table=...)` to load
|
||||
a single table or view from a SQLite path. Use
|
||||
`load_rate_series_by_granularity()` to load multiple instrument/granularity
|
||||
targets without hard-coding view names:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from mt5cli import (
|
||||
load_rate_data,
|
||||
load_rate_series_by_granularity,
|
||||
load_rate_series_from_sqlite,
|
||||
resolve_rate_table_name,
|
||||
)
|
||||
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)
|
||||
same_rates = load_rate_series_from_sqlite(Path("history.db"), table=view, count=1000)
|
||||
|
||||
table = resolve_rate_table_name("EURUSD", "M1") # "rates"
|
||||
series = load_rate_series_by_granularity(
|
||||
Path("history.db"),
|
||||
symbols=["EURUSD", "GBPUSD"],
|
||||
granularities=["M1", "H1"],
|
||||
count=500,
|
||||
)
|
||||
```
|
||||
|
||||
`count` returns the latest rows while preserving chronological order. Missing
|
||||
tables/views and mismatched `explicit_tables` lengths raise `ValueError` with
|
||||
the requested database target in the message.
|
||||
|
||||
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)
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
This section documents the mt5cli public Python API and CLI modules.
|
||||
|
||||
Start with the [Public API Contract](public-contract.md) for the stable
|
||||
downstream SDK surface, CLI boundary, internal modules, and out-of-scope strategy
|
||||
responsibilities.
|
||||
|
||||
## Public API layers
|
||||
|
||||
| Module | Purpose |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| [Public API Contract](public-contract.md) | Stable downstream SDK exports, CLI boundary, and out-of-scope items |
|
||||
| [Client](client.md) | `MT5Client` session abstraction for data access and order primitives |
|
||||
| [Schemas](schemas.md) | Canonical DataFrame contracts and normalization helpers |
|
||||
| [Storage](storage.md) | CSV/JSON/Parquet/SQLite export and history collection helpers |
|
||||
| [Converters](converters.md) | Symbol, timeframe, timezone, and date-range utilities |
|
||||
| [Exceptions](exceptions.md) | Stable mt5cli exception types and MT5 error normalization |
|
||||
| [SDK](sdk.md) | Module-level fetch helpers, multi-account collectors, incremental history |
|
||||
| [Trading](trading.md) | Trading-capable sessions and operational helpers |
|
||||
| [History Collection (SQLite)](history.md) | SQLite schema, incremental writes, dedup, and rate views |
|
||||
| [CLI](cli.md) | Typer commands that delegate to the Python API |
|
||||
| [Utils](utils.md) | Parsing helpers and Click parameter types |
|
||||
|
||||
## Architecture overview
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
App["Downstream application"] --> Client["MT5Client"]
|
||||
CLI["mt5cli CLI"] --> Client
|
||||
Client --> SDK["sdk / pdmt5"]
|
||||
Client --> Schemas["schemas"]
|
||||
Storage["storage"] --> History["history SQLite"]
|
||||
Storage --> Utils["utils export"]
|
||||
SDK --> PDMT5["pdmt5.Mt5DataClient"]
|
||||
```
|
||||
|
||||
Downstream packages should depend on the package root exports documented in the
|
||||
[Public API Contract](public-contract.md) (`MT5Client`,
|
||||
`DataKind`, `normalize_dataframe`, `collect_history`, `load_rate_data`,
|
||||
`resolve_rate_view_name`, etc.) rather than private modules.
|
||||
|
||||
`MT5Client.order_send()` 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.
|
||||
|
||||
## Quick start
|
||||
|
||||
```python
|
||||
from mt5cli import MT5Client, build_config, mt5_session
|
||||
|
||||
with mt5_session(build_config(login=12345)) as client:
|
||||
rates = client.copy_rates_range("EURUSD", "H1", "2024-01-01", "2024-02-01")
|
||||
positions = client.positions()
|
||||
```
|
||||
|
||||
```bash
|
||||
mt5cli -o account.csv account-info
|
||||
mt5cli -o rates.parquet rates-range --symbol EURUSD --timeframe H1 \
|
||||
--date-from 2024-01-01 --date-to 2024-02-01
|
||||
```
|
||||
|
||||
See individual module pages for detailed usage examples.
|
||||
@@ -1,237 +0,0 @@
|
||||
# Public API Contract
|
||||
|
||||
mt5cli is the generic MT5 data and execution infrastructure layer for downstream
|
||||
Python applications. The intended dependency direction is:
|
||||
|
||||
```text
|
||||
downstream app -> mt5cli -> pdmt5 -> MetaTrader 5
|
||||
```
|
||||
|
||||
Downstream packages should import from the package root (`from mt5cli import
|
||||
...`) and use the public tier sets in `mt5cli.contract` to distinguish API
|
||||
stability. CLI commands mirror the same behavior but are not importable Python
|
||||
APIs.
|
||||
|
||||
## Public API tiers
|
||||
|
||||
mt5cli classifies package-root imports by intended downstream use:
|
||||
|
||||
| Tier | Contract set | Meaning |
|
||||
| ---------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Stable core | `STABLE_SDK_EXPORTS` | Preferred SDK surface for downstream MT5 infrastructure adapters. Changes require a deliberate compatibility path. |
|
||||
| Secondary public | `SECONDARY_PUBLIC_EXPORTS` | Public helpers for CLI/export/schema integrations and lower-level MT5 wrappers. Importable, but less central to the downstream trading SDK. |
|
||||
|
||||
## Stable downstream SDK API
|
||||
|
||||
These names are exported from `mt5cli` and covered by the contract in
|
||||
`mt5cli.STABLE_SDK_EXPORTS` (defined in `mt5cli.contract`).
|
||||
|
||||
### Session lifecycle and configuration
|
||||
|
||||
| Symbol | Role |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `MT5Client` | Read-only data client with optional `order_check` / `order_send` |
|
||||
| `build_config` | Build `pdmt5.Mt5Config` from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
|
||||
| `mt5_session` | Context manager: initialize, login, yield client, shutdown |
|
||||
| `create_trading_client`, `mt5_trading_session` | Trading-capable `pdmt5.Mt5TradingClient` lifecycle |
|
||||
| `AccountSpec` | Generic account group: symbols plus optional credentials |
|
||||
| `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
|
||||
| `substitute_env_placeholders` | Replace `${NAME}` substrings from the environment; opt-in `allow_whole_dollar_env` for whole-value `$NAME` |
|
||||
| `substitute_mapping_values` | Recursively traverse a dict/list/scalar structure and substitute `${ENV_VAR}` placeholders for caller-selected mapping keys only; optionally normalise blank strings to `None` for a separate caller-selected key set; does not hard-code any application-specific key names |
|
||||
|
||||
Credential resolution is generic: any environment variable name may appear inside
|
||||
`${...}`. mt5cli does not hard-code application-specific keys such as
|
||||
`mt5_login` or `mt5_exe`.
|
||||
|
||||
Pass `allow_whole_dollar_env=True` to `substitute_env_placeholders()`,
|
||||
`substitute_mapping_values()`, `resolve_account_spec()`, `resolve_account_specs()`,
|
||||
and `build_config()` to additionally expand strings whose entire value is a bare
|
||||
`$ENV_NAME` identifier.
|
||||
Partial strings such as `"plan$pass"`, `"abc$ENV"`, or `"$ENV-suffix"` are
|
||||
**never** expanded — only an exact `$IDENTIFIER` whole-string match qualifies.
|
||||
Default is `False` to preserve backward compatibility.
|
||||
|
||||
### Closed-bar rate helpers
|
||||
|
||||
MetaTrader 5 returns the still-forming bar as the last row when
|
||||
`start_pos=0`. Use these helpers instead of reimplementing bar trimming or
|
||||
timestamp normalization in downstream apps.
|
||||
|
||||
| Symbol | Role |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `drop_forming_rate_bar` | Remove the last row from chronologically ordered rate data |
|
||||
| `fetch_latest_closed_rates` | Single connected client: fetch `count + 1`, drop forming bar |
|
||||
| `fetch_latest_closed_rates_for_trading_client` | Closed bars from an active `Mt5TradingClient` session; returns RangeIndex |
|
||||
| `fetch_latest_closed_rates_indexed` | Same as above but returns a UTC `DatetimeIndex` named `"time"` (no time column) |
|
||||
| `collect_latest_closed_rates_for_accounts` | Multi-account closed bars with optional retry wrapper |
|
||||
| `collect_latest_closed_rates_by_granularity` | Same data keyed by `(symbol, granularity_name)` |
|
||||
| `collect_latest_rates_for_accounts_with_retries` | Bounded exponential backoff for transient MT5 errors |
|
||||
|
||||
### SQLite history collection and rate loading
|
||||
|
||||
| Symbol | Role |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `collect_history` | One-shot date-range export into SQLite |
|
||||
| `update_history`, `update_history_with_config` | Incremental append from `MAX(time)` cursors |
|
||||
| `ThrottledHistoryUpdater` | Minimum interval between successful incremental updates; optional `update_backend` injection |
|
||||
| `resolve_history_datasets`, `resolve_history_timeframes`, `resolve_history_tick_flags` | History pipeline configuration |
|
||||
| `build_rate_view_name`, `resolve_rate_table_name`, `resolve_rate_view_name`, `resolve_rate_view_names`, `resolve_rate_tables` | Map symbols/timeframes to mt5cli-managed table or view names |
|
||||
| `RateTarget`, `build_rate_targets` | Neutral `(symbol, timeframe)` series descriptors |
|
||||
| `load_rate_data`, `load_rate_data_from_connection` | Load one table/view into a time-indexed DataFrame |
|
||||
| `load_rate_series_from_sqlite`, `load_rate_series_by_granularity` | Load one or many series; fail clearly when managed views are missing |
|
||||
|
||||
Pass `require_existing=True` to rate view resolution helpers when downstream
|
||||
code must fail instead of receiving a best-guess view name. Multi-series loaders
|
||||
require existing managed `rate_*__*` views unless `explicit_tables` is supplied.
|
||||
|
||||
See [History Collection (SQLite)](history.md) for schema, view naming, and ER
|
||||
diagrams.
|
||||
|
||||
### Trading and sizing primitives (generic)
|
||||
|
||||
These helpers implement broker-facing calculations only. They do not encode
|
||||
strategy entries, exits, Kelly sizing, or signal logic.
|
||||
|
||||
| Symbol | Role |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| `get_account_snapshot`, `get_symbol_snapshot`, `get_tick_snapshot`, `get_positions_frame` | Normalized account/symbol/tick/position views |
|
||||
| `extract_tick_price` | Positive finite bid/ask extraction from tick mappings |
|
||||
| `detect_position_side` | Net long / short / flat from open positions |
|
||||
| `calculate_spread_ratio` | Relative bid-ask spread |
|
||||
| `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_new_position_margin_ratio` | Margin budget and volume sizing |
|
||||
| `normalize_order_volume`, `estimate_order_margin`, `calculate_positions_margin` | Broker volume normalization and margin totals |
|
||||
| `calculate_positions_margin_by_symbol` | Per-symbol margin map (resilient, first-seen order) |
|
||||
| `calculate_positions_margin_safe` | Summed total margin across symbols (failed symbols skipped) |
|
||||
| `calculate_projected_margin_ratio` | Estimated symbol-scoped margin/equity after optional new exposure |
|
||||
| `calculate_account_projected_margin_ratio` | Account snapshot margin/equity after optional new exposure |
|
||||
| `calculate_symbol_group_margin_ratio` | Estimated symbol-group margin/equity with optional exposure |
|
||||
| `determine_order_limits` | SL/TP price levels from ratios |
|
||||
| `calculate_trailing_stop_updates` | Per-ticket generic trailing stop-loss update plan |
|
||||
| `ensure_symbol_selected` | Select/verify Market Watch visibility |
|
||||
| `place_market_order`, `close_open_positions`, `update_sltp_for_open_positions`, `update_trailing_stop_loss_for_open_positions` | Order execution helpers (`dry_run` supported) |
|
||||
| `MarginVolume`, `OrderLimits`, `OrderExecutionResult` | Typed return contracts for order helpers |
|
||||
| `OrderSide`, `OrderFillingMode`, `OrderTimeMode`, `PositionSide`, `ExecutionStatus` | Typed enums for order helpers |
|
||||
| `ProjectionMode` | Literal type for `calculate_symbol_group_margin_ratio` projection |
|
||||
|
||||
`calculate_symbol_group_margin_ratio` accepts an optional `projection_mode`
|
||||
parameter (`"add"` by default). Pass `projection_mode="replace_symbol"` to
|
||||
subtract current exposure for `new_symbol` 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.
|
||||
|
||||
`MT5Client.order_send()` and CLI `order-send --yes` are live execution paths.
|
||||
|
||||
Order helpers validate broker stop-level distance in `determine_order_limits()` and
|
||||
raise `Mt5TradingError` when computed SL/TP prices are too close to the entry
|
||||
quote. Validation uses `trade_stops_level * point` 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 `trade_freeze_level`. Live
|
||||
`place_market_order()` and SL/TP updates call
|
||||
`ensure_symbol_selected()` so hidden symbols are added to Market Watch before
|
||||
sending requests. Failed, malformed, or unknown broker retcodes are fail-closed
|
||||
and returned as `status="failed"` with normalized `request` / `response` details;
|
||||
`dry_run=True` never calls `ensure_symbol_selected()` or `order_send()`.
|
||||
|
||||
### Errors and MT5 type re-exports
|
||||
|
||||
| Symbol | Role |
|
||||
| ------------------------------------------------------------------------------------ | ----------------------------------------------- |
|
||||
| `Mt5CliError`, `Mt5ConnectionError`, `Mt5OperationError`, `Mt5SchemaError` | Stable mt5cli exception types |
|
||||
| `normalize_mt5_exception`, `call_with_normalized_errors`, `is_recoverable_mt5_error` | Error normalization and retry classification |
|
||||
| `Mt5Config`, `Mt5RuntimeError`, `Mt5TradingClient`, `Mt5TradingError` | Re-exported pdmt5 types for adapter convenience |
|
||||
|
||||
## Secondary public exports
|
||||
|
||||
These names remain importable from `mt5cli` and are covered by
|
||||
`SECONDARY_PUBLIC_EXPORTS`, but they are oriented toward CLI/export/schema
|
||||
integrations, parsing, and lower-level MT5 access rather than the stable core
|
||||
SDK surface. Prefer the stable symbols above for downstream infrastructure
|
||||
adapters.
|
||||
|
||||
### Read-only MT5 data wrappers
|
||||
|
||||
Module-level helpers open a transient connection per call. Prefer `mt5_session`
|
||||
or `MT5Client` when making many requests in one process.
|
||||
|
||||
| Area | Symbols |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Rates | `copy_rates_from`, `copy_rates_from_pos`, `copy_rates_range`, `latest_rates`, `collect_latest_rates` |
|
||||
| Ticks | `copy_ticks_from`, `copy_ticks_range`, `recent_ticks` |
|
||||
| Account / terminal | `account_info`, `terminal_info`, `mt5_version`, `last_error`, `mt5_summary`, `mt5_summary_as_df` |
|
||||
| Symbols / market | `symbols`, `symbol_info`, `symbol_info_tick`, `market_book`, `minimum_margins` |
|
||||
| Trading state (read) | `orders`, `positions`, `history_orders`, `history_deals`, `recent_history_deals` |
|
||||
| Multi-account rates | `collect_latest_rates_for_accounts` |
|
||||
|
||||
Use `mt5_version` for MetaTrader 5 terminal version data. The name `version` at
|
||||
the package root refers to `importlib.metadata.version` (package metadata), not
|
||||
the MT5 SDK helper.
|
||||
|
||||
### Schema, export, and parser helpers
|
||||
|
||||
| Area | Symbols |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Dataset contracts | `DataKind`, `Dataset`, `IfExists`, `DEDUP_KEYS`, `REQUIRED_COLUMNS`, `TIME_COLUMNS`, `KNOWN_MT5_TIME_COLUMNS` |
|
||||
| Schema normalization | `normalize_dataframe`, `normalize_time_columns`, `schema_columns`, `validate_schema` |
|
||||
| Export helpers | `detect_format`, `export_dataframe`, `export_dataframe_to_sqlite` |
|
||||
| Symbol parsing | `normalize_symbol`, `normalize_symbols` |
|
||||
| Time parsing | `ensure_utc`, `parse_date_range`, `parse_datetime`, `recent_window` |
|
||||
| MT5 parsing maps | `granularity_name`, `parse_tick_flags`, `parse_timeframe`, `TICK_FLAG_MAP`, `TIMEFRAME_MAP` |
|
||||
| Trading data shapes | `POSITION_COLUMNS` |
|
||||
|
||||
## CLI commands
|
||||
|
||||
The Typer application in `mt5cli.cli` exposes file-export commands documented in
|
||||
[CLI Module](cli.md) and the project README. CLI commands:
|
||||
|
||||
- Require `-o/--output` and write CSV, JSON, Parquet, or SQLite.
|
||||
- Accept global MT5 connection options (`--login`, `--password`, `--server`,
|
||||
`--path`, `--timeout`).
|
||||
- Delegate to the same Python APIs described here; they are not duplicated
|
||||
business logic.
|
||||
|
||||
`order-send` is the expert raw-request path; it requires `--yes` and a fully
|
||||
constructed request payload. `close-positions` is the safer high-level helper
|
||||
that closes open positions by `--symbol` or `--ticket` using
|
||||
`close_open_positions()`. Both `order-send --yes` and `close-positions --yes`
|
||||
are live execution paths. `close-positions --dry-run` previews close orders
|
||||
without placing them and does not require `--yes`.
|
||||
|
||||
## Internal helpers (not stable)
|
||||
|
||||
Do not import these for downstream contracts; they may change without a semver
|
||||
notice:
|
||||
|
||||
| Module | Examples |
|
||||
| ------------------------ | ------------------------------------------------------------------------- |
|
||||
| `mt5cli.sdk` | `connected_client`, `_run_with_client`, private coercion helpers |
|
||||
| `mt5cli.history` | `write_*_dataset`, `deduplicate_history_tables`, `parse_sqlite_timestamp` |
|
||||
| `mt5cli.retry` | `retry_with_backoff` |
|
||||
| `mt5cli.cli` | Typer command handlers and Click parameter types |
|
||||
| Leading-underscore names | Any `_`-prefixed function or method |
|
||||
|
||||
Use the package-root stable exports instead of reaching into submodule
|
||||
internals.
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
mt5cli must **not** implement downstream strategy or research responsibilities.
|
||||
The following belong in consuming applications, not in mt5cli:
|
||||
|
||||
- Signal detection (for example AR-GARCH or other model-specific triggers)
|
||||
- Backtesting, walk-forward analysis, or parameter optimization
|
||||
- Strategy-specific risk policy, position sizing systems, or Kelly fractions
|
||||
- Entry/exit decision logic or YAML strategy semantics
|
||||
- Application-specific credential schema keys wired into mt5cli internals
|
||||
|
||||
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.
|
||||
|
||||
## Contract verification
|
||||
|
||||
`tests/test_contracts.py` asserts that every name in the stable and secondary
|
||||
tier sets is importable from `mt5cli`, documents key closed-bar, rate-view,
|
||||
SQLite loading, account-resolution, and trading-session behaviors, and keeps the
|
||||
tier sets aligned with `__all__`.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Schemas
|
||||
|
||||
::: mt5cli.schemas
|
||||
-174
@@ -1,174 +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. `fetch_latest_closed_rates()` handles one connected `MT5Client`; use
|
||||
`fetch_latest_closed_rates_for_trading_client()` from an active
|
||||
`Mt5TradingClient` session. Multi-account helpers fetch `count + 1` bars, drop
|
||||
that row with `drop_forming_rate_bar()`, and validate each series is non-empty. Returned frames are ordered
|
||||
oldest-to-newest and may contain fewer than `count` rows only when MT5 returns
|
||||
fewer closed bars.
|
||||
|
||||
```python
|
||||
from mt5cli import (
|
||||
AccountSpec,
|
||||
collect_latest_closed_rates_by_granularity,
|
||||
fetch_latest_closed_rates,
|
||||
)
|
||||
|
||||
closed = fetch_latest_closed_rates(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=500,
|
||||
)
|
||||
|
||||
rates = collect_latest_closed_rates_by_granularity(
|
||||
[AccountSpec(symbols=["EURUSD"], login=12345)],
|
||||
["M1", "H1"],
|
||||
count=500,
|
||||
retry_count=3,
|
||||
)
|
||||
closed_m1 = rates["EURUSD", "M1"]
|
||||
```
|
||||
|
||||
Use `collect_latest_closed_rates_by_granularity()` when callers prefer keys such
|
||||
as `("EURUSD", "M1")` instead of integer timeframes.
|
||||
|
||||
### 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"
|
||||
```
|
||||
|
||||
Pass `allow_whole_dollar_env=True` to also expand strings whose **entire value**
|
||||
is a bare `$ENV_NAME` identifier (no braces). This opt-in covers
|
||||
`substitute_env_placeholders()`, `resolve_account_spec()`,
|
||||
`resolve_account_specs()`, and `build_config()`. Note: `build_config` cannot
|
||||
expand `login` because that parameter is `int | None`; use
|
||||
`resolve_account_spec` for a string `login` placeholder. Partial strings such as
|
||||
`"plan$pass"`, `"abc$ENV"`, or `"$ENV-suffix"` are never expanded — only an
|
||||
exact `$IDENTIFIER` whole-string match qualifies. The default is `False` to
|
||||
preserve backward compatibility.
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from mt5cli import AccountSpec, resolve_account_specs
|
||||
|
||||
os.environ["MT5_PASSWORD"] = "secret"
|
||||
accounts = [AccountSpec(symbols=["EURUSD"], password="$MT5_PASSWORD")]
|
||||
|
||||
resolved = resolve_account_specs(accounts, allow_whole_dollar_env=True)
|
||||
# resolved[0].password == "secret"
|
||||
```
|
||||
|
||||
### 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()
|
||||
```
|
||||
|
||||
Pass `update_backend` to substitute the default `update_history` implementation
|
||||
without monkey-patching `mt5cli.sdk.update_history`. The callable receives the
|
||||
same keyword arguments as `update_history` (`client`, `output`, `symbols`,
|
||||
`datasets`, `timeframes`, `flags`, `lookback_hours`, `with_views`,
|
||||
`include_account_events`). The resolved backend is stored on
|
||||
`updater.update_backend` for inspection or subclassing.
|
||||
|
||||
```python
|
||||
from mt5cli import ThrottledHistoryUpdater, update_history
|
||||
|
||||
|
||||
def app_update_history(**kwargs) -> None:
|
||||
update_history(**kwargs) # or delegate to application-specific logic
|
||||
|
||||
|
||||
updater = ThrottledHistoryUpdater(
|
||||
output="history.db",
|
||||
interval_seconds=60,
|
||||
update_backend=app_update_history,
|
||||
)
|
||||
```
|
||||
|
||||
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). Use `mt5_session()` / `MT5Client` for read-only
|
||||
collection.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Storage
|
||||
|
||||
::: mt5cli.storage
|
||||
@@ -1,199 +0,0 @@
|
||||
# Trading Module
|
||||
|
||||
::: mt5cli.trading
|
||||
|
||||
## Trading-capable MT5 sessions
|
||||
|
||||
`create_trading_client()` and `mt5_trading_session()` complement the read-only
|
||||
`mt5_session()` helper in `sdk.py`. They return or yield an initialized
|
||||
`pdmt5.Mt5TradingClient`, use `Mt5Config.path` to launch the terminal when
|
||||
configured, and `mt5_trading_session()` always calls `shutdown()` on exit.
|
||||
|
||||
```python
|
||||
from mt5cli import create_trading_client, mt5_trading_session
|
||||
|
||||
with mt5_trading_session(
|
||||
path=r"C:\Program Files\MetaTrader 5\terminal64.exe",
|
||||
login="12345",
|
||||
password="secret",
|
||||
server="Broker-Demo",
|
||||
retry_count=2,
|
||||
) as client:
|
||||
positions = client.positions_get_as_df(symbol="EURUSD")
|
||||
|
||||
client = create_trading_client(login=12345, server="Broker-Demo")
|
||||
try:
|
||||
account = client.account_info_as_dict()
|
||||
finally:
|
||||
client.shutdown()
|
||||
```
|
||||
|
||||
`login` accepts `int`, numeric `str`, or an empty string; empty strings are
|
||||
treated as unset. `path`, `password`, `server`, and `timeout` are forwarded to
|
||||
`pdmt5.Mt5Config`, and omitted `timeout` values keep the lower-level default.
|
||||
Use `mt5_session()` / `MT5Client` for read-only data collection.
|
||||
|
||||
## State and order 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_positions_margin,
|
||||
calculate_spread_ratio,
|
||||
calculate_margin_and_volume,
|
||||
close_open_positions,
|
||||
detect_position_side,
|
||||
determine_order_limits,
|
||||
estimate_order_margin,
|
||||
fetch_latest_closed_rates_for_trading_client,
|
||||
fetch_latest_closed_rates_indexed,
|
||||
get_account_snapshot,
|
||||
get_positions_frame,
|
||||
get_symbol_snapshot,
|
||||
get_tick_snapshot,
|
||||
normalize_order_volume,
|
||||
place_market_order,
|
||||
)
|
||||
|
||||
account = get_account_snapshot(client)
|
||||
symbol = get_symbol_snapshot(client, "EURUSD")
|
||||
tick = get_tick_snapshot(client, "EURUSD")
|
||||
positions = get_positions_frame(client, "EURUSD")
|
||||
side = detect_position_side(client, "EURUSD")
|
||||
spread_ratio = calculate_spread_ratio(client, "EURUSD")
|
||||
volume = normalize_order_volume(
|
||||
0.15,
|
||||
volume_min=symbol["volume_min"],
|
||||
volume_max=symbol["volume_max"],
|
||||
volume_step=symbol["volume_step"],
|
||||
)
|
||||
buy_margin = (
|
||||
estimate_order_margin(client, "EURUSD", "BUY", volume) if volume > 0 else 0.0
|
||||
)
|
||||
open_margin = calculate_positions_margin(client, symbols=["EURUSD"])
|
||||
closed_bars = fetch_latest_closed_rates_for_trading_client(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=100,
|
||||
)
|
||||
# Or fetch with a UTC DatetimeIndex instead of a "time" column:
|
||||
indexed_bars = fetch_latest_closed_rates_indexed(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=100,
|
||||
)
|
||||
# indexed_bars.index is a UTC-aware DatetimeIndex named "time"
|
||||
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,
|
||||
)
|
||||
preview = place_market_order(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
volume=sizing["buy_volume"],
|
||||
order_side="BUY",
|
||||
sl=limits["stop_loss"],
|
||||
tp=limits["take_profit"],
|
||||
dry_run=True,
|
||||
)
|
||||
closed = close_open_positions(client, symbols="EURUSD", dry_run=True)
|
||||
```
|
||||
|
||||
`detect_position_side()` returns `long` for buy-only exposure, `short` for
|
||||
sell-only exposure, and `None` for no positions or mixed long/short exposure.
|
||||
`calculate_spread_ratio()` uses `(ask - bid) / ((ask + bid) / 2)` and raises
|
||||
`Mt5TradingError` when bid or ask is missing or non-positive.
|
||||
`normalize_order_volume()` returns `0.0` for invalid constraints or
|
||||
sub-minimum requests; check the result before calling `estimate_order_margin()`,
|
||||
which requires a positive finite volume. `calculate_positions_margin()` silently
|
||||
skips rows with missing symbols, non-positive volumes, non-finite volumes, or
|
||||
unsupported position types, but propagates `Mt5TradingError` from `estimate_order_margin()` when a valid row
|
||||
encounters invalid tick data or margin results from the broker.
|
||||
|
||||
SL/TP ratios for `determine_order_limits()` must satisfy `0 <= ratio < 1`; `0`
|
||||
omits that level. SL/TP prices are rounded with symbol `digits` metadata when
|
||||
available. `determine_order_limits()` pre-validates computed SL/TP prices against
|
||||
available `trade_stops_level * point` metadata when present; violations raise
|
||||
`Mt5TradingError`. This is a planning helper only: it does not guarantee broker
|
||||
acceptance because live validation can still depend on price movement, bid/ask
|
||||
side, freeze levels, and server-side rules, and it does not validate
|
||||
`trade_freeze_level`. When symbol metadata cannot be loaded, protective prices
|
||||
still round with `digits=8` and stop-level validation is skipped.
|
||||
`unit_margin_ratio` and `preserved_margin_ratio` for `calculate_margin_and_volume()`
|
||||
accept `0 <= ratio <= 1`; `unit_margin_ratio=0` requests one minimum valid unit
|
||||
when the post-reserve margin can afford it. Negative `margin_free` is clamped to
|
||||
`0.0` before sizing. Execution helpers return normalized `OrderExecutionResult`
|
||||
dictionaries containing the request, response, status, retcode, and `dry_run`
|
||||
flag; `dry_run=True` never sends an order or mutates Market Watch visibility.
|
||||
`ensure_symbol_selected()` adds hidden symbols to Market Watch before live order
|
||||
placement and SL/TP updates. Failed, malformed, or unknown broker retcodes are
|
||||
fail-closed and returned as `status="failed"` while keeping the normalized
|
||||
response for inspection.
|
||||
|
||||
## Order planning return contracts
|
||||
|
||||
```python
|
||||
from mt5cli import MarginVolume, OrderLimits, OrderExecutionResult
|
||||
|
||||
sizing: MarginVolume = calculate_margin_and_volume(
|
||||
client,
|
||||
"EURUSD",
|
||||
unit_margin_ratio=0.5,
|
||||
preserved_margin_ratio=0.2,
|
||||
)
|
||||
limits: OrderLimits = determine_order_limits(
|
||||
client,
|
||||
"EURUSD",
|
||||
side="long",
|
||||
stop_loss_limit_ratio=0.01,
|
||||
take_profit_limit_ratio=0.02,
|
||||
)
|
||||
preview: OrderExecutionResult = place_market_order(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
volume=sizing["buy_volume"],
|
||||
order_side="BUY",
|
||||
sl=limits["stop_loss"],
|
||||
tp=limits["take_profit"],
|
||||
dry_run=True,
|
||||
)
|
||||
updates: list[OrderExecutionResult] = update_sltp_for_open_positions(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
stop_loss=limits["stop_loss"],
|
||||
dry_run=True,
|
||||
)
|
||||
```
|
||||
|
||||
Closes issue #33: strategy-neutral order planning and execution helpers exposed
|
||||
through the stable package root without embedding entry/exit policy.
|
||||
|
||||
## Migration from application-local helpers
|
||||
|
||||
| Application-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 broker volume step normalization | `normalize_order_volume()` |
|
||||
| Local order or position margin estimation | `estimate_order_margin()`, `calculate_positions_margin()` |
|
||||
| Local closed-bar fetch from a trading session | `fetch_latest_closed_rates_for_trading_client()`, `fetch_latest_closed_rates_indexed()` |
|
||||
| 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()` / `MT5Client`; use
|
||||
`mt5_trading_session()` only where order placement or trading calculations are
|
||||
required.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utils Module
|
||||
|
||||
::: mt5cli.utils
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
# mt5cli
|
||||
|
||||
Generic MT5 data and execution infrastructure for Python applications.
|
||||
|
||||
## Overview
|
||||
|
||||
mt5cli provides a stable `MT5Client` Python API, standardized dataset schemas, storage helpers, and a CLI for exporting MetaTrader 5 data. 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** — public `MT5Client` API, schema contracts, storage helpers, CLI commands, and SQLite history collection 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
|
||||
```
|
||||
|
||||
Parquet export is not included by default. To enable it, install the `parquet` extra:
|
||||
|
||||
```bash
|
||||
pip install "mt5cli[parquet]"
|
||||
```
|
||||
|
||||
## Python API for downstream packages
|
||||
|
||||
Import `MT5Client` for generic MT5 data access, schema normalization, and optional order primitives.
|
||||
|
||||
```python
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from mt5cli import (
|
||||
DataKind,
|
||||
Dataset,
|
||||
MT5Client,
|
||||
build_config,
|
||||
collect_history,
|
||||
export_dataframe,
|
||||
load_rate_data,
|
||||
minimum_margins,
|
||||
mt5_session,
|
||||
normalize_dataframe,
|
||||
recent_ticks,
|
||||
resolve_rate_view_name,
|
||||
)
|
||||
|
||||
# Persistent session for multiple calls
|
||||
with mt5_session(build_config(login=12345, server="Broker-Demo")) as client:
|
||||
rates = client.copy_rates_range(
|
||||
"EURUSD",
|
||||
timeframe="H1",
|
||||
date_from="2024-01-01",
|
||||
date_to="2024-02-01",
|
||||
)
|
||||
positions = client.positions()
|
||||
check = client.order_check({"action": 1, "symbol": "EURUSD", "volume": 0.1})
|
||||
|
||||
# Normalize MT5 frames to the public schema contract before storage
|
||||
closed_rates = normalize_dataframe(
|
||||
rates, DataKind.rates, symbol="EURUSD", timeframe="H1"
|
||||
)
|
||||
export_dataframe(closed_rates, Path("rates.csv"), "csv")
|
||||
|
||||
# Offline rate loading from mt5cli-managed SQLite history
|
||||
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
|
||||
offline_rates = load_rate_data(Path("history.db"), view, count=1000)
|
||||
|
||||
# One-off helpers still work without instantiating a client
|
||||
ticks = recent_ticks("EURUSD", seconds=300)
|
||||
margins = minimum_margins("EURUSD")
|
||||
|
||||
collect_history(
|
||||
Path("history.db"),
|
||||
symbols=["EURUSD", "GBPUSD"],
|
||||
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
||||
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
||||
datasets={Dataset.rates, Dataset.history_deals},
|
||||
)
|
||||
```
|
||||
|
||||
Schema contracts live in `mt5cli.schemas` (`DataKind`, `validate_schema`, `normalize_dataframe`). Storage helpers are re-exported from `mt5cli.storage` and the package root.
|
||||
|
||||
`MT5Client.order_send()` 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 `--yes` for `order-send`).
|
||||
|
||||
`MT5Client.mt5_summary()` returns structured nested Python values. Use `MT5Client.mt5_summary_as_df()` when you need a one-row DataFrame for export.
|
||||
|
||||
## 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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
+686
@@ -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">¶</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">¶</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">¶</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">¶</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">¶</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">"mt5cli[parquet]"</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">¶</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">"Broker-Demo"</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">"EURUSD"</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">"H1"</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">"2024-01-01"</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">"2024-02-01"</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">"action"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"symbol"</span><span class="p">:</span> <span class="s2">"EURUSD"</span><span class="p">,</span> <span class="s2">"volume"</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">"EURUSD"</span><span class="p">,</span> <span class="n">timeframe</span><span class="o">=</span><span class="s2">"H1"</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">"rates.csv"</span><span class="p">),</span> <span class="s2">"csv"</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">"history.db"</span><span class="p">),</span> <span class="s2">"EURUSD"</span><span class="p">,</span> <span class="s2">"M1"</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">"history.db"</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">"EURUSD"</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">"EURUSD"</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">"history.db"</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">"EURUSD"</span><span class="p">,</span> <span class="s2">"GBPUSD"</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">¶</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">"*USD*"</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">'${MT5_LOGIN}'</span><span class="w"> </span>--password<span class="w"> </span><span class="s1">'${MT5_PASSWORD}'</span><span class="w"> </span>--server<span class="w"> </span><span class="s1">'${MT5_SERVER}'</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">¶</a></h2>
|
||||
<h3 id="rates">Rates<a class="headerlink" href="#rates" title="Permanent link">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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
@@ -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: '←',
|
||||
38: '↑',
|
||||
39: '→',
|
||||
40: '↓',
|
||||
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: ';',
|
||||
187: '=',
|
||||
188: ',',
|
||||
189: '‐',
|
||||
190: '.',
|
||||
191: '?',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\',
|
||||
221: ']',
|
||||
222: ''',
|
||||
};
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
});
|
||||
});
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
site_name: mt5cli API Documentation
|
||||
site_description: Generic MT5 data and execution infrastructure for Python
|
||||
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
|
||||
- Public API Contract: api/public-contract.md
|
||||
- Client: api/client.md
|
||||
- Schemas: api/schemas.md
|
||||
- Storage: api/storage.md
|
||||
- Converters: api/converters.md
|
||||
- Exceptions: api/exceptions.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
|
||||
@@ -1,299 +0,0 @@
|
||||
"""mt5cli: Generic MT5 data and execution infrastructure for Python applications.
|
||||
|
||||
Downstream packages should import from this module (``from mt5cli import ...``)
|
||||
rather than private submodule helpers. See ``docs/api/public-contract.md`` for
|
||||
the stable SDK contract, CLI surface, internal modules, and out-of-scope
|
||||
strategy responsibilities.
|
||||
"""
|
||||
|
||||
from importlib.metadata import version
|
||||
|
||||
from pdmt5 import Mt5Config, Mt5RuntimeError, Mt5TradingClient, Mt5TradingError
|
||||
|
||||
from .client import MT5Client, build_config, mt5_session
|
||||
from .contract import (
|
||||
PUBLIC_EXPORT_TIERS,
|
||||
SECONDARY_PUBLIC_EXPORTS,
|
||||
STABLE_SDK_EXPORTS,
|
||||
)
|
||||
from .converters import (
|
||||
ensure_utc,
|
||||
granularity_name,
|
||||
normalize_symbol,
|
||||
normalize_symbols,
|
||||
parse_date_range,
|
||||
recent_window,
|
||||
)
|
||||
from .exceptions import (
|
||||
Mt5CliError,
|
||||
Mt5ConnectionError,
|
||||
Mt5OperationError,
|
||||
Mt5SchemaError,
|
||||
call_with_normalized_errors,
|
||||
is_recoverable_mt5_error,
|
||||
normalize_mt5_exception,
|
||||
)
|
||||
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_table_name,
|
||||
resolve_rate_tables,
|
||||
resolve_rate_view_name,
|
||||
resolve_rate_view_names,
|
||||
)
|
||||
from .schemas import (
|
||||
DEDUP_KEYS,
|
||||
KNOWN_MT5_TIME_COLUMNS,
|
||||
REQUIRED_COLUMNS,
|
||||
TIME_COLUMNS,
|
||||
DataKind,
|
||||
normalize_dataframe,
|
||||
normalize_time_columns,
|
||||
schema_columns,
|
||||
validate_schema,
|
||||
)
|
||||
from .sdk import (
|
||||
AccountSpec,
|
||||
ThrottledHistoryUpdater,
|
||||
account_info,
|
||||
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,
|
||||
fetch_latest_closed_rates,
|
||||
history_deals,
|
||||
history_orders,
|
||||
last_error,
|
||||
latest_rates,
|
||||
market_book,
|
||||
minimum_margins,
|
||||
mt5_summary,
|
||||
mt5_summary_as_df,
|
||||
orders,
|
||||
positions,
|
||||
recent_history_deals,
|
||||
recent_ticks,
|
||||
resolve_account_spec,
|
||||
resolve_account_specs,
|
||||
substitute_env_placeholders,
|
||||
substitute_mapping_values,
|
||||
symbol_info,
|
||||
symbol_info_tick,
|
||||
symbols,
|
||||
terminal_info,
|
||||
update_history,
|
||||
update_history_with_config,
|
||||
)
|
||||
from .sdk import (
|
||||
version as mt5_version,
|
||||
)
|
||||
from .storage import (
|
||||
Dataset,
|
||||
IfExists,
|
||||
detect_format,
|
||||
export_dataframe,
|
||||
export_dataframe_to_sqlite,
|
||||
)
|
||||
from .trading import (
|
||||
POSITION_COLUMNS,
|
||||
ExecutionStatus,
|
||||
MarginVolume,
|
||||
OrderExecutionResult,
|
||||
OrderFillingMode,
|
||||
OrderLimits,
|
||||
OrderSide,
|
||||
OrderTimeMode,
|
||||
PositionSide,
|
||||
ProjectionMode,
|
||||
calculate_account_projected_margin_ratio,
|
||||
calculate_margin_and_volume,
|
||||
calculate_new_position_margin_ratio,
|
||||
calculate_positions_margin,
|
||||
calculate_positions_margin_by_symbol,
|
||||
calculate_positions_margin_safe,
|
||||
calculate_projected_margin_ratio,
|
||||
calculate_spread_ratio,
|
||||
calculate_symbol_group_margin_ratio,
|
||||
calculate_trailing_stop_updates,
|
||||
calculate_volume_by_margin,
|
||||
close_open_positions,
|
||||
create_trading_client,
|
||||
detect_position_side,
|
||||
determine_order_limits,
|
||||
ensure_symbol_selected,
|
||||
estimate_order_margin,
|
||||
extract_tick_price,
|
||||
fetch_latest_closed_rates_for_trading_client,
|
||||
fetch_latest_closed_rates_indexed,
|
||||
get_account_snapshot,
|
||||
get_positions_frame,
|
||||
get_symbol_snapshot,
|
||||
get_tick_snapshot,
|
||||
mt5_trading_session,
|
||||
normalize_order_volume,
|
||||
place_market_order,
|
||||
update_sltp_for_open_positions,
|
||||
update_trailing_stop_loss_for_open_positions,
|
||||
)
|
||||
from .utils import (
|
||||
TICK_FLAG_MAP,
|
||||
TIMEFRAME_MAP,
|
||||
parse_datetime,
|
||||
parse_tick_flags,
|
||||
parse_timeframe,
|
||||
)
|
||||
|
||||
__version__ = version(__package__) if __package__ else None
|
||||
|
||||
__all__ = [
|
||||
"DEDUP_KEYS",
|
||||
"KNOWN_MT5_TIME_COLUMNS",
|
||||
"POSITION_COLUMNS",
|
||||
"PUBLIC_EXPORT_TIERS",
|
||||
"REQUIRED_COLUMNS",
|
||||
"SECONDARY_PUBLIC_EXPORTS",
|
||||
"STABLE_SDK_EXPORTS",
|
||||
"TICK_FLAG_MAP",
|
||||
"TIMEFRAME_MAP",
|
||||
"TIME_COLUMNS",
|
||||
"AccountSpec",
|
||||
"DataKind",
|
||||
"Dataset",
|
||||
"ExecutionStatus",
|
||||
"IfExists",
|
||||
"MT5Client",
|
||||
"MarginVolume",
|
||||
"Mt5CliError",
|
||||
"Mt5Config",
|
||||
"Mt5ConnectionError",
|
||||
"Mt5OperationError",
|
||||
"Mt5RuntimeError",
|
||||
"Mt5SchemaError",
|
||||
"Mt5TradingClient",
|
||||
"Mt5TradingError",
|
||||
"OrderExecutionResult",
|
||||
"OrderFillingMode",
|
||||
"OrderLimits",
|
||||
"OrderSide",
|
||||
"OrderTimeMode",
|
||||
"PositionSide",
|
||||
"ProjectionMode",
|
||||
"RateTarget",
|
||||
"ThrottledHistoryUpdater",
|
||||
"account_info",
|
||||
"build_config",
|
||||
"build_rate_targets",
|
||||
"build_rate_view_name",
|
||||
"calculate_account_projected_margin_ratio",
|
||||
"calculate_margin_and_volume",
|
||||
"calculate_new_position_margin_ratio",
|
||||
"calculate_positions_margin",
|
||||
"calculate_positions_margin_by_symbol",
|
||||
"calculate_positions_margin_safe",
|
||||
"calculate_projected_margin_ratio",
|
||||
"calculate_spread_ratio",
|
||||
"calculate_symbol_group_margin_ratio",
|
||||
"calculate_trailing_stop_updates",
|
||||
"calculate_volume_by_margin",
|
||||
"call_with_normalized_errors",
|
||||
"close_open_positions",
|
||||
"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",
|
||||
"create_trading_client",
|
||||
"detect_format",
|
||||
"detect_position_side",
|
||||
"determine_order_limits",
|
||||
"drop_forming_rate_bar",
|
||||
"ensure_symbol_selected",
|
||||
"ensure_utc",
|
||||
"estimate_order_margin",
|
||||
"export_dataframe",
|
||||
"export_dataframe_to_sqlite",
|
||||
"extract_tick_price",
|
||||
"fetch_latest_closed_rates",
|
||||
"fetch_latest_closed_rates_for_trading_client",
|
||||
"fetch_latest_closed_rates_indexed",
|
||||
"get_account_snapshot",
|
||||
"get_positions_frame",
|
||||
"get_symbol_snapshot",
|
||||
"get_tick_snapshot",
|
||||
"granularity_name",
|
||||
"history_deals",
|
||||
"history_orders",
|
||||
"is_recoverable_mt5_error",
|
||||
"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",
|
||||
"normalize_dataframe",
|
||||
"normalize_mt5_exception",
|
||||
"normalize_order_volume",
|
||||
"normalize_symbol",
|
||||
"normalize_symbols",
|
||||
"normalize_time_columns",
|
||||
"orders",
|
||||
"parse_date_range",
|
||||
"parse_datetime",
|
||||
"parse_tick_flags",
|
||||
"parse_timeframe",
|
||||
"place_market_order",
|
||||
"positions",
|
||||
"recent_history_deals",
|
||||
"recent_ticks",
|
||||
"recent_window",
|
||||
"resolve_account_spec",
|
||||
"resolve_account_specs",
|
||||
"resolve_history_datasets",
|
||||
"resolve_history_tick_flags",
|
||||
"resolve_history_timeframes",
|
||||
"resolve_rate_table_name",
|
||||
"resolve_rate_tables",
|
||||
"resolve_rate_view_name",
|
||||
"resolve_rate_view_names",
|
||||
"schema_columns",
|
||||
"substitute_env_placeholders",
|
||||
"substitute_mapping_values",
|
||||
"symbol_info",
|
||||
"symbol_info_tick",
|
||||
"symbols",
|
||||
"terminal_info",
|
||||
"update_history",
|
||||
"update_history_with_config",
|
||||
"update_sltp_for_open_positions",
|
||||
"update_trailing_stop_loss_for_open_positions",
|
||||
"validate_schema",
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Entry point for running mt5cli as a module via ``python -m mt5cli``."""
|
||||
|
||||
from mt5cli.cli import main
|
||||
|
||||
main()
|
||||
-793
@@ -1,793 +0,0 @@
|
||||
"""Command-line interface for MetaTrader 5 data export."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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 pandas as pd
|
||||
import typer
|
||||
from pdmt5 import Mt5Config
|
||||
|
||||
from . import sdk
|
||||
from .client import MT5Client
|
||||
from .trading import OrderExecutionResult, close_open_positions, create_trading_client
|
||||
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
|
||||
|
||||
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) -> MT5Client:
|
||||
export_ctx = _get_export_context(ctx)
|
||||
return MT5Client(config=export_ctx.config)
|
||||
|
||||
|
||||
def _export_command(
|
||||
ctx: typer.Context,
|
||||
fetch_fn: Callable[[MT5Client], pd.DataFrame],
|
||||
) -> None:
|
||||
"""Create an SDK client, fetch a DataFrame, and export it."""
|
||||
client = _sdk_client(ctx)
|
||||
_execute_export(ctx, lambda: fetch_fn(client))
|
||||
|
||||
|
||||
@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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(ctx, lambda client: client.account_info())
|
||||
|
||||
|
||||
@app.command()
|
||||
def terminal_info(ctx: typer.Context) -> None:
|
||||
"""Export terminal information."""
|
||||
_export_command(ctx, lambda client: client.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."""
|
||||
_export_command(ctx, lambda client: client.symbols(group=group))
|
||||
|
||||
|
||||
@app.command()
|
||||
def symbol_info(
|
||||
ctx: typer.Context,
|
||||
symbol: Annotated[str, typer.Option(help="Symbol name.")],
|
||||
) -> None:
|
||||
"""Export symbol details."""
|
||||
_export_command(ctx, lambda client: 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."""
|
||||
_export_command(ctx, lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(
|
||||
ctx,
|
||||
lambda client: 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."""
|
||||
_export_command(ctx, lambda client: client.mt5_summary_as_df())
|
||||
|
||||
|
||||
@app.command()
|
||||
def version(ctx: typer.Context) -> None:
|
||||
"""Export MetaTrader5 version information."""
|
||||
_export_command(ctx, lambda client: client.version())
|
||||
|
||||
|
||||
@app.command()
|
||||
def last_error(ctx: typer.Context) -> None:
|
||||
"""Export the last error information."""
|
||||
_export_command(ctx, lambda client: client.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."""
|
||||
_export_command(ctx, lambda client: 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."""
|
||||
_export_command(ctx, lambda client: 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_command(ctx, lambda client: client.order_check(request))
|
||||
|
||||
|
||||
@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_command(ctx, lambda client: client.order_send(request))
|
||||
|
||||
|
||||
_EXECUTION_RESULT_COLUMNS: list[str] = [
|
||||
"status",
|
||||
"symbol",
|
||||
"order_side",
|
||||
"volume",
|
||||
"retcode",
|
||||
"comment",
|
||||
"request",
|
||||
"response",
|
||||
"dry_run",
|
||||
]
|
||||
|
||||
|
||||
def _execution_results_to_df(results: list[OrderExecutionResult]) -> pd.DataFrame:
|
||||
if not results:
|
||||
return pd.DataFrame(columns=_EXECUTION_RESULT_COLUMNS)
|
||||
rows = [
|
||||
{
|
||||
**r,
|
||||
"request": json.dumps(r["request"]),
|
||||
"response": json.dumps(r["response"]),
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
@app.command()
|
||||
def close_positions(
|
||||
ctx: typer.Context,
|
||||
symbol: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--symbol",
|
||||
"-s",
|
||||
help="Symbol to close (repeat for multiple symbols).",
|
||||
),
|
||||
] = None,
|
||||
ticket: Annotated[
|
||||
list[int] | None,
|
||||
typer.Option(
|
||||
"--ticket",
|
||||
"-t",
|
||||
help="Position ticket to close (repeat for multiple tickets).",
|
||||
),
|
||||
] = None,
|
||||
dry_run: Annotated[
|
||||
bool,
|
||||
typer.Option("--dry-run", help="Preview close orders without executing them."),
|
||||
] = False,
|
||||
yes: Annotated[
|
||||
bool,
|
||||
typer.Option("--yes", help="Confirm live position closing."),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Close open positions by symbol or ticket.
|
||||
|
||||
Delegates to :func:`mt5cli.trading.close_open_positions`. At least one
|
||||
``--symbol`` or ``--ticket`` must be provided to avoid accidentally closing
|
||||
all positions. Use ``--dry-run`` to preview without executing; ``--yes`` is
|
||||
required for live execution.
|
||||
|
||||
``order-send`` is the expert raw-request path. ``close-positions`` is the
|
||||
safer high-level helper that builds correct close requests automatically.
|
||||
|
||||
Raises:
|
||||
typer.BadParameter: If neither ``--symbol`` nor ``--ticket`` is given,
|
||||
or if ``--yes`` is missing for a live (non-dry-run) run.
|
||||
"""
|
||||
if not symbol and not ticket:
|
||||
msg = "Provide at least one --symbol or --ticket to close positions."
|
||||
raise typer.BadParameter(msg)
|
||||
if not dry_run and not yes:
|
||||
msg = "Pass --yes to close live positions."
|
||||
raise typer.BadParameter(msg, param_hint="--yes")
|
||||
export_ctx = _get_export_context(ctx)
|
||||
client = create_trading_client(config=export_ctx.config)
|
||||
try:
|
||||
results = close_open_positions(
|
||||
client,
|
||||
symbols=list(symbol) if symbol else None,
|
||||
tickets=list(ticket) if ticket else None,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
finally:
|
||||
client.shutdown()
|
||||
df = _execution_results_to_df(results)
|
||||
_execute_export(ctx, lambda: df)
|
||||
|
||||
|
||||
@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()
|
||||
@@ -1,86 +0,0 @@
|
||||
"""Stable public client abstraction for MT5 data and execution operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
from .sdk import Mt5CliClient, build_config, connected_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pandas as pd
|
||||
from pdmt5 import Mt5Config, Mt5DataClient
|
||||
|
||||
__all__ = [
|
||||
"MT5Client",
|
||||
"build_config",
|
||||
"mt5_session",
|
||||
]
|
||||
|
||||
|
||||
class MT5Client(Mt5CliClient):
|
||||
"""Public client for generic MT5 data access and order primitives.
|
||||
|
||||
Extends the read-only SDK client with optional order check/send helpers and
|
||||
exposes the same connection lifecycle as :func:`mt5_session`.
|
||||
|
||||
mt5cli intentionally exposes minimal execution primitives only. Trading
|
||||
decisions, signals, strategies, backtests, and optimization remain the
|
||||
responsibility of downstream applications.
|
||||
"""
|
||||
|
||||
def order_check(self, request: dict[str, Any]) -> pd.DataFrame:
|
||||
"""Check funds sufficiency for a trade request.
|
||||
|
||||
Args:
|
||||
request: MT5 order request dictionary.
|
||||
|
||||
Returns:
|
||||
One-row DataFrame with the order-check result.
|
||||
"""
|
||||
return self._fetch(lambda client: client.order_check_as_df(request=request))
|
||||
|
||||
def order_send(self, request: dict[str, Any]) -> pd.DataFrame:
|
||||
"""Send a live trade request to the MT5 trade server.
|
||||
|
||||
Warning:
|
||||
This is a live execution primitive. A successful call can place,
|
||||
modify, or close real trades on the connected account. Downstream
|
||||
applications must gate usage explicitly (for example behind manual
|
||||
confirmation or application-specific risk controls). mt5cli does
|
||||
not implement strategy logic, signal generation, or trade sizing.
|
||||
|
||||
Args:
|
||||
request: MT5 order request dictionary.
|
||||
|
||||
Returns:
|
||||
One-row DataFrame with the order-send result.
|
||||
"""
|
||||
return self._fetch(lambda client: client.order_send_as_df(request=request))
|
||||
|
||||
@classmethod
|
||||
def from_connected_client(cls, client: Mt5DataClient) -> Self:
|
||||
"""Bind to an already-connected ``Mt5DataClient`` without owning it.
|
||||
|
||||
Returns:
|
||||
Client wrapper bound to the injected connection.
|
||||
"""
|
||||
return cls(client=client)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mt5_session(config: Mt5Config | None = None) -> Iterator[MT5Client]:
|
||||
"""Open an MT5 terminal session and yield a connected :class:`MT5Client`.
|
||||
|
||||
Args:
|
||||
config: MT5 connection configuration. Defaults to an empty config that
|
||||
attaches to a running terminal.
|
||||
|
||||
Yields:
|
||||
Connected :class:`MT5Client` bound to the session.
|
||||
"""
|
||||
mt5_config = config or build_config()
|
||||
with connected_client(mt5_config) as client:
|
||||
yield MT5Client.from_connected_client(client)
|
||||
@@ -1,151 +0,0 @@
|
||||
"""Downstream SDK export tiers for mt5cli."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
|
||||
"AccountSpec",
|
||||
"MT5Client",
|
||||
"Mt5CliError",
|
||||
"Mt5Config",
|
||||
"Mt5ConnectionError",
|
||||
"Mt5OperationError",
|
||||
"Mt5RuntimeError",
|
||||
"Mt5SchemaError",
|
||||
"Mt5TradingClient",
|
||||
"Mt5TradingError",
|
||||
"OrderFillingMode",
|
||||
"OrderSide",
|
||||
"OrderTimeMode",
|
||||
"PositionSide",
|
||||
"ProjectionMode",
|
||||
"ExecutionStatus",
|
||||
"MarginVolume",
|
||||
"OrderExecutionResult",
|
||||
"OrderLimits",
|
||||
"RateTarget",
|
||||
"ThrottledHistoryUpdater",
|
||||
"build_config",
|
||||
"build_rate_targets",
|
||||
"build_rate_view_name",
|
||||
"calculate_account_projected_margin_ratio",
|
||||
"calculate_margin_and_volume",
|
||||
"calculate_new_position_margin_ratio",
|
||||
"calculate_projected_margin_ratio",
|
||||
"calculate_positions_margin",
|
||||
"calculate_positions_margin_by_symbol",
|
||||
"calculate_positions_margin_safe",
|
||||
"calculate_spread_ratio",
|
||||
"calculate_symbol_group_margin_ratio",
|
||||
"calculate_trailing_stop_updates",
|
||||
"calculate_volume_by_margin",
|
||||
"call_with_normalized_errors",
|
||||
"close_open_positions",
|
||||
"collect_history",
|
||||
"collect_latest_closed_rates_by_granularity",
|
||||
"collect_latest_closed_rates_for_accounts",
|
||||
"collect_latest_rates_for_accounts_with_retries",
|
||||
"create_trading_client",
|
||||
"detect_position_side",
|
||||
"determine_order_limits",
|
||||
"drop_forming_rate_bar",
|
||||
"ensure_symbol_selected",
|
||||
"estimate_order_margin",
|
||||
"extract_tick_price",
|
||||
"fetch_latest_closed_rates",
|
||||
"fetch_latest_closed_rates_for_trading_client",
|
||||
"fetch_latest_closed_rates_indexed",
|
||||
"get_account_snapshot",
|
||||
"get_positions_frame",
|
||||
"get_symbol_snapshot",
|
||||
"get_tick_snapshot",
|
||||
"is_recoverable_mt5_error",
|
||||
"load_rate_data",
|
||||
"load_rate_data_from_connection",
|
||||
"load_rate_series_by_granularity",
|
||||
"load_rate_series_from_sqlite",
|
||||
"mt5_session",
|
||||
"mt5_trading_session",
|
||||
"normalize_mt5_exception",
|
||||
"normalize_order_volume",
|
||||
"place_market_order",
|
||||
"resolve_account_spec",
|
||||
"resolve_account_specs",
|
||||
"resolve_history_datasets",
|
||||
"resolve_history_tick_flags",
|
||||
"resolve_history_timeframes",
|
||||
"resolve_rate_table_name",
|
||||
"resolve_rate_tables",
|
||||
"resolve_rate_view_name",
|
||||
"resolve_rate_view_names",
|
||||
"substitute_env_placeholders",
|
||||
"substitute_mapping_values",
|
||||
"update_history",
|
||||
"update_history_with_config",
|
||||
"update_sltp_for_open_positions",
|
||||
"update_trailing_stop_loss_for_open_positions",
|
||||
})
|
||||
|
||||
SECONDARY_PUBLIC_EXPORTS: frozenset[str] = frozenset({
|
||||
"DEDUP_KEYS",
|
||||
"DataKind",
|
||||
"Dataset",
|
||||
"IfExists",
|
||||
"KNOWN_MT5_TIME_COLUMNS",
|
||||
"POSITION_COLUMNS",
|
||||
"REQUIRED_COLUMNS",
|
||||
"TICK_FLAG_MAP",
|
||||
"TIMEFRAME_MAP",
|
||||
"TIME_COLUMNS",
|
||||
"account_info",
|
||||
"collect_latest_rates",
|
||||
"collect_latest_rates_for_accounts",
|
||||
"copy_rates_from",
|
||||
"copy_rates_from_pos",
|
||||
"copy_rates_range",
|
||||
"copy_ticks_from",
|
||||
"copy_ticks_range",
|
||||
"detect_format",
|
||||
"ensure_utc",
|
||||
"export_dataframe",
|
||||
"export_dataframe_to_sqlite",
|
||||
"granularity_name",
|
||||
"history_deals",
|
||||
"history_orders",
|
||||
"last_error",
|
||||
"latest_rates",
|
||||
"market_book",
|
||||
"minimum_margins",
|
||||
"mt5_summary",
|
||||
"mt5_summary_as_df",
|
||||
"mt5_version",
|
||||
"normalize_dataframe",
|
||||
"normalize_symbol",
|
||||
"normalize_symbols",
|
||||
"normalize_time_columns",
|
||||
"orders",
|
||||
"parse_date_range",
|
||||
"parse_datetime",
|
||||
"parse_tick_flags",
|
||||
"parse_timeframe",
|
||||
"positions",
|
||||
"recent_history_deals",
|
||||
"recent_ticks",
|
||||
"recent_window",
|
||||
"schema_columns",
|
||||
"symbol_info",
|
||||
"symbol_info_tick",
|
||||
"symbols",
|
||||
"terminal_info",
|
||||
"validate_schema",
|
||||
})
|
||||
|
||||
PUBLIC_EXPORT_TIERS: dict[str, frozenset[str]] = {
|
||||
"stable": STABLE_SDK_EXPORTS,
|
||||
"secondary": SECONDARY_PUBLIC_EXPORTS,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"PUBLIC_EXPORT_TIERS",
|
||||
"SECONDARY_PUBLIC_EXPORTS",
|
||||
"STABLE_SDK_EXPORTS",
|
||||
]
|
||||
@@ -1,162 +0,0 @@
|
||||
"""Shared conversion helpers for MT5 symbols, timeframes, and date ranges."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pdmt5 import get_timeframe_name as _get_timeframe_name
|
||||
|
||||
from .utils import parse_datetime, parse_tick_flags, parse_timeframe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
__all__ = [
|
||||
"ensure_utc",
|
||||
"granularity_name",
|
||||
"normalize_symbol",
|
||||
"normalize_symbols",
|
||||
"parse_date_range",
|
||||
"parse_datetime",
|
||||
"parse_tick_flags",
|
||||
"parse_timeframe",
|
||||
"recent_window",
|
||||
]
|
||||
|
||||
|
||||
def normalize_symbol(symbol: str) -> str:
|
||||
"""Normalize a broker symbol name for MT5 API calls.
|
||||
|
||||
Strips surrounding whitespace while preserving broker-specific casing and
|
||||
suffixes (for example ``XAUUSDm``, ``US500.cash``, or ``EURUSD.r``).
|
||||
|
||||
Args:
|
||||
symbol: Raw symbol name.
|
||||
|
||||
Returns:
|
||||
Normalized symbol string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the symbol is empty after normalization.
|
||||
"""
|
||||
normalized = symbol.strip()
|
||||
if not normalized:
|
||||
msg = "Symbol must not be empty."
|
||||
raise ValueError(msg)
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_symbols(symbols: Sequence[str]) -> list[str]:
|
||||
"""Normalize a sequence of broker symbol names.
|
||||
|
||||
Args:
|
||||
symbols: Raw symbol names.
|
||||
|
||||
Returns:
|
||||
List of normalized, de-duplicated symbols preserving first-seen order.
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
resolved: list[str] = []
|
||||
for symbol in symbols:
|
||||
normalized = normalize_symbol(symbol)
|
||||
if normalized not in seen:
|
||||
seen.add(normalized)
|
||||
resolved.append(normalized)
|
||||
return resolved
|
||||
|
||||
|
||||
def ensure_utc(value: datetime | str) -> datetime:
|
||||
"""Return a timezone-aware UTC datetime.
|
||||
|
||||
Args:
|
||||
value: Datetime instance or ISO 8601 string.
|
||||
|
||||
Returns:
|
||||
UTC-aware datetime.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return parse_datetime(value)
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=UTC)
|
||||
return value.astimezone(UTC)
|
||||
|
||||
|
||||
def parse_date_range(
|
||||
date_from: datetime | str,
|
||||
date_to: datetime | str,
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""Parse and validate an inclusive UTC date range.
|
||||
|
||||
Args:
|
||||
date_from: Range start as datetime or ISO 8601 string.
|
||||
date_to: Range end as datetime or ISO 8601 string.
|
||||
|
||||
Returns:
|
||||
Tuple of UTC-aware ``(start, end)`` datetimes.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``date_from`` is after ``date_to``.
|
||||
"""
|
||||
start = ensure_utc(date_from)
|
||||
end = ensure_utc(date_to)
|
||||
if start > end:
|
||||
msg = (
|
||||
f"date_from ({start.isoformat()}) must not be after "
|
||||
f"date_to ({end.isoformat()})."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return start, end
|
||||
|
||||
|
||||
def recent_window(
|
||||
*,
|
||||
hours: float | None = None,
|
||||
seconds: float | None = None,
|
||||
date_to: datetime | str | None = None,
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""Build a trailing UTC window ending at ``date_to`` or now.
|
||||
|
||||
Exactly one of ``hours`` or ``seconds`` must be provided.
|
||||
|
||||
Args:
|
||||
hours: Trailing window length in hours.
|
||||
seconds: Trailing window length in seconds.
|
||||
date_to: Window end. Defaults to current UTC time.
|
||||
|
||||
Returns:
|
||||
Tuple of UTC-aware ``(start, end)`` datetimes.
|
||||
|
||||
Raises:
|
||||
ValueError: If neither or both window lengths are provided, or if a
|
||||
length is not positive.
|
||||
"""
|
||||
if (hours is None) == (seconds is None):
|
||||
msg = "Provide exactly one of hours or seconds."
|
||||
raise ValueError(msg)
|
||||
if hours is not None:
|
||||
length = timedelta(hours=hours)
|
||||
else:
|
||||
length = timedelta(seconds=seconds if seconds is not None else 0)
|
||||
if length.total_seconds() <= 0:
|
||||
msg = "Window length must be positive."
|
||||
raise ValueError(msg)
|
||||
end = ensure_utc(date_to) if date_to is not None else datetime.now(UTC)
|
||||
return end - length, end
|
||||
|
||||
|
||||
def granularity_name(timeframe: int | str) -> str:
|
||||
"""Return a short granularity label for a timeframe integer or name.
|
||||
|
||||
Args:
|
||||
timeframe: MT5 timeframe as integer or name (for example ``M1``).
|
||||
|
||||
Returns:
|
||||
Short name such as ``M1`` or the stringified integer when unknown.
|
||||
"""
|
||||
tf = parse_timeframe(timeframe)
|
||||
try:
|
||||
name = _get_timeframe_name(tf)
|
||||
except ValueError:
|
||||
return str(tf)
|
||||
return name.removeprefix("TIMEFRAME_")
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Normalized exception types for MT5 and mt5cli operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from pdmt5 import Mt5RuntimeError, Mt5TradingError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
__all__ = [
|
||||
"Mt5CliError",
|
||||
"Mt5ConnectionError",
|
||||
"Mt5OperationError",
|
||||
"Mt5SchemaError",
|
||||
"call_with_normalized_errors",
|
||||
"is_recoverable_mt5_error",
|
||||
"normalize_mt5_exception",
|
||||
]
|
||||
|
||||
_RECOVERABLE_MT5_ERRORS: tuple[type[BaseException], ...] = (
|
||||
Mt5TradingError,
|
||||
Mt5RuntimeError,
|
||||
)
|
||||
|
||||
|
||||
class Mt5CliError(Exception):
|
||||
"""Base exception for mt5cli public API errors."""
|
||||
|
||||
|
||||
class Mt5ConnectionError(Mt5CliError):
|
||||
"""Raised when MT5 initialization, login, or shutdown fails."""
|
||||
|
||||
|
||||
class Mt5OperationError(Mt5CliError):
|
||||
"""Raised when an MT5 data or trading operation fails."""
|
||||
|
||||
|
||||
class Mt5SchemaError(Mt5CliError):
|
||||
"""Raised when a DataFrame does not match an expected dataset schema."""
|
||||
|
||||
|
||||
def is_recoverable_mt5_error(exc: BaseException) -> bool:
|
||||
"""Return whether an exception is a transient MT5 failure worth retrying.
|
||||
|
||||
Args:
|
||||
exc: Exception raised by MT5 or pdmt5.
|
||||
|
||||
Returns:
|
||||
True for ``Mt5RuntimeError`` and ``Mt5TradingError``.
|
||||
"""
|
||||
return isinstance(exc, _RECOVERABLE_MT5_ERRORS)
|
||||
|
||||
|
||||
def normalize_mt5_exception(exc: BaseException) -> Mt5CliError:
|
||||
"""Map pdmt5/MT5 exceptions to stable mt5cli exception types.
|
||||
|
||||
Args:
|
||||
exc: Original exception from MT5 or pdmt5.
|
||||
|
||||
Returns:
|
||||
``Mt5ConnectionError`` for runtime failures, ``Mt5OperationError`` for
|
||||
trading failures, or the original exception when it is not recognized.
|
||||
"""
|
||||
if isinstance(exc, Mt5TradingError):
|
||||
return Mt5OperationError(str(exc))
|
||||
if isinstance(exc, Mt5RuntimeError):
|
||||
return Mt5ConnectionError(str(exc))
|
||||
if isinstance(exc, Mt5CliError):
|
||||
return exc
|
||||
return Mt5CliError(str(exc))
|
||||
|
||||
|
||||
def call_with_normalized_errors(fn: Callable[[], T]) -> T:
|
||||
"""Run ``fn`` and map recoverable MT5 errors to mt5cli types.
|
||||
|
||||
Args:
|
||||
fn: Callable performing MT5 work.
|
||||
|
||||
Returns:
|
||||
Value returned by ``fn``.
|
||||
"""
|
||||
try:
|
||||
return fn()
|
||||
except _RECOVERABLE_MT5_ERRORS as exc:
|
||||
normalized = normalize_mt5_exception(exc)
|
||||
raise normalized from exc
|
||||
-1964
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
"""Retry and reconnect helpers for transient MT5 failures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from .exceptions import is_recoverable_mt5_error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"retry_with_backoff",
|
||||
]
|
||||
|
||||
|
||||
def retry_with_backoff(
|
||||
fn: Callable[[], T],
|
||||
*,
|
||||
retry_count: int = 0,
|
||||
backoff_base: float = 2.0,
|
||||
operation: str = "MT5 operation",
|
||||
) -> T:
|
||||
"""Call ``fn`` with bounded exponential backoff on recoverable MT5 errors.
|
||||
|
||||
Only ``pdmt5.Mt5RuntimeError`` and ``pdmt5.Mt5TradingError`` are retried.
|
||||
Other exceptions propagate immediately. The final failure is re-raised once
|
||||
retries are exhausted.
|
||||
|
||||
Args:
|
||||
fn: Callable performing MT5 work.
|
||||
retry_count: Maximum number of retries after the first attempt. ``0``
|
||||
disables retries.
|
||||
backoff_base: Base for exponential backoff. The delay before retry
|
||||
attempt ``n`` (1-indexed) is ``backoff_base ** n`` seconds.
|
||||
operation: Label used in warning logs.
|
||||
|
||||
Returns:
|
||||
Value returned by ``fn`` on success.
|
||||
"""
|
||||
attempts = max(retry_count, 0) + 1
|
||||
for attempt in range(attempts - 1):
|
||||
try:
|
||||
return fn()
|
||||
except Exception as exc:
|
||||
if not is_recoverable_mt5_error(exc):
|
||||
raise
|
||||
delay = backoff_base ** (attempt + 1)
|
||||
logger.warning(
|
||||
"%s failed (attempt %d/%d): %s; retrying in %.1fs",
|
||||
operation,
|
||||
attempt + 1,
|
||||
attempts,
|
||||
exc,
|
||||
delay,
|
||||
)
|
||||
time.sleep(delay)
|
||||
return fn()
|
||||
@@ -1,291 +0,0 @@
|
||||
"""Canonical DataFrame schemas for MT5 market and account datasets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from .converters import normalize_symbol, parse_timeframe
|
||||
from .exceptions import Mt5SchemaError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
__all__ = [
|
||||
"DEDUP_KEYS",
|
||||
"KNOWN_MT5_TIME_COLUMNS",
|
||||
"REQUIRED_COLUMNS",
|
||||
"TIME_COLUMNS",
|
||||
"DataKind",
|
||||
"normalize_dataframe",
|
||||
"normalize_time_columns",
|
||||
"schema_columns",
|
||||
"validate_schema",
|
||||
]
|
||||
|
||||
KNOWN_MT5_TIME_COLUMNS: Final[frozenset[str]] = frozenset({
|
||||
"time",
|
||||
"time_setup",
|
||||
"time_setup_msc",
|
||||
"time_done",
|
||||
"time_done_msc",
|
||||
"time_msc",
|
||||
})
|
||||
|
||||
_TIME_COLUMN_NAMES = KNOWN_MT5_TIME_COLUMNS
|
||||
|
||||
|
||||
class DataKind(StrEnum):
|
||||
"""Supported MT5 dataset kinds with canonical column contracts."""
|
||||
|
||||
rates = "rates"
|
||||
ticks = "ticks"
|
||||
orders = "orders"
|
||||
positions = "positions"
|
||||
history_orders = "history_orders"
|
||||
history_deals = "history_deals"
|
||||
|
||||
|
||||
REQUIRED_COLUMNS: dict[DataKind, frozenset[str]] = {
|
||||
DataKind.rates: frozenset({
|
||||
"time",
|
||||
"open",
|
||||
"high",
|
||||
"low",
|
||||
"close",
|
||||
"tick_volume",
|
||||
"spread",
|
||||
"real_volume",
|
||||
}),
|
||||
DataKind.ticks: frozenset({
|
||||
"time",
|
||||
"bid",
|
||||
"ask",
|
||||
"last",
|
||||
"volume",
|
||||
"time_msc",
|
||||
"flags",
|
||||
"volume_real",
|
||||
}),
|
||||
DataKind.orders: frozenset({
|
||||
"ticket",
|
||||
"time_setup",
|
||||
"type",
|
||||
"state",
|
||||
"symbol",
|
||||
"volume_current",
|
||||
"price_open",
|
||||
}),
|
||||
DataKind.positions: frozenset({
|
||||
"ticket",
|
||||
"time",
|
||||
"type",
|
||||
"symbol",
|
||||
"volume",
|
||||
"price_open",
|
||||
"price_current",
|
||||
"profit",
|
||||
}),
|
||||
DataKind.history_orders: frozenset({
|
||||
"ticket",
|
||||
"time_setup",
|
||||
"type",
|
||||
"state",
|
||||
"symbol",
|
||||
"volume_initial",
|
||||
"price_open",
|
||||
}),
|
||||
DataKind.history_deals: frozenset({
|
||||
"ticket",
|
||||
"order",
|
||||
"time",
|
||||
"type",
|
||||
"entry",
|
||||
"symbol",
|
||||
"volume",
|
||||
"price",
|
||||
"profit",
|
||||
}),
|
||||
}
|
||||
|
||||
_OPTIONAL_TIME_COLUMNS_BY_KIND: dict[DataKind, frozenset[str]] = {
|
||||
DataKind.orders: frozenset({
|
||||
"time_setup_msc",
|
||||
"time_done",
|
||||
"time_done_msc",
|
||||
}),
|
||||
DataKind.history_orders: frozenset({
|
||||
"time_setup_msc",
|
||||
"time_done",
|
||||
"time_done_msc",
|
||||
}),
|
||||
DataKind.positions: frozenset({"time_msc"}),
|
||||
}
|
||||
|
||||
TIME_COLUMNS: dict[DataKind, frozenset[str]] = {
|
||||
kind: (REQUIRED_COLUMNS[kind] & _TIME_COLUMN_NAMES)
|
||||
| _OPTIONAL_TIME_COLUMNS_BY_KIND.get(kind, frozenset())
|
||||
for kind in DataKind
|
||||
}
|
||||
|
||||
DEDUP_KEYS: dict[DataKind, tuple[tuple[str, ...], ...]] = {
|
||||
DataKind.rates: (("symbol", "timeframe", "time"), ("symbol", "time")),
|
||||
DataKind.ticks: (("symbol", "time_msc"), ("symbol", "time")),
|
||||
DataKind.history_orders: (("ticket",), ("symbol", "time", "type")),
|
||||
DataKind.history_deals: (("ticket",), ("symbol", "time", "type", "entry")),
|
||||
}
|
||||
|
||||
|
||||
def schema_columns(kind: DataKind) -> frozenset[str]:
|
||||
"""Return required column names for a dataset kind.
|
||||
|
||||
Args:
|
||||
kind: Dataset kind.
|
||||
|
||||
Returns:
|
||||
Required column names for ``kind``.
|
||||
"""
|
||||
return REQUIRED_COLUMNS[kind]
|
||||
|
||||
|
||||
def validate_schema(
|
||||
frame: pd.DataFrame,
|
||||
kind: DataKind,
|
||||
*,
|
||||
extra_required: Iterable[str] | None = None,
|
||||
) -> None:
|
||||
"""Validate that a DataFrame includes required columns for a dataset kind.
|
||||
|
||||
Args:
|
||||
frame: DataFrame to validate.
|
||||
kind: Expected dataset kind.
|
||||
extra_required: Additional columns that must be present (for example
|
||||
``symbol`` and ``timeframe`` on stored rate history).
|
||||
|
||||
Raises:
|
||||
Mt5SchemaError: If required columns are missing.
|
||||
"""
|
||||
if frame.empty and len(frame.columns) == 0:
|
||||
return
|
||||
required = set(REQUIRED_COLUMNS[kind])
|
||||
if extra_required is not None:
|
||||
required.update(extra_required)
|
||||
missing = required - set(frame.columns)
|
||||
if missing:
|
||||
msg = (
|
||||
f"{kind.value} schema is missing required columns: "
|
||||
f"{', '.join(sorted(missing))}."
|
||||
)
|
||||
raise Mt5SchemaError(msg)
|
||||
|
||||
|
||||
def _coerce_mt5_time_column(series: pd.Series, column: str) -> pd.Series:
|
||||
"""Coerce one MT5 time column to UTC-aware datetimes.
|
||||
|
||||
Returns:
|
||||
Series with UTC-aware datetime values.
|
||||
"""
|
||||
if pd.api.types.is_datetime64_any_dtype(series):
|
||||
return pd.to_datetime(series, utc=True, errors="coerce")
|
||||
if pd.api.types.is_numeric_dtype(series):
|
||||
unit = "ms" if column.endswith("_msc") else "s"
|
||||
return pd.to_datetime(series, unit=unit, utc=True, errors="coerce")
|
||||
return pd.to_datetime(series, utc=True, errors="coerce")
|
||||
|
||||
|
||||
def normalize_time_columns(frame: pd.DataFrame, kind: DataKind) -> pd.DataFrame:
|
||||
"""Coerce dataset time columns to UTC-aware datetimes when present.
|
||||
|
||||
Any column in :data:`KNOWN_MT5_TIME_COLUMNS` that is present in ``frame``
|
||||
is normalized. Numeric MT5 epoch values use seconds for ``time``,
|
||||
``time_setup``, and ``time_done``, and milliseconds for ``*_msc`` columns.
|
||||
|
||||
Args:
|
||||
frame: Source DataFrame from MT5 or pdmt5.
|
||||
kind: Dataset kind (retained for API compatibility).
|
||||
|
||||
Returns:
|
||||
DataFrame copy with normalized time columns.
|
||||
"""
|
||||
del kind
|
||||
normalized = frame.copy()
|
||||
for column in normalized.columns:
|
||||
if column not in _TIME_COLUMN_NAMES:
|
||||
continue
|
||||
normalized[column] = _coerce_mt5_time_column(normalized[column], column)
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_dataframe(
|
||||
frame: pd.DataFrame,
|
||||
kind: DataKind,
|
||||
*,
|
||||
symbol: str | None = None,
|
||||
timeframe: int | str | None = None,
|
||||
sort: bool = True,
|
||||
) -> pd.DataFrame:
|
||||
"""Normalize MT5 DataFrame columns, timestamps, and storage metadata.
|
||||
|
||||
Ensures UTC timestamps, optionally injects ``symbol`` / ``timeframe`` for
|
||||
storage-oriented datasets, and sorts chronologically when a ``time`` column
|
||||
exists.
|
||||
|
||||
Args:
|
||||
frame: Source DataFrame from MT5 or pdmt5.
|
||||
kind: Dataset kind guiding normalization rules.
|
||||
symbol: Optional symbol to inject when missing.
|
||||
timeframe: Optional timeframe integer or name to inject for rates.
|
||||
sort: Whether to sort by ``time`` or ``time_msc`` when present.
|
||||
|
||||
Returns:
|
||||
Normalized DataFrame copy.
|
||||
"""
|
||||
if frame.empty and len(frame.columns) == 0:
|
||||
return frame.copy()
|
||||
|
||||
normalized = normalize_time_columns(frame, kind)
|
||||
|
||||
if symbol is not None and "symbol" not in normalized.columns:
|
||||
normalized.insert(0, "symbol", normalize_symbol(symbol))
|
||||
|
||||
if timeframe is not None and kind is DataKind.rates:
|
||||
tf = parse_timeframe(timeframe)
|
||||
if "timeframe" not in normalized.columns:
|
||||
insert_at = 1 if "symbol" in normalized.columns else 0
|
||||
normalized.insert(insert_at, "timeframe", tf)
|
||||
|
||||
validate_schema(normalized, kind)
|
||||
|
||||
if sort:
|
||||
if "time" in normalized.columns:
|
||||
normalized = normalized.sort_values("time", kind="stable")
|
||||
elif "time_msc" in normalized.columns:
|
||||
normalized = normalized.sort_values("time_msc", kind="stable")
|
||||
normalized = normalized.reset_index(drop=True)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def ensure_utc_columns(frame: pd.DataFrame, columns: Iterable[str]) -> pd.DataFrame:
|
||||
"""Return a copy with selected columns coerced to UTC datetimes.
|
||||
|
||||
Args:
|
||||
frame: Source DataFrame.
|
||||
columns: Column names to coerce.
|
||||
|
||||
Returns:
|
||||
DataFrame copy with UTC-aware datetime columns.
|
||||
"""
|
||||
normalized = frame.copy()
|
||||
for column in columns:
|
||||
if column not in normalized.columns:
|
||||
continue
|
||||
if column in _TIME_COLUMN_NAMES:
|
||||
normalized[column] = _coerce_mt5_time_column(normalized[column], column)
|
||||
else:
|
||||
normalized[column] = pd.to_datetime(
|
||||
normalized[column], utc=True, errors="coerce"
|
||||
)
|
||||
return normalized
|
||||
-2142
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
"""Generic storage helpers for MT5 market and account history."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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_rate_tables,
|
||||
resolve_rate_view_name,
|
||||
resolve_rate_view_names,
|
||||
)
|
||||
from .sdk import collect_history, update_history, update_history_with_config
|
||||
from .utils import (
|
||||
Dataset,
|
||||
IfExists,
|
||||
OutputFormat,
|
||||
detect_format,
|
||||
export_dataframe,
|
||||
export_dataframe_to_sqlite,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Dataset",
|
||||
"IfExists",
|
||||
"OutputFormat",
|
||||
"RateTarget",
|
||||
"build_rate_targets",
|
||||
"build_rate_view_name",
|
||||
"collect_history",
|
||||
"detect_format",
|
||||
"drop_forming_rate_bar",
|
||||
"export_dataframe",
|
||||
"export_dataframe_to_sqlite",
|
||||
"load_rate_data",
|
||||
"load_rate_data_from_connection",
|
||||
"load_rate_series_by_granularity",
|
||||
"load_rate_series_from_sqlite",
|
||||
"resolve_rate_tables",
|
||||
"resolve_rate_view_name",
|
||||
"resolve_rate_view_names",
|
||||
"update_history",
|
||||
"update_history_with_config",
|
||||
]
|
||||
-1720
File diff suppressed because it is too large
Load Diff
-457
@@ -1,457 +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 coerce_login(login: int | str | None) -> int | None:
|
||||
"""Coerce a login value to int, treating empty strings as unset.
|
||||
|
||||
Returns:
|
||||
Integer login, or None when unset or an empty string.
|
||||
"""
|
||||
if login is None or isinstance(login, int):
|
||||
return login
|
||||
text = login.strip()
|
||||
if not text:
|
||||
return None
|
||||
return int(text)
|
||||
|
||||
|
||||
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:
|
||||
ImportError: If the parquet format is requested but pyarrow is not installed.
|
||||
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":
|
||||
try:
|
||||
__import__("pyarrow")
|
||||
except ImportError as exc:
|
||||
msg = (
|
||||
"Parquet export requires the optional dependency pyarrow. "
|
||||
'Install it with: pip install "mt5cli[parquet]"'
|
||||
)
|
||||
raise ImportError(msg) from exc
|
||||
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
Binary file not shown.
-185
@@ -1,185 +0,0 @@
|
||||
[project]
|
||||
name = "mt5cli"
|
||||
version = "0.9.7"
|
||||
description = "Generic MT5 data and execution infrastructure for Python applications"
|
||||
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",
|
||||
"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.optional-dependencies]
|
||||
parquet = ["pyarrow >= 19.0.0"]
|
||||
|
||||
[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",
|
||||
"pyarrow >= 19.0.0",
|
||||
"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]
|
||||
"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
File diff suppressed because it is too large
Load Diff
+109
@@ -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, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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>
|
||||
Binary file not shown.
@@ -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 +0,0 @@
|
||||
"""Test suite for mt5cli."""
|
||||
@@ -1,52 +0,0 @@
|
||||
"""Shared pytest fixtures for mt5cli tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture # noqa: TC002
|
||||
|
||||
_DATAFRAME_METHODS = (
|
||||
"copy_rates_from_as_df",
|
||||
"copy_rates_from_pos_as_df",
|
||||
"copy_rates_range_as_df",
|
||||
"copy_ticks_from_as_df",
|
||||
"copy_ticks_range_as_df",
|
||||
"account_info_as_df",
|
||||
"terminal_info_as_df",
|
||||
"symbols_get_as_df",
|
||||
"symbol_info_as_df",
|
||||
"orders_get_as_df",
|
||||
"positions_get_as_df",
|
||||
"history_orders_get_as_df",
|
||||
"history_deals_get_as_df",
|
||||
"version_as_df",
|
||||
"last_error_as_df",
|
||||
"symbol_info_tick_as_df",
|
||||
"market_book_get_as_df",
|
||||
"order_check_as_df",
|
||||
"order_send_as_df",
|
||||
)
|
||||
|
||||
|
||||
def build_mock_mt5_data_client() -> MagicMock:
|
||||
"""Return a MagicMock Mt5DataClient with common DataFrame stubs."""
|
||||
client = MagicMock()
|
||||
sample_df = pd.DataFrame({"col": [1]})
|
||||
for method_name in _DATAFRAME_METHODS:
|
||||
getattr(client, method_name).return_value = sample_df
|
||||
client.version.return_value = (5, 0, 1)
|
||||
client.terminal_info.return_value = {"connected": True, "paths": ["terminal.exe"]}
|
||||
client.account_info.return_value = {"login": 123, "limits": {"modes": ["demo"]}}
|
||||
client.symbols_total.return_value = 42
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(mocker: MockerFixture) -> MagicMock:
|
||||
"""Create and patch a mock Mt5DataClient for CLI and SDK tests."""
|
||||
client = build_mock_mt5_data_client()
|
||||
mocker.patch("mt5cli.sdk.Mt5DataClient", return_value=client)
|
||||
return client
|
||||
-1768
File diff suppressed because it is too large
Load Diff
@@ -1,856 +0,0 @@
|
||||
"""Contract tests for the mt5cli public API and dataset schemas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import UTC, datetime
|
||||
from importlib.metadata import requires
|
||||
from pathlib import Path
|
||||
from typing import get_type_hints
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from pdmt5 import Mt5RuntimeError, Mt5TradingError
|
||||
from pytest_mock import MockerFixture # noqa: TC002
|
||||
|
||||
import mt5cli
|
||||
from mt5cli import (
|
||||
DEDUP_KEYS,
|
||||
PUBLIC_EXPORT_TIERS,
|
||||
REQUIRED_COLUMNS,
|
||||
SECONDARY_PUBLIC_EXPORTS,
|
||||
STABLE_SDK_EXPORTS,
|
||||
TIME_COLUMNS,
|
||||
AccountSpec,
|
||||
DataKind,
|
||||
Dataset,
|
||||
ExecutionStatus,
|
||||
MarginVolume,
|
||||
MT5Client,
|
||||
Mt5CliError,
|
||||
Mt5ConnectionError,
|
||||
Mt5OperationError,
|
||||
Mt5SchemaError,
|
||||
OrderExecutionResult,
|
||||
OrderLimits,
|
||||
RateTarget,
|
||||
build_config,
|
||||
build_rate_targets,
|
||||
calculate_account_projected_margin_ratio,
|
||||
calculate_margin_and_volume,
|
||||
calculate_positions_margin,
|
||||
calculate_projected_margin_ratio,
|
||||
calculate_symbol_group_margin_ratio,
|
||||
calculate_trailing_stop_updates,
|
||||
call_with_normalized_errors,
|
||||
detect_format,
|
||||
drop_forming_rate_bar,
|
||||
ensure_symbol_selected,
|
||||
ensure_utc,
|
||||
export_dataframe,
|
||||
export_dataframe_to_sqlite,
|
||||
extract_tick_price,
|
||||
fetch_latest_closed_rates,
|
||||
fetch_latest_closed_rates_for_trading_client,
|
||||
fetch_latest_closed_rates_indexed,
|
||||
granularity_name,
|
||||
is_recoverable_mt5_error,
|
||||
load_rate_data,
|
||||
load_rate_series_from_sqlite,
|
||||
mt5_session,
|
||||
mt5_trading_session,
|
||||
normalize_dataframe,
|
||||
normalize_mt5_exception,
|
||||
normalize_order_volume,
|
||||
normalize_symbol,
|
||||
normalize_symbols,
|
||||
parse_date_range,
|
||||
place_market_order,
|
||||
recent_window,
|
||||
resolve_account_spec,
|
||||
resolve_account_specs,
|
||||
resolve_rate_view_name,
|
||||
schema_columns,
|
||||
validate_schema,
|
||||
)
|
||||
from mt5cli.history import create_rate_compatibility_views
|
||||
from mt5cli.retry import retry_with_backoff
|
||||
from mt5cli.schemas import ensure_utc_columns, normalize_time_columns
|
||||
|
||||
|
||||
def _sample_frame(kind: DataKind) -> pd.DataFrame:
|
||||
if kind is DataKind.rates:
|
||||
return pd.DataFrame({
|
||||
"time": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"open": [1.1],
|
||||
"high": [1.2],
|
||||
"low": [1.0],
|
||||
"close": [1.15],
|
||||
"tick_volume": [10],
|
||||
"spread": [1],
|
||||
"real_volume": [0],
|
||||
})
|
||||
if kind is DataKind.ticks:
|
||||
return pd.DataFrame({
|
||||
"time": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"bid": [1.1],
|
||||
"ask": [1.11],
|
||||
"last": [1.105],
|
||||
"volume": [1],
|
||||
"time_msc": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"flags": [2],
|
||||
"volume_real": [0.0],
|
||||
})
|
||||
if kind is DataKind.orders:
|
||||
return pd.DataFrame({
|
||||
"ticket": [1],
|
||||
"time_setup": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"type": [0],
|
||||
"state": [1],
|
||||
"symbol": ["EURUSD"],
|
||||
"volume_current": [0.1],
|
||||
"price_open": [1.1],
|
||||
})
|
||||
if kind is DataKind.positions:
|
||||
return pd.DataFrame({
|
||||
"ticket": [1],
|
||||
"time": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"type": [0],
|
||||
"symbol": ["EURUSD"],
|
||||
"volume": [0.1],
|
||||
"price_open": [1.1],
|
||||
"price_current": [1.11],
|
||||
"profit": [1.0],
|
||||
})
|
||||
if kind is DataKind.history_orders:
|
||||
return pd.DataFrame({
|
||||
"ticket": [1],
|
||||
"time_setup": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"type": [0],
|
||||
"state": [3],
|
||||
"symbol": ["EURUSD"],
|
||||
"volume_initial": [0.1],
|
||||
"price_open": [1.1],
|
||||
})
|
||||
return pd.DataFrame({
|
||||
"ticket": [1],
|
||||
"order": [2],
|
||||
"time": [datetime(2024, 1, 1, tzinfo=UTC)],
|
||||
"type": [0],
|
||||
"entry": [0],
|
||||
"symbol": ["EURUSD"],
|
||||
"volume": [0.1],
|
||||
"price": [1.1],
|
||||
"profit": [0.0],
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", list(DataKind))
|
||||
def test_required_columns_contract(kind: DataKind) -> None:
|
||||
"""Each dataset kind exposes a non-empty required column contract."""
|
||||
assert REQUIRED_COLUMNS[kind]
|
||||
validate_schema(_sample_frame(kind), kind)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", list(DataKind))
|
||||
def test_normalize_dataframe_injects_storage_metadata(kind: DataKind) -> None:
|
||||
"""Normalization accepts MT5 frames and optional storage metadata."""
|
||||
frame = _sample_frame(kind)
|
||||
normalized = normalize_dataframe(
|
||||
frame,
|
||||
kind,
|
||||
symbol="eurusd",
|
||||
timeframe="M1" if kind is DataKind.rates else None,
|
||||
)
|
||||
if kind is DataKind.rates:
|
||||
assert normalized.loc[0, "symbol"] == "eurusd"
|
||||
assert normalized.loc[0, "timeframe"] == 1
|
||||
validate_schema(normalized, kind)
|
||||
|
||||
|
||||
def test_validate_schema_raises_for_missing_columns() -> None:
|
||||
"""Schema validation fails fast on missing required columns."""
|
||||
with pytest.raises(Mt5SchemaError, match="missing required columns"):
|
||||
validate_schema(pd.DataFrame({"time": [1]}), DataKind.rates)
|
||||
|
||||
|
||||
def test_history_dedup_keys_match_schema_contract() -> None:
|
||||
"""SQLite history dedup keys stay aligned with schema contracts."""
|
||||
assert DEDUP_KEYS[DataKind.rates][0] == ("symbol", "timeframe", "time")
|
||||
assert DEDUP_KEYS[DataKind.ticks][0] == ("symbol", "time_msc")
|
||||
assert Dataset.rates.table_name == "rates"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "expected"),
|
||||
[
|
||||
(" eurusd ", "eurusd"),
|
||||
("GbpJpy", "GbpJpy"),
|
||||
("XAUUSDm", "XAUUSDm"),
|
||||
("US500.cash", "US500.cash"),
|
||||
("EURUSD.r", "EURUSD.r"),
|
||||
],
|
||||
)
|
||||
def test_normalize_symbol(raw: str, expected: str) -> None:
|
||||
"""Symbol normalization trims whitespace and preserves broker casing."""
|
||||
assert normalize_symbol(raw) == expected
|
||||
|
||||
|
||||
def test_normalize_symbols_deduplicates() -> None:
|
||||
"""Symbol lists are normalized and de-duplicated in order."""
|
||||
assert normalize_symbols(["XAUUSDm", " XAUUSDm ", "EURUSD.r", "eurusd"]) == [
|
||||
"XAUUSDm",
|
||||
"EURUSD.r",
|
||||
"eurusd",
|
||||
]
|
||||
|
||||
|
||||
def test_parse_date_range_rejects_inverted_bounds() -> None:
|
||||
"""Date ranges must not be inverted."""
|
||||
with pytest.raises(ValueError, match="must not be after"):
|
||||
parse_date_range("2024-02-01", "2024-01-01")
|
||||
|
||||
|
||||
def test_recent_window_builds_trailing_bounds() -> None:
|
||||
"""Recent windows end at the provided timestamp."""
|
||||
end = datetime(2024, 1, 2, tzinfo=UTC)
|
||||
start, resolved_end = recent_window(hours=24, date_to=end)
|
||||
assert resolved_end == end
|
||||
assert start < end
|
||||
|
||||
|
||||
def test_granularity_name_maps_timeframe_alias() -> None:
|
||||
"""Granularity labels resolve MT5 timeframe aliases."""
|
||||
assert granularity_name("M1") == "M1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[Mt5RuntimeError("init failed"), Mt5TradingError("trade failed")],
|
||||
)
|
||||
def test_is_recoverable_mt5_error(exc: Exception) -> None:
|
||||
"""Recoverable MT5 errors are classified consistently."""
|
||||
assert is_recoverable_mt5_error(exc)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exc", "expected_type"),
|
||||
[
|
||||
(Mt5RuntimeError("x"), Mt5ConnectionError),
|
||||
(Mt5TradingError("x"), Mt5OperationError),
|
||||
],
|
||||
)
|
||||
def test_normalize_mt5_exception_maps_types(
|
||||
exc: Exception,
|
||||
expected_type: type[Mt5ConnectionError | Mt5OperationError],
|
||||
) -> None:
|
||||
"""MT5 exceptions map to stable mt5cli types."""
|
||||
assert isinstance(normalize_mt5_exception(exc), expected_type)
|
||||
|
||||
|
||||
def test_call_with_normalized_errors_reraises_mapped_type() -> None:
|
||||
"""Normalized error helper re-raises mapped mt5cli exceptions."""
|
||||
|
||||
def _raise() -> None:
|
||||
message = "boom"
|
||||
raise Mt5RuntimeError(message)
|
||||
|
||||
with pytest.raises(Mt5ConnectionError):
|
||||
call_with_normalized_errors(_raise)
|
||||
|
||||
|
||||
def test_retry_with_backoff_retries_recoverable_errors(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Retry helper retries recoverable MT5 failures."""
|
||||
calls = {"count": 0}
|
||||
|
||||
def _flaky() -> str:
|
||||
calls["count"] += 1
|
||||
if calls["count"] == 1:
|
||||
message = "transient"
|
||||
raise Mt5RuntimeError(message)
|
||||
return "ok"
|
||||
|
||||
mocker.patch("mt5cli.retry.time.sleep")
|
||||
assert retry_with_backoff(_flaky, retry_count=1) == "ok"
|
||||
assert calls["count"] == 2
|
||||
|
||||
|
||||
def test_public_api_exports_mt5_client() -> None:
|
||||
"""MT5Client is the primary importable client abstraction."""
|
||||
client = MT5Client(config=build_config())
|
||||
assert isinstance(client, MT5Client)
|
||||
assert isinstance(client, MT5Client.__mro__[1])
|
||||
|
||||
|
||||
def test_mt5_client_order_primitives_use_connected_client(
|
||||
mock_client: object,
|
||||
) -> None:
|
||||
"""Order check/send route through the same client fetch path as exports."""
|
||||
request = {"action": 1}
|
||||
client = MT5Client()
|
||||
client.order_check(request)
|
||||
client.order_send(request)
|
||||
assert mock_client.order_check_as_df.call_count == 1 # type: ignore[attr-defined]
|
||||
assert mock_client.order_send_as_df.call_count == 1 # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_storage_export_round_trip_csv(tmp_path: Path) -> None:
|
||||
"""Storage helpers export normalized rate frames to CSV."""
|
||||
frame = normalize_dataframe(
|
||||
_sample_frame(DataKind.rates),
|
||||
DataKind.rates,
|
||||
symbol="EURUSD",
|
||||
timeframe="M1",
|
||||
)
|
||||
output = tmp_path / "rates.csv"
|
||||
export_dataframe(frame, output, detect_format(output))
|
||||
loaded = pd.read_csv(output)
|
||||
assert len(loaded) == 1
|
||||
assert "close" in loaded.columns
|
||||
|
||||
|
||||
def test_normalize_symbol_rejects_empty_value() -> None:
|
||||
"""Empty symbols are rejected after trimming."""
|
||||
with pytest.raises(ValueError, match="must not be empty"):
|
||||
normalize_symbol(" ")
|
||||
|
||||
|
||||
def test_ensure_utc_handles_naive_and_aware_datetimes() -> None:
|
||||
"""UTC coercion accepts naive and timezone-aware datetimes."""
|
||||
naive = datetime(2024, 1, 1, tzinfo=UTC).replace(tzinfo=None)
|
||||
aware = datetime(2024, 1, 1, tzinfo=UTC)
|
||||
assert ensure_utc(naive).tzinfo == UTC
|
||||
assert ensure_utc(aware).tzinfo == UTC
|
||||
assert ensure_utc("2024-01-01T00:00:00+00:00").tzinfo == UTC
|
||||
|
||||
|
||||
def test_recent_window_validation_errors() -> None:
|
||||
"""Recent window helpers validate mutually exclusive length arguments."""
|
||||
with pytest.raises(ValueError, match="exactly one"):
|
||||
recent_window()
|
||||
with pytest.raises(ValueError, match="exactly one"):
|
||||
recent_window(hours=1, seconds=1)
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
recent_window(hours=0)
|
||||
|
||||
|
||||
def test_recent_window_supports_seconds_argument() -> None:
|
||||
"""Recent windows can be built from a seconds-based length."""
|
||||
end = datetime(2024, 1, 2, tzinfo=UTC)
|
||||
start, resolved_end = recent_window(seconds=3600, date_to=end)
|
||||
assert resolved_end == end
|
||||
assert start < end
|
||||
|
||||
|
||||
def test_parse_date_range_returns_ordered_bounds() -> None:
|
||||
"""Valid date ranges return UTC-aware bounds."""
|
||||
start, end = parse_date_range("2024-01-01", "2024-02-01")
|
||||
assert start < end
|
||||
|
||||
|
||||
def test_granularity_name_falls_back_for_unknown_timeframe(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Unknown timeframe integers stringify as granularity labels."""
|
||||
mocker.patch(
|
||||
"mt5cli.converters._get_timeframe_name",
|
||||
side_effect=ValueError("unknown"),
|
||||
)
|
||||
assert granularity_name(1) == "1"
|
||||
|
||||
|
||||
def test_normalize_mt5_exception_passthrough_and_generic() -> None:
|
||||
"""Normalization preserves mt5cli errors and wraps unknown exceptions."""
|
||||
original = Mt5CliError("known")
|
||||
assert normalize_mt5_exception(original) is original
|
||||
assert isinstance(normalize_mt5_exception(ValueError("x")), Mt5CliError)
|
||||
|
||||
|
||||
def test_schema_columns_and_extra_required_validation() -> None:
|
||||
"""Schema helpers expose contracts and honor extra required columns."""
|
||||
assert schema_columns(DataKind.rates) == REQUIRED_COLUMNS[DataKind.rates]
|
||||
validate_schema(pd.DataFrame(), DataKind.rates)
|
||||
frame = _sample_frame(DataKind.rates)
|
||||
with pytest.raises(Mt5SchemaError, match="storage_symbol"):
|
||||
validate_schema(frame, DataKind.rates, extra_required=["storage_symbol"])
|
||||
|
||||
|
||||
def test_normalize_dataframe_empty_and_tick_sort_paths() -> None:
|
||||
"""Normalization handles empty frames and tick time_msc sorting."""
|
||||
empty = pd.DataFrame()
|
||||
assert normalize_dataframe(empty, DataKind.rates).empty
|
||||
|
||||
ticks = _sample_frame(DataKind.ticks)
|
||||
ticks = pd.concat([ticks, ticks], ignore_index=True)
|
||||
sorted_ticks = normalize_dataframe(ticks, DataKind.ticks, sort=True)
|
||||
assert len(sorted_ticks) == 2
|
||||
unsorted_ticks = normalize_dataframe(ticks, DataKind.ticks, sort=False)
|
||||
assert len(unsorted_ticks) == 2
|
||||
|
||||
|
||||
def test_normalize_dataframe_rate_timeframe_without_symbol() -> None:
|
||||
"""Rate normalization can inject timeframe without symbol metadata."""
|
||||
frame = _sample_frame(DataKind.rates)
|
||||
normalized = normalize_dataframe(frame, DataKind.rates, timeframe="M1")
|
||||
assert "timeframe" in normalized.columns
|
||||
|
||||
|
||||
def test_normalize_dataframe_keeps_existing_symbol_and_timeframe() -> None:
|
||||
"""Normalization does not duplicate existing storage metadata columns."""
|
||||
frame = normalize_dataframe(
|
||||
_sample_frame(DataKind.rates),
|
||||
DataKind.rates,
|
||||
symbol="EURUSD",
|
||||
timeframe="M1",
|
||||
)
|
||||
normalized = normalize_dataframe(
|
||||
frame,
|
||||
DataKind.rates,
|
||||
symbol="GBPUSD",
|
||||
timeframe="H1",
|
||||
)
|
||||
assert normalized.loc[0, "symbol"] == "EURUSD"
|
||||
assert normalized.loc[0, "timeframe"] == 1
|
||||
|
||||
|
||||
def test_normalize_time_columns_skips_absent_time_fields() -> None:
|
||||
"""Time normalization ignores absent optional time columns."""
|
||||
frame = pd.DataFrame({"open": [1.0]})
|
||||
result = normalize_time_columns(frame, DataKind.rates)
|
||||
assert list(result.columns) == ["open"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("col", "value", "kind"),
|
||||
[
|
||||
("time", 1704067200, DataKind.rates),
|
||||
("time_msc", 1704067200000, DataKind.ticks),
|
||||
("time", datetime(2024, 1, 1, tzinfo=UTC), DataKind.rates),
|
||||
("time", "2024-01-01T00:00:00+00:00", DataKind.rates),
|
||||
],
|
||||
)
|
||||
def test_normalize_time_columns_coerces_value(
|
||||
col: str,
|
||||
value: object,
|
||||
kind: DataKind,
|
||||
) -> None:
|
||||
"""Time column values are coerced to UTC timestamps regardless of input type."""
|
||||
frame = pd.DataFrame({col: [value]})
|
||||
result = normalize_time_columns(frame, kind)
|
||||
assert result.loc[0, col] == pd.Timestamp("2024-01-01T00:00:00+00:00")
|
||||
|
||||
|
||||
def test_normalize_time_columns_handles_optional_order_times() -> None:
|
||||
"""Optional order/history time columns are normalized when present."""
|
||||
frame = pd.DataFrame({
|
||||
"time_setup": [1704067200],
|
||||
"time_setup_msc": [1704067200000],
|
||||
"time_done": [1704153600],
|
||||
"time_done_msc": [1704153600000],
|
||||
})
|
||||
result = normalize_time_columns(frame, DataKind.orders)
|
||||
assert result.loc[0, "time_setup"] == pd.Timestamp("2024-01-01T00:00:00+00:00")
|
||||
assert result.loc[0, "time_setup_msc"] == pd.Timestamp(
|
||||
"2024-01-01T00:00:00+00:00",
|
||||
)
|
||||
assert result.loc[0, "time_done"] == pd.Timestamp("2024-01-02T00:00:00+00:00")
|
||||
assert result.loc[0, "time_done_msc"] == pd.Timestamp(
|
||||
"2024-01-02T00:00:00+00:00",
|
||||
)
|
||||
|
||||
|
||||
def test_time_columns_include_optional_order_fields() -> None:
|
||||
"""Schema contracts document optional MT5 time columns per dataset kind."""
|
||||
assert "time_done" in TIME_COLUMNS[DataKind.orders]
|
||||
assert "time_setup_msc" in TIME_COLUMNS[DataKind.history_orders]
|
||||
|
||||
|
||||
def test_normalize_dataframe_sorts_ticks_by_time_msc(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Tick frames without ``time`` can still sort on ``time_msc``."""
|
||||
mocker.patch("mt5cli.schemas.validate_schema")
|
||||
ticks = pd.concat([_sample_frame(DataKind.ticks)] * 2, ignore_index=True).drop(
|
||||
columns=["time"],
|
||||
)
|
||||
ticks.loc[0, "time_msc"] = datetime(2024, 1, 1, tzinfo=UTC)
|
||||
ticks.loc[1, "time_msc"] = datetime(2024, 1, 2, tzinfo=UTC)
|
||||
ticks = pd.concat([ticks.iloc[[1]], ticks.iloc[[0]]], ignore_index=True)
|
||||
normalized = normalize_dataframe(ticks, DataKind.ticks, sort=True)
|
||||
assert normalized.iloc[0]["time_msc"] <= normalized.iloc[1]["time_msc"]
|
||||
|
||||
|
||||
def test_ensure_utc_columns_skips_missing_columns() -> None:
|
||||
"""UTC column coercion ignores absent columns."""
|
||||
frame = _sample_frame(DataKind.rates)
|
||||
result = ensure_utc_columns(frame, ["time", "missing"])
|
||||
assert "time" in result.columns
|
||||
|
||||
|
||||
def test_ensure_utc_columns_coerces_non_mt5_columns() -> None:
|
||||
"""Non-MT5 columns still coerce to UTC datetimes."""
|
||||
frame = pd.DataFrame({"created_at": ["2024-01-01T00:00:00+00:00"]})
|
||||
result = ensure_utc_columns(frame, ["created_at"])
|
||||
assert result.loc[0, "created_at"] == pd.Timestamp("2024-01-01T00:00:00+00:00")
|
||||
|
||||
|
||||
def test_mt5_session_yields_connected_client(mocker: MockerFixture) -> None:
|
||||
"""Public mt5_session yields an MT5Client bound to a connected session."""
|
||||
connected = mocker.MagicMock()
|
||||
context = mocker.MagicMock()
|
||||
context.__enter__.return_value = connected
|
||||
context.__exit__.return_value = False
|
||||
mocker.patch("mt5cli.client.connected_client", return_value=context)
|
||||
with mt5_session(build_config()) as client:
|
||||
assert isinstance(client, MT5Client)
|
||||
|
||||
|
||||
def test_retry_with_backoff_reraises_non_recoverable_errors() -> None:
|
||||
"""Non-MT5 errors are not retried."""
|
||||
|
||||
def _raise() -> None:
|
||||
message = "fatal"
|
||||
raise ValueError(message)
|
||||
|
||||
with pytest.raises(ValueError, match="fatal"):
|
||||
retry_with_backoff(_raise, retry_count=2)
|
||||
|
||||
|
||||
def test_storage_export_round_trip_sqlite(tmp_path: Path) -> None:
|
||||
"""Storage helpers append deduplicated frames to SQLite."""
|
||||
frame = normalize_dataframe(
|
||||
_sample_frame(DataKind.rates),
|
||||
DataKind.rates,
|
||||
symbol="EURUSD",
|
||||
timeframe="M1",
|
||||
)
|
||||
output = tmp_path / "rates.db"
|
||||
export_dataframe_to_sqlite(
|
||||
frame,
|
||||
output,
|
||||
"rates",
|
||||
deduplicate_on=DEDUP_KEYS[DataKind.rates][0],
|
||||
)
|
||||
with __import__("sqlite3").connect(output) as conn:
|
||||
count = conn.execute("SELECT COUNT(*) FROM rates").fetchone()[0]
|
||||
assert count == 1
|
||||
|
||||
|
||||
class TestStableSdkContract:
|
||||
"""Tests for the documented stable downstream SDK contract."""
|
||||
|
||||
def test_stable_exports_are_subset_of_all(self) -> None:
|
||||
"""Every stable export is also listed in the package __all__."""
|
||||
missing = sorted(STABLE_SDK_EXPORTS - set(mt5cli.__all__))
|
||||
assert not missing, f"STABLE_SDK_EXPORTS missing from __all__: {missing}"
|
||||
|
||||
def test_public_export_tiers_are_disjoint_and_complete(self) -> None:
|
||||
"""Documented public tiers do not overlap and classify root exports."""
|
||||
assert PUBLIC_EXPORT_TIERS == {
|
||||
"stable": STABLE_SDK_EXPORTS,
|
||||
"secondary": SECONDARY_PUBLIC_EXPORTS,
|
||||
}
|
||||
assert not (STABLE_SDK_EXPORTS & SECONDARY_PUBLIC_EXPORTS)
|
||||
tiered_exports = STABLE_SDK_EXPORTS | SECONDARY_PUBLIC_EXPORTS
|
||||
root_exports = set(mt5cli.__all__)
|
||||
|
||||
missing_from_root = sorted(tiered_exports - root_exports)
|
||||
assert not missing_from_root, (
|
||||
f"Tiered exports missing from __all__: {missing_from_root}"
|
||||
)
|
||||
|
||||
tier_metadata_exports = {
|
||||
"PUBLIC_EXPORT_TIERS",
|
||||
"SECONDARY_PUBLIC_EXPORTS",
|
||||
"STABLE_SDK_EXPORTS",
|
||||
}
|
||||
unclassified_root_exports = sorted(
|
||||
root_exports - tiered_exports - tier_metadata_exports,
|
||||
)
|
||||
assert not unclassified_root_exports, (
|
||||
f"Root exports missing from public API tiers: {unclassified_root_exports}"
|
||||
)
|
||||
|
||||
def test_stable_docs_do_not_document_nonstable_exports(self) -> None:
|
||||
"""Stable docs do not promote secondary root exports."""
|
||||
docs_path = Path("docs/api/public-contract.md")
|
||||
docs = docs_path.read_text(encoding="utf-8")
|
||||
stable_section = docs.split("## Stable downstream SDK API", maxsplit=1)[
|
||||
1
|
||||
].split(
|
||||
"## Secondary public exports",
|
||||
maxsplit=1,
|
||||
)[0]
|
||||
documented_symbols = set(
|
||||
re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)`", stable_section)
|
||||
)
|
||||
nonstable_exports = SECONDARY_PUBLIC_EXPORTS
|
||||
|
||||
wrongly_stable = sorted(documented_symbols & nonstable_exports)
|
||||
assert not wrongly_stable, (
|
||||
f"Non-stable exports documented in stable section: {wrongly_stable}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(STABLE_SDK_EXPORTS))
|
||||
def test_stable_exports_are_importable_from_package_root(self, name: str) -> None:
|
||||
"""Stable SDK names resolve through ``from mt5cli import ...``."""
|
||||
assert hasattr(mt5cli, name), f"{name!r} missing from mt5cli package root"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
sorted(SECONDARY_PUBLIC_EXPORTS),
|
||||
)
|
||||
def test_secondary_exports_are_importable(
|
||||
self,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Non-stable public names remain available from the package root."""
|
||||
assert hasattr(mt5cli, name), f"{name!r} missing from mt5cli package root"
|
||||
|
||||
def test_drop_forming_rate_bar_from_package_root(self) -> None:
|
||||
"""Closed-bar trimming is available from the stable package surface."""
|
||||
frame = pd.DataFrame({"time": [1, 2, 3], "close": [1.0, 1.1, 1.2]})
|
||||
closed = drop_forming_rate_bar(frame)
|
||||
assert list(closed["close"]) == [1.0, 1.1]
|
||||
assert len(closed) == 2
|
||||
|
||||
def test_fetch_latest_closed_rates_from_package_root(self) -> None:
|
||||
"""Single-client closed-bar helper drops the forming row."""
|
||||
client = MagicMock()
|
||||
client.latest_rates.return_value = pd.DataFrame(
|
||||
{"time": [1, 2, 3], "close": [1.0, 1.1, 1.2]},
|
||||
)
|
||||
|
||||
result = fetch_latest_closed_rates(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=2,
|
||||
)
|
||||
|
||||
client.latest_rates.assert_called_once_with("EURUSD", "M1", 3, start_pos=0)
|
||||
assert list(result["close"]) == [1.0, 1.1]
|
||||
|
||||
def test_fetch_latest_closed_rates_for_trading_client_from_package_root(
|
||||
self,
|
||||
) -> None:
|
||||
"""Trading-client closed-bar helper is importable from the stable surface."""
|
||||
client = MagicMock()
|
||||
client.fetch_latest_rates_as_df.return_value = pd.DataFrame(
|
||||
{"time": [1, 2, 3], "close": [1.0, 1.1, 1.2]},
|
||||
)
|
||||
|
||||
result = fetch_latest_closed_rates_for_trading_client(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=2,
|
||||
)
|
||||
|
||||
assert list(result["close"]) == [1.0, 1.1]
|
||||
|
||||
def test_normalize_order_volume_from_package_root(self) -> None:
|
||||
"""Volume normalization helper is importable from the stable surface."""
|
||||
result = normalize_order_volume(
|
||||
0.25,
|
||||
volume_min=0.1,
|
||||
volume_max=1.0,
|
||||
volume_step=0.1,
|
||||
)
|
||||
assert abs(result - 0.2) < 1e-9
|
||||
|
||||
def test_calculate_positions_margin_from_package_root(self) -> None:
|
||||
"""Position margin helper is importable from the stable surface."""
|
||||
client = MagicMock()
|
||||
client.mt5.POSITION_TYPE_BUY = 0
|
||||
client.mt5.POSITION_TYPE_SELL = 1
|
||||
client.mt5.ORDER_TYPE_BUY = 10
|
||||
client.mt5.ORDER_TYPE_SELL = 11
|
||||
client.positions_get_as_df.return_value = pd.DataFrame()
|
||||
|
||||
assert calculate_positions_margin(client) == 0
|
||||
|
||||
def test_generic_trading_helpers_from_package_root(self) -> None:
|
||||
"""New generic trading helpers resolve through the stable surface."""
|
||||
price = extract_tick_price({"bid": "1.2"}, "bid")
|
||||
assert price is not None
|
||||
assert abs(price - 1.2) < 1e-9
|
||||
assert callable(calculate_trailing_stop_updates)
|
||||
assert callable(calculate_account_projected_margin_ratio)
|
||||
assert callable(calculate_projected_margin_ratio)
|
||||
assert callable(calculate_symbol_group_margin_ratio)
|
||||
|
||||
def test_resolve_rate_view_name_from_package_root(self, tmp_path: Path) -> None:
|
||||
"""Rate view resolution is importable and honors require_existing."""
|
||||
db_path = tmp_path / "rates.db"
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
"CREATE TABLE rates("
|
||||
" symbol TEXT, timeframe INTEGER, time TEXT, close REAL)",
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO rates(symbol, timeframe, time, close) VALUES (?, ?, ?, ?)",
|
||||
("EURUSD", 1, "2024-01-01T00:00:00+00:00", 1.0),
|
||||
)
|
||||
create_rate_compatibility_views(conn)
|
||||
|
||||
assert resolve_rate_view_name(db_path, "EURUSD", "M1") == "rate_EURUSD__1"
|
||||
missing = tmp_path / "missing.db"
|
||||
with pytest.raises(ValueError, match="SQLite database not found"):
|
||||
resolve_rate_view_name(missing, "EURUSD", "M1", require_existing=True)
|
||||
|
||||
def test_load_rate_data_from_package_root(self, tmp_path: Path) -> None:
|
||||
"""SQLite rate loading normalizes timestamps through the stable API."""
|
||||
db_path = tmp_path / "view.db"
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
'CREATE VIEW "rate_EURUSD__1" AS'
|
||||
" SELECT '2024-01-01T00:00:00+00:00' AS time, 1.1 AS close",
|
||||
)
|
||||
|
||||
frame = load_rate_data(db_path, "rate_EURUSD__1")
|
||||
assert frame.index.name == "time"
|
||||
assert abs(float(frame.iloc[0]["close"]) - 1.1) < 1e-9
|
||||
|
||||
def test_load_rate_series_from_sqlite_requires_managed_views(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Multi-series loading fails clearly when managed views are absent."""
|
||||
db_path = tmp_path / "empty-views.db"
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
"CREATE TABLE rates("
|
||||
" symbol TEXT, timeframe INTEGER, time TEXT, close REAL)",
|
||||
)
|
||||
|
||||
targets = build_rate_targets(["EURUSD"], ["M1"])
|
||||
with pytest.raises(ValueError, match="No rate compatibility view exists"):
|
||||
load_rate_series_from_sqlite(db_path, targets, count=10)
|
||||
|
||||
assert targets == [RateTarget(symbol="EURUSD", timeframe=1)]
|
||||
|
||||
def test_resolve_account_spec_from_package_root(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Account credential resolution uses generic ${ENV_VAR} placeholders."""
|
||||
monkeypatch.setenv("APP_MT5_LOGIN", "555")
|
||||
monkeypatch.setenv("APP_MT5_PASSWORD", "secret")
|
||||
account = AccountSpec(
|
||||
symbols=["EURUSD"],
|
||||
login="${APP_MT5_LOGIN}",
|
||||
password="${APP_MT5_PASSWORD}",
|
||||
server="Broker-Demo",
|
||||
)
|
||||
|
||||
resolved = resolve_account_spec(account, timeout=3000)
|
||||
assert resolved.login == "555"
|
||||
assert resolved.password == "secret" # noqa: S105
|
||||
assert resolved.timeout == 3000
|
||||
|
||||
batch = resolve_account_specs([account], server="Override")
|
||||
assert batch[0].server == "Override"
|
||||
|
||||
def test_mt5_trading_session_lifecycle_from_package_root(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Trading session helper initializes and always shuts down."""
|
||||
mock_client = MagicMock()
|
||||
mocker.patch(
|
||||
"mt5cli.trading.Mt5TradingClient",
|
||||
return_value=mock_client,
|
||||
)
|
||||
|
||||
with mt5_trading_session(login=12345, server="Broker-Demo") as client:
|
||||
assert client is mock_client
|
||||
mock_client.initialize_and_login_mt5.assert_called_once()
|
||||
|
||||
mock_client.shutdown.assert_called_once()
|
||||
|
||||
def test_trading_order_helpers_importable_from_package_root(self) -> None:
|
||||
"""Order planning helpers resolve through the stable package surface."""
|
||||
assert callable(calculate_margin_and_volume)
|
||||
assert callable(ensure_symbol_selected)
|
||||
assert callable(place_market_order)
|
||||
margin_hints = get_type_hints(MarginVolume)
|
||||
limits_hints = get_type_hints(OrderLimits)
|
||||
execution_hints = get_type_hints(OrderExecutionResult)
|
||||
assert margin_hints["buy_volume"] is float
|
||||
assert limits_hints["stop_loss"] == float | None
|
||||
assert execution_hints["status"] == ExecutionStatus
|
||||
|
||||
def test_mt5_trading_session_shuts_down_on_exception(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Trading session helper shuts down even when the body raises."""
|
||||
mock_client = MagicMock()
|
||||
mocker.patch(
|
||||
"mt5cli.trading.Mt5TradingClient",
|
||||
return_value=mock_client,
|
||||
)
|
||||
|
||||
message = "strategy error"
|
||||
with (
|
||||
pytest.raises(RuntimeError, match=message),
|
||||
mt5_trading_session(login=12345, server="Broker-Demo"),
|
||||
):
|
||||
raise RuntimeError(message)
|
||||
|
||||
mock_client.shutdown.assert_called_once()
|
||||
|
||||
def test_fetch_latest_closed_rates_indexed_from_package_root(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Indexed closed-bar helper returns a UTC DatetimeIndex named 'time'."""
|
||||
client = MagicMock()
|
||||
mocker.patch(
|
||||
"mt5cli.trading.fetch_latest_closed_rates_for_trading_client",
|
||||
return_value=pd.DataFrame(
|
||||
{
|
||||
"time": [1704067200, 1704153600, 1704240000],
|
||||
"close": [1.0, 1.1, 1.2],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
result = fetch_latest_closed_rates_indexed(
|
||||
client,
|
||||
symbol="EURUSD",
|
||||
granularity="M1",
|
||||
count=2,
|
||||
)
|
||||
|
||||
assert isinstance(result.index, pd.DatetimeIndex)
|
||||
assert result.index.name == "time"
|
||||
assert result.index.tz is not None
|
||||
assert "time" not in result.columns
|
||||
assert "close" in result.columns
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Packaging metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parquet_extra_declares_pyarrow() -> None:
|
||||
"""Package metadata lists pyarrow under the parquet optional extra."""
|
||||
reqs = requires("mt5cli") or []
|
||||
parquet_reqs = [r for r in reqs if "pyarrow" in r and "parquet" in r]
|
||||
assert parquet_reqs, "pyarrow not found in parquet optional extra"
|
||||
|
||||
|
||||
def test_pyarrow_not_in_core_dependencies() -> None:
|
||||
"""Pyarrow is not a core dependency; it belongs only in the parquet extra."""
|
||||
reqs = requires("mt5cli") or []
|
||||
core_reqs = [r for r in reqs if "extra ==" not in r]
|
||||
assert not any("pyarrow" in r for r in core_reqs), (
|
||||
"pyarrow should not appear in core dependencies"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
-2687
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,492 +0,0 @@
|
||||
"""Tests for mt5cli.utils module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
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_parquet_without_pyarrow(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
sample_df: pd.DataFrame,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that a clear error is raised when pyarrow is not installed."""
|
||||
monkeypatch.setitem(sys.modules, "pyarrow", None)
|
||||
with pytest.raises(ImportError, match="mt5cli\\[parquet\\]"):
|
||||
export_dataframe(sample_df, tmp_path / "out.parquet", "parquet")
|
||||
|
||||
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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user