From 087ecff5d2b07e840b9cb113878208846f188400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Euko?= <50120357+Joaopeuko@users.noreply.github.com> Date: Sat, 26 Apr 2025 21:08:47 +0100 Subject: [PATCH 1/4] refactor: Re-arrange the project to make improvements (#34) ### Description This PR adds a few things: - [x] Documentation page - [x] Pre-commit CI - [ ] Pytest CI - [x] Integration test CI Fixes #29 This is a breaking changes. --- .codespell-ignore | 0 .github/ISSUE_TEMPLATE/build.yaml | 2 +- .github/ISSUE_TEMPLATE/feat.yaml | 2 +- .github/ISSUE_TEMPLATE/perf.yaml | 1 - .github/workflows/deploy-mkdocs.yml | 44 + .../workflows/deploy-semantic-release.yaml | 24 + .github/workflows/deploy.yaml | 3 +- .../test-metatrader5-integration.yml | 76 + .github/workflows/test-pre-commit.yaml | 20 + .gitignore | 2 +- .pre-commit-config.yaml | 85 +- LICENSE | 2 +- README.md | 5 + docs/assets/favicon.svg | 27 + docs/assets/logo-icon.svg | 34 + docs/assets/logo.svg | 41 + docs/contributing.md | 42 + docs/extra.css | 588 +++++ docs/extra.js | 114 + docs/index.md | 22 + example.py | 8 +- examples_of_expert_advisor/__init__.py | 4 + .../example_sockets_connection.py | 99 +- examples_of_expert_advisor/fimathe/README.md | 28 +- .../fimathe/__init__.py | 4 + .../fimathe/eurusd_fimathe.py | 81 +- .../fimathe/win_fimathe.py | 103 +- mkdocs.yaml | 133 ++ mqpy/__init__.py | 5 + mqpy/__main__.py | 2 + mqpy/book.py | 37 +- mqpy/indicator_connector.py | 1400 ++++++++---- mqpy/rates.py | 72 +- mqpy/template.py | 27 +- mqpy/tick.py | 16 +- mqpy/trade.py | 197 +- mqpy/utilities.py | 37 +- poetry.lock | 422 ---- pyproject.toml | 140 +- scripts/__init__.py | 1 + scripts/build_docs.sh | 32 + scripts/gen_ref_pages.py | 73 + setup.py | 11 +- tests/integration/test_mt5_connection.py | 43 + uv.lock | 1979 +++++++++++++++++ 45 files changed, 4840 insertions(+), 1248 deletions(-) create mode 100644 .codespell-ignore create mode 100644 .github/workflows/deploy-mkdocs.yml create mode 100644 .github/workflows/deploy-semantic-release.yaml create mode 100644 .github/workflows/test-metatrader5-integration.yml create mode 100644 .github/workflows/test-pre-commit.yaml create mode 100644 docs/assets/favicon.svg create mode 100644 docs/assets/logo-icon.svg create mode 100644 docs/assets/logo.svg create mode 100644 docs/contributing.md create mode 100644 docs/extra.css create mode 100644 docs/extra.js create mode 100644 docs/index.md create mode 100644 examples_of_expert_advisor/__init__.py create mode 100644 examples_of_expert_advisor/fimathe/__init__.py create mode 100644 mkdocs.yaml delete mode 100644 poetry.lock create mode 100644 scripts/__init__.py create mode 100755 scripts/build_docs.sh create mode 100755 scripts/gen_ref_pages.py create mode 100644 tests/integration/test_mt5_connection.py create mode 100644 uv.lock diff --git a/.codespell-ignore b/.codespell-ignore new file mode 100644 index 0000000..e69de29 diff --git a/.github/ISSUE_TEMPLATE/build.yaml b/.github/ISSUE_TEMPLATE/build.yaml index 4fed7aa..bd41e85 100644 --- a/.github/ISSUE_TEMPLATE/build.yaml +++ b/.github/ISSUE_TEMPLATE/build.yaml @@ -10,7 +10,7 @@ body: label: "Where?" description: "Provide path from the root of the repository." placeholder: "mqpy/" - + - type: textarea id: requested_change attributes: diff --git a/.github/ISSUE_TEMPLATE/feat.yaml b/.github/ISSUE_TEMPLATE/feat.yaml index 4b1c1d1..0372994 100644 --- a/.github/ISSUE_TEMPLATE/feat.yaml +++ b/.github/ISSUE_TEMPLATE/feat.yaml @@ -16,4 +16,4 @@ body: attributes: label: Task Description description: Provide a clear and concise description of what needs to be done. - placeholder: Describe the problem or proposed solution. \ No newline at end of file + placeholder: Describe the problem or proposed solution. diff --git a/.github/ISSUE_TEMPLATE/perf.yaml b/.github/ISSUE_TEMPLATE/perf.yaml index 013bcf8..da7b232 100644 --- a/.github/ISSUE_TEMPLATE/perf.yaml +++ b/.github/ISSUE_TEMPLATE/perf.yaml @@ -17,4 +17,3 @@ body: label: Area for Performance Improvement description: Specify the part of the codebase or system that requires optimization. Explain why it needs improvement and describe the current performance issue as the proposed solution. placeholder: e.g., "The sorting algorithm in module X is slow when handling large datasets." - diff --git a/.github/workflows/deploy-mkdocs.yml b/.github/workflows/deploy-mkdocs.yml new file mode 100644 index 0000000..5d3d060 --- /dev/null +++ b/.github/workflows/deploy-mkdocs.yml @@ -0,0 +1,44 @@ +name: Deploy | Deploy MkDocs + +on: + workflow_dispatch: + push: + branches: ['main'] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: windows-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv and set the python version + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.11" + + - name: Install the project + run: uv sync --locked --group docs + + - name: Build MkDocs + run: mkdocs build --site-dir ./deploy + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './deploy' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/deploy-semantic-release.yaml b/.github/workflows/deploy-semantic-release.yaml new file mode 100644 index 0000000..1082fbf --- /dev/null +++ b/.github/workflows/deploy-semantic-release.yaml @@ -0,0 +1,24 @@ +name: Deploy | Semantic Release + +on: + workflow_dispatch: + +jobs: + release: + runs-on: windows-latest + concurrency: release + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Python Release + uses: python-semantic-release/python-semantic-release@v9.20.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + push: "true" + changelog: "true" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6c2c756..8599b7a 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -2,8 +2,7 @@ name: Publish distributions on: push: - branches: - - '*' + branches: ['main'] tags: - v* diff --git a/.github/workflows/test-metatrader5-integration.yml b/.github/workflows/test-metatrader5-integration.yml new file mode 100644 index 0000000..450c149 --- /dev/null +++ b/.github/workflows/test-metatrader5-integration.yml @@ -0,0 +1,76 @@ +name: Test | Integration Test + +on: [push] + +jobs: + build: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Download MetaTrader5 Installer + shell: pwsh + run: | + $url = "https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5setup.exe" + $output = "$env:GITHUB_WORKSPACE\mt5setup.exe" + Invoke-WebRequest -Uri $url -OutFile $output + Write-Host "Download completed. File size: $((Get-Item $output).Length) bytes" + + - name: Install MetaTrader5 + run: | + $process = Start-Process -FilePath ".\mt5setup.exe" -ArgumentList "/auto", "/portable" -PassThru + $process.WaitForExit(300000) + if (-not $process.HasExited) { + Write-Host "MT5 installer stuck, killing..." + Stop-Process -Id $process.Id -Force + exit 1 + } + shell: pwsh + + - name: Launch MT5 + shell: pwsh + run: | + $mt5Path = Resolve-Path "C:\Program Files\MetaTrader 5\terminal64.exe" + + # Launch with diagnostics + Start-Process $mt5Path -ArgumentList @( + "/portable", + "/headless", + "/config:config", + "/noreport" + ) -NoNewWindow + + # Verify process start + $attempts = 0 + while ($attempts -lt 10) { + if (Get-Process terminal64 -ErrorAction SilentlyContinue) { + Write-Host "MT5 process detected" + break + } + $attempts++ + Start-Sleep 5 + } + + if (-not (Get-Process terminal64 -ErrorAction SilentlyContinue)) { + Get-Content ".\MetaTrader 5\logs\*.log" | Write-Host + throw "MT5 failed to start" + } + + - name: Install MetaTrader5 Python package + run: pip install MetaTrader5 + + - name: Run MT5 Test + env: + MT5_LOGIN: ${{ secrets.MT5_LOGIN }} + MT5_PASSWORD: ${{ secrets.MT5_PASSWORD }} + MT5_SERVER: "MetaQuotes-Demo" + MT5_PATH: "C:\\Program Files\\MetaTrader 5\\terminal64.exe" + run: | + python tests/integration/test_mt5_connection.py diff --git a/.github/workflows/test-pre-commit.yaml b/.github/workflows/test-pre-commit.yaml new file mode 100644 index 0000000..c042d91 --- /dev/null +++ b/.github/workflows/test-pre-commit.yaml @@ -0,0 +1,20 @@ +name: Test | Pre-commit + +on: + pull_request: + push: + branches: ['*'] + +jobs: + pre-commit: + permissions: write-all + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Pre-commit + uses: pre-commit/action@v3.0.1 + with: + extra_args: --all-files diff --git a/.gitignore b/.gitignore index ee79715..85118b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .env -*.pyc \ No newline at end of file +*.pyc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 50001c9..eb46e76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,72 @@ +default_language_version: + python: python3 repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - language_version: python3 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-byte-order-marker + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: destroyed-symlinks + - id: end-of-file-fixer + - id: file-contents-sorter + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + language: python + types: [text] + entry: codespell --ignore-words=.codespell-ignore --check-filenames + exclude: uv.lock -- repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - args: [--safe, --line-length=120] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.1 + hooks: + - id: mypy + name: mypy + pass_filenames: false + args: + [ + --strict-equality, + --disallow-untyped-calls, + --disallow-untyped-defs, + --disallow-incomplete-defs, + --disallow-any-generics, + --check-untyped-defs, + --disallow-untyped-decorators, + --warn-redundant-casts, + --warn-unused-ignores, + --no-warn-no-return, + --warn-unreachable, + ] + additional_dependencies: ["types-requests", "types-PyYAML"] -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [--profile=black] + - repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: python + additional_dependencies: ["pylint"] + types: [python] + args: ["--disable=all", "--enable=missing-docstring,unused-argument"] + exclude: 'test_\.py$' diff --git a/LICENSE b/LICENSE index 6ae122e..4d636c7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Joao Euko +Copyright (c) 2021-2025 Joao Euko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d392b3e..9180e40 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +
+ MQPy Logo +
+ + ![PyPI - Downloads](https://img.shields.io/pypi/dm/mqpy) ![PyPI](https://img.shields.io/pypi/v/mqpy) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/mqpy) diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg new file mode 100644 index 0000000..351a06b --- /dev/null +++ b/docs/assets/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo-icon.svg b/docs/assets/logo-icon.svg new file mode 100644 index 0000000..43fd2df --- /dev/null +++ b/docs/assets/logo-icon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..33cfd7f --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..f48f427 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,42 @@ +# Contributing to MQPy + +Thank you for your interest in contributing to MQPy! There are many ways to contribute, and we appreciate all of them. + +## Development Process + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +## Reporting Issues + +If you've found a bug or have a feature request, please file an issue using one of our issue templates: + +### Issue Templates + +Choose the appropriate template for your issue: + +- **Bug Fix**: [Report a bug ๐Ÿ›](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=fix.yaml&title=fix%3A+) +- **Documentation**: [Request documentation improvements ๐Ÿ“š](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=docs.yaml&title=docs%3A+) +- **Feature Request**: [Suggest a new feature โœจ](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=feat.yaml&title=feat%3A+) +- **Performance Improvement**: [Report performance issues โšก](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=perf.yaml&title=perf%3A+) +- **Test**: [Suggest test improvements ๐Ÿงช](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=test.yaml&title=test%3A+) +- **Build**: [Report build issues ๐Ÿ”จ](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=build.yaml&title=build%3A+) +- **Chore**: [Suggest maintenance tasks ๐Ÿงน](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=chore.yaml&title=chore%3A+) +- **Style**: [Report style issues ๐Ÿ’…](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=style.yaml&title=style%3A+) +- **Refactor**: [Suggest code refactoring ๐Ÿ”„](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=refactor.yaml&title=refactor%3A+) +- **CI**: [Suggest CI improvements โš™๏ธ](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=ci.yaml&title=ci%3A+) + +## Coding Standards + +- Follow PEP 8 style guide +- Write docstrings in Google format +- Include appropriate tests + +## Pull Request Process + +1. Ensure your code meets all tests +2. Update documentation where necessary +3. The PR should work for Python 3.6 and above +4. PRs will be merged once reviewed by maintainers diff --git a/docs/extra.css b/docs/extra.css new file mode 100644 index 0000000..243eafe --- /dev/null +++ b/docs/extra.css @@ -0,0 +1,588 @@ + /* Hide the In [ ]: labels */ + div.prompt, + .jp-InputPrompt, + .input_prompt, + .out_prompt_overlay, + .prompt_overlay { + display: none !important; + } + + .jp-OutputPrompt.jp-OutputArea-prompt { + all: unset !important; + } + + /* For Dark Mode */ + [data-md-color-scheme="astral-dark"] .jp-Cell { + background-color: var(--space); + color: var(--white); + border: none !important; + box-shadow: none !important; + } + + [data-md-color-scheme="astral-dark"] .jp-CodeMirrorEditor { + background-color: var(--galaxy); + color: var(--white); + border: none !important; + box-shadow: none !important; + } + + [data-md-color-scheme="astral-dark"] .jp-InputPrompt { + color: var(--white); + } + + [data-md-color-scheme="astral-dark"] .jp-InputArea { + border: none !important; + } + + [data-md-color-scheme="astral-dark"] .highlight-ipynb { + background-color: var(--space); + color: var(--white); + } + [data-md-color-scheme="astral-dark"] .clipboard-copy { + color: var(--white); + } + + [data-md-color-scheme="astral-dark"] .clipboard-copy-txt { + background-color: var(--comet); + color: var(--white); + } + + [data-md-color-scheme="astral-dark"] .jp-OutputArea pre { + color: var(--white) !important; + } + + [data-md-color-scheme="astral-dark"] .highlight-ipynb .s2 { + color: var(--radiate) !important; + } + + :root { + --black: #0b1a34; + --white: #ffffff; + --radiate: #ffd700; /* Pure gold */ + --flare: #ffcc00; /* Yellow gold */ + --rock: #ffdf00; /* Bright yellow */ + --galaxy: #665500; /* Dark yellow background */ + --space: #806600; /* Medium yellow-brown */ + --comet: #ccaa00; /* Dark yellow */ + --cosmic: #ffdd00; /* Vibrant yellow */ + --sun: #ffea00; /* Bright yellow */ + --electron: #ffbb00; /* Golden yellow */ + --aurora: #ffe600; /* Electric yellow */ + --constellation: #ffd100; /* Deep yellow */ + --neutron: #fff6c3; /* Pale yellow */ + --proton: #ffe066; /* Light yellow */ + --nebula: #ffde59; /* Yellow */ + --supernova: #ffc400; /* Amber yellow */ + --starlight: #fffacd; /* Light yellow */ + --lunar: #fffce3; /* Cream */ + --asteroid: #d4b300; /* Dark gold */ + --crater: #fff0a0; /* Pale gold */ + } + + [data-md-color-scheme="astral-light"] { + --md-default-bg-color--dark: var(--black); + --md-primary-fg-color: var(--galaxy); + --md-typeset-a-color: var(--flare); + --md-accent-fg-color: var(--cosmic); + } + + [data-md-color-scheme="astral-dark"] { + --md-default-bg-color: var(--galaxy); + --md-default-fg-color: var(--white); + --md-default-fg-color--light: var(--white); + --md-default-fg-color--lighter: var(--white); + --md-primary-fg-color: var(--space); + --md-primary-bg-color: var(--white); + --md-accent-fg-color: var(--cosmic); + + --md-typeset-color: var(--white); + --md-typeset-a-color: var(--radiate); + --md-typeset-mark-color: var(--sun); + + --md-code-fg-color: var(--white); + --md-code-bg-color: var(--space); + + --md-code-hl-comment-color: var(--crater); + --md-code-hl-punctuation-color: var(--crater); + --md-code-hl-generic-color: var(--supernova); + --md-code-hl-variable-color: var(--starlight); + --md-code-hl-string-color: var(--radiate); + --md-code-hl-keyword-color: var(--supernova); + --md-code-hl-operator-color: var(--supernova); + --md-code-hl-number-color: var(--electron); + --md-code-hl-special-color: var(--electron); + --md-code-hl-function-color: var(--neutron); + --md-code-hl-constant-color: var(--radiate); + --md-code-hl-name-color: var(--md-code-fg-color); + + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(50, 90%, 44%, 0.15); + + --md-typeset-table-color: hsla(0, 0%, 100%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 100%, 0.035); + } + + [data-md-color-scheme="astral-light"] img[src$="#only-dark"], + [data-md-color-scheme="astral-light"] img[src$="#gh-dark-mode-only"] { + display: none; /* Hide dark images in light mode */ + } + + [data-md-color-scheme="astral-light"] img[src$="#only-light"], + [data-md-color-scheme="astral-light"] img[src$="#gh-light-mode-only"] { + display: inline; /* Show light images in light mode */ + } + + [data-md-color-scheme="astral-dark"] img[src$="#only-light"], + [data-md-color-scheme="astral-dark"] img[src$="#gh-light-mode-only"] { + display: none; /* Hide light images in dark mode */ + } + + [data-md-color-scheme="astral-dark"] img[src$="#only-dark"], + [data-md-color-scheme="astral-dark"] img[src$="#gh-dark-mode-only"] { + display: inline; /* Show dark images in dark mode */ + } + + /* See: https://github.com/squidfunk/mkdocs-material/issues/175#issuecomment-616694465 */ + .md-typeset__table { + min-width: 100%; + } + .md-typeset table:not([class]) { + display: table; + } + + /* See: https://github.com/astral-sh/ruff/issues/8519 */ + [data-md-color-scheme="astral-dark"] details summary a { + color: var(--flare); + } + + /* See: https://github.com/astral-sh/ruff/issues/9046 */ + [data-md-color-scheme="astral-dark"] div.admonition { + color: var(--md-code-fg-color); + background-color: var(--md-code-bg-color); + } + + /* Prevent the shadow from the nav title from blurring the top link. + The box shadow isn't really doing anything anyway. + + This is a consequence of the reduced nav spacing below. */ + .md-nav--primary .md-nav__title { + box-shadow: none; + } + + /* Omits the nav title "uv" entirely unless on a small screen, in which case + the nav title is needed for backwards navigation in the collapsible + nav variant. + + See https://github.com/astral-sh/uv/issues/5130 */ + .md-nav__title { + display: none; + } + @media screen and (max-width: 1219px) { + .md-nav__title { + display: flex; + } + } + + /* Reducing spacing between nav items to fit more content */ + .md-nav__link { + margin-top: 0.25em; + } + .md-nav__item--section { + margin: 0.75em 0; + } + /* Retain larger spacing for each top-level section in the nav */ + .md-nav__item--nested { + margin-top: 1em; + } + /* Retain larger spacing for the right-side table-of-contents nav */ + .md-nav--secondary .md-nav__link { + margin-top: 0.5em; + } + /* See: https://mkdocstrings.github.io/recipes/#prevent-selection-of-prompts-and-output-in-python-code-blocks */ + .highlight .gp, + .highlight .go { + /* Generic.Prompt, Generic.Output */ + user-select: none; + } + + /* Increase the size of the sections headings, remove the bold */ + .md-nav__container > .md-nav__link:first-child { + font-size: 17.5px; + font-weight: normal; + margin-bottom: 0.1em; + } + + /* Increase the size of the index nav item to match the sections */ + .md-nav__item:first-child { + font-size: 17.5px; + font-weight: normal; + } + /* But do not increase the size of other nav items */ + .md-nav__item--section > .md-nav > .md-nav__list > .md-nav__item { + font-size: 15px; + font-weight: normal; + } + + /* Bold the active nav link for accessibility */ + .md-nav__link--active { + font-weight: bold; + } + + /* Styling for the generated CLI reference page */ + .cli-reference dd { + margin-top: 0.1em; + margin-bottom: 0.5em; + } + .cli-reference dd p { + margin-block-start: 0.2em; + margin-block-end: 0.3em; + } + .cli-reference ul { + margin-bottom: 0.1em; + } + h3.cli-reference { + font-size: 1.1em; + margin: 0 0 0 0; + } + + /* Adjust font size and weight for better readability */ + .md-nav__title, + .md-nav__link { + font-size: 14px; + font-weight: 500; + } + + /* Improve indentation and spacing */ + .md-nav__list { + padding-left: 10px; + } + + .md-nav__item { + margin-bottom: 2px; + } + + /* Adjust the styling for nested elements */ + .md-nav__item .md-nav__list { + padding-left: 15px; + font-size: 13px; + } + + /* Improve hover effect */ + .md-nav__link:hover { + color: #007bff; /* Adjust the color as needed */ + font-weight: 600; + } + + /* Fix inconsistent line height */ + .md-nav__list, + .md-nav__item { + line-height: 1.5; + } + + /* Adjust spacing between menu items */ + .md-nav__list { + margin-bottom: 5px; + } + + /* Improve property display */ + .doc-property { + margin-bottom: 1.5rem; + border-left: 3px solid var(--md-primary-fg-color); + padding-left: 1rem; + background-color: rgba(0, 0, 0, 0.03); + padding: 0.5rem 1rem; + border-radius: 0.2rem; + } + + .doc-property .doc-label { + font-size: 1.1rem; + font-weight: 600; + color: var(--md-primary-fg-color); + margin-bottom: 0.5rem; + font-family: var(--md-code-font-family); + } + + .doc-property .doc-object-name { + font-weight: normal; + font-size: 1rem; + margin-left: 0.5rem; + } + + /* Improve hierarchy with indentation */ + .doc-class { + margin-left: 0; + } + + .doc-class .doc-contents { + margin-left: 1rem; + border-left: 1px solid rgba(0, 0, 0, 0.1); + padding-left: 1rem; + } + + .doc-method, .doc-function, .doc-attribute, .doc-property { + margin-left: 1rem; + } + + /* Improve headings */ + .doc h1.doc-heading { + font-size: 2rem; + margin-bottom: 1.5rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + + .doc h2.doc-heading { + font-size: 1.5rem; + margin-top: 2rem; + margin-bottom: 1rem; + padding-bottom: 0.2rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + } + + .doc h3.doc-heading, .doc h4.doc-heading { + font-size: 1.2rem; + margin-top: 1.5rem; + margin-bottom: 0.75rem; + } + + /* Property value styling */ + .doc-property .doc p { + margin: 0.5rem 0; + } + + /* Hide repetitive property text */ + .doc-property .doc-label .doc-annotation { + font-size: 0.8rem; + opacity: 0.6; + font-weight: normal; + } + + /* Better display for copy buttons */ + .doc-contents .doc-function .md-clipboard, + .doc-contents .doc-method .md-clipboard, + .doc-contents .doc-class .md-clipboard, + .doc-contents .doc-property .md-clipboard { + color: rgba(0, 0, 0, 0.3); + } + + .doc-contents .doc-function .md-clipboard:hover, + .doc-contents .doc-method .md-clipboard:hover, + .doc-contents .doc-class .md-clipboard:hover, + .doc-contents .doc-property .md-clipboard:hover { + color: var(--md-primary-fg-color); + } + + /* Hide module h1 headers */ + .doc-module > h1.doc-heading { + display: none !important; + } + + /* Hide all first h1 headers - typically module names */ + h1:first-of-type { + display: none !important; + } + body > div.md-container > main > div > div.md-sidebar.md-sidebar--secondary > div > div > nav > ul > li:nth-child(1) > a > span { + display: none !important; + } + + /* Custom styles for header and footer */ + .md-header { + background-color: var(--rock); + color: var(--black); + } + + .md-footer { + background-color: var(--cosmic); + color: var(--black); + } + + .md-footer-meta { + background-color: var(--flare); + color: var(--black); + } + + /* Adjust the nav tabs in header to be more visible */ + .md-tabs { + background-color: var(--electron); + } + + .md-tabs__link { + color: var(--black); + opacity: 0.9; + } + + .md-tabs__link--active, + .md-tabs__link:hover { + color: var(--space); + font-weight: bold; + } + + /* Adjust nav text for better visibility on yellow */ + .md-header-nav__title, + .md-header-nav__button, + .md-footer-nav__link, + .md-footer-meta__inner { + color: var(--black); + } + + /* Set favicon link */ + [data-md-color-scheme="astral-light"] { + --md-favicon: url('assets/favicon.svg'); + } + + [data-md-color-scheme="astral-dark"] { + --md-favicon: url('assets/favicon.svg'); + } + + /* Logo in header */ + .md-header-nav__button.md-logo img, + .md-header-nav__button.md-logo svg { + width: 40px; + height: 40px; + } + + @media screen and (max-width: 76.1875em) { + .repo-stats { + display: none !important; + } + } + + /* Sponsor popup styling */ + .sponsor-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s, visibility 0.3s; + } + + .sponsor-popup.active { + opacity: 1; + visibility: visible; + } + + .sponsor-popup-content { + background-color: #fff; + border-radius: 8px; + max-width: 500px; + width: 90%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + overflow: hidden; + } + + [data-md-color-scheme="astral-dark"] .sponsor-popup-content { + background-color: var(--galaxy); + color: var(--white); + } + + .sponsor-popup-header { + padding: 16px 20px; + background-color: var(--rock); + color: var(--black); + display: flex; + justify-content: space-between; + align-items: center; + } + + .sponsor-popup-header h2 { + margin: 0; + font-size: 1.3rem; + font-weight: 600; + } + + .sponsor-popup-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: var(--black); + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + transition: background-color 0.2s; + } + + .sponsor-popup-close:hover { + background-color: rgba(0, 0, 0, 0.1); + } + + .sponsor-popup-body { + padding: 20px; + } + + .sponsor-popup-buttons { + display: flex; + gap: 12px; + margin: 20px 0; + } + + .sponsor-popup-button { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 4px; + text-decoration: none; + font-weight: 600; + background-color: #f0f0f0; + color: #333; + transition: background-color 0.2s; + } + + .sponsor-popup-button.primary { + background-color: var(--rock); + color: var(--black); + } + + .sponsor-popup-button:hover { + opacity: 0.9; + } + + .sponsor-popup-footer { + margin-top: 16px; + display: flex; + justify-content: flex-end; + font-size: 0.9rem; + } + + .sponsor-popup-footer label { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + } + + /* Sponsor admonition styling */ + .md-typeset .admonition.sponsor, + .md-typeset details.sponsor { + border-left-color: var(--rock); + } + + .md-typeset .sponsor > .admonition-title, + .md-typeset .sponsor > summary { + background-color: rgba(255, 215, 0, 0.1); + } + + .md-typeset .sponsor > .admonition-title::before, + .md-typeset .sponsor > summary::before { + background-color: var(--rock); + -webkit-mask-image: var(--md-admonition-icon--note); + mask-image: var(--md-admonition-icon--note); + } + + /* Sponsor button styling */ + .md-typeset .md-button { + margin-top: 0.5em; + margin-right: 0.5em; + } diff --git a/docs/extra.js b/docs/extra.js new file mode 100644 index 0000000..a61bdb4 --- /dev/null +++ b/docs/extra.js @@ -0,0 +1,114 @@ +function cleanupClipboardText(targetSelector) { + const targetElement = document.querySelector(targetSelector); + + // exclude "Generic Prompt" and "Generic Output" spans from copy + const excludedClasses = ["gp", "go"]; + + const clipboardText = Array.from(targetElement.childNodes) + .filter( + (node) => + !excludedClasses.some((className) => + node?.classList?.contains(className), + ), + ) + .map((node) => node.textContent) + .filter((s) => s != ""); + return clipboardText.join("").trim(); + } + + // Sets copy text to attributes lazily using an Intersection Observer. + function setCopyText() { + // The `data-clipboard-text` attribute allows for customized content in the copy + // See: https://www.npmjs.com/package/clipboard#copy-text-from-attribute + const attr = "clipboardText"; + // all "copy" buttons whose target selector is a element + const elements = document.querySelectorAll( + 'button[data-clipboard-target$="code"]', + ); + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + // target in the viewport that have not been patched + if ( + entry.intersectionRatio > 0 && + entry.target.dataset[attr] === undefined + ) { + entry.target.dataset[attr] = cleanupClipboardText( + entry.target.dataset.clipboardTarget, + ); + } + }); + }); + + elements.forEach((elt) => { + observer.observe(elt); + }); + } + + // Show sponsor popup on first visit + function showSponsorPopup() { + // Check if user has seen the popup before + if (!localStorage.getItem('mqpy_sponsor_popup_shown')) { + // Create popup container + const popup = document.createElement('div'); + popup.className = 'sponsor-popup'; + + // Create popup content + popup.innerHTML = ` + + `; + + // Add popup to body + document.body.appendChild(popup); + + // Show popup with animation + setTimeout(() => { + popup.classList.add('active'); + }, 1000); + + // Close button event + const closeBtn = popup.querySelector('.sponsor-popup-close'); + closeBtn.addEventListener('click', () => { + popup.classList.remove('active'); + + // Check if "don't show again" is checked + const dontShowAgain = document.getElementById('sponsor-popup-dont-show').checked; + if (dontShowAgain) { + localStorage.setItem('mqpy_sponsor_popup_shown', 'true'); + } + + // Remove popup after animation + setTimeout(() => { + popup.remove(); + }, 300); + }); + } + } + + // Using the document$ observable is particularly important if you are using instant loading since + // it will not result in a page refresh in the browser + // See `How to integrate with third-party JavaScript libraries` guideline: + // https://squidfunk.github.io/mkdocs-material/customization/?h=javascript#additional-javascript + document$.subscribe(function () { + setCopyText(); + showSponsorPopup(); + }); diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9d287a9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,22 @@ +
+ MQPy Logo +
+ +!!! sponsor "Support MQPy Development" + MQPy is a free and open-source project that needs your support to continue development! + + [๐Ÿ’› Become a Sponsor](https://github.com/sponsors/Joaopeuko){ .md-button .md-button--primary } + +# MQPy + +MQPy is a Python library designed to simplify the process of creating Expert Advisors for MetaTrader 5. While developing directly in MQL5 can be complex, MQPy provides a more streamlined experience using Python. + +## Need Help or Found an Issue? + +If you need help or have found an issue, you can: + +- [Report a bug ๐Ÿ›](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=fix.yaml&title=fix%3A+) +- [Request documentation improvements ๐Ÿ“š](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=docs.yaml&title=docs%3A+) +- [Suggest a new feature โœจ](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=feat.yaml&title=feat%3A+) + +See the [Contributing](contributing.md) page for all available issue templates and more ways to contribute. diff --git a/example.py b/example.py index 09ad5fc..cee288a 100644 --- a/example.py +++ b/example.py @@ -1,3 +1,8 @@ +"""Example trading strategy using the MQL5-Python integration. + +This example demonstrates a Moving Average Crossover strategy. +""" + from mqpy.src.rates import Rates from mqpy.src.tick import Tick from mqpy.src.trade import Trade @@ -48,6 +53,3 @@ if trade.days_end(): trade.close_position("End of the trading day reached.") break - -print("Finishing the program.") -print("Program finished.") diff --git a/examples_of_expert_advisor/__init__.py b/examples_of_expert_advisor/__init__.py new file mode 100644 index 0000000..8f362cf --- /dev/null +++ b/examples_of_expert_advisor/__init__.py @@ -0,0 +1,4 @@ +"""Expert Advisor examples for MetaTrader 5. + +This package contains example implementations of various trading strategies. +""" diff --git a/examples_of_expert_advisor/example_sockets_connection.py b/examples_of_expert_advisor/example_sockets_connection.py index 620f0a7..441a460 100644 --- a/examples_of_expert_advisor/example_sockets_connection.py +++ b/examples_of_expert_advisor/example_sockets_connection.py @@ -1,31 +1,42 @@ -from include.trade import Trade -from include.tick import Tick -from include.rates import Rates -from include.indicator_connector import Indicator +"""Example Expert Advisor demonstrating socket connections with MetaTrader 5. + +This example uses stochastic oscillator and moving average indicators to generate trading signals. +""" + +import logging + import MetaTrader5 as Mt5 +from include.indicator_connector import Indicator +from include.rates import Rates +from include.tick import Tick +from include.trade import Trade + +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) # You need this MQL5 service to use indicator: # https://www.mql5.com/en/market/product/57574 indicator = Indicator() -trade = Trade('Example', # Expert name - 0.1, # Expert Version - 'PETR4', # symbol - 567, # Magic number - 100.0, # lot - 10, # stop loss - 10 cents - 30, # emergency stop loss - 30 cents - 10, # take profit - 10 cents - 30, # emergency take profit - 30 cents - '9:15', # It is allowed to trade after that hour. Do not use zeros, like: 09 - '17:30', # It is not allowed to trade after that hour but let open all the position already opened. - '17:50', # It closes all the position opened. Do not use zeros, like: 09 - 0.5, # average fee - ) +trade = Trade( + "Example", # Expert name + 0.1, # Expert Version + "PETR4", # symbol + 567, # Magic number + 100.0, # lot + 10, # stop loss - 10 cents + 30, # emergency stop loss - 30 cents + 10, # take profit - 10 cents + 30, # emergency take profit - 30 cents + "9:15", # It is allowed to trade after that hour. Do not use zeros, like: 09 + "17:30", # It is not allowed to trade after that hour but let open all the position already opened. + "17:50", # It closes all the position opened. Do not use zeros, like: 09 + 0.5, # average fee +) time = 0 while True: - # You need this MQL5 service to use indicator: # https://www.mql5.com/en/market/product/57574 @@ -40,18 +51,12 @@ # It uses "try" and catch because sometimes it returns None. try: + # When in doubt how to handle the indicator, it returns a Dictionary. + k_now = stochastic_now["k_result"] + d_now = stochastic_now["d_result"] - # When in doubt how to handle the indicator, print it, it returns a Dictionary. - # print(moving_average) - # It prints: - # {'symbol': 'PETR4', 'time_frame': 1, 'period': 50, 'start_position': 0, 'method': 0, - # 'applied_price': 0, 'moving_average_result': 23.103} - - k_now = stochastic_now['k_result'] - d_now = stochastic_now['d_result'] - - k_past3 = stochastic_now['k_result'] - d_past3 = stochastic_now['d_result'] + k_past3 = stochastic_now["k_result"] + d_past3 = stochastic_now["d_result"] if tick.time_msc != time: # It is trading of the time frame of one minute. @@ -70,21 +75,11 @@ # It is the buy logic. buy = ( - # Stochastic - ( - k_now > d_now - and - k_past3 < d_past3 - ) - + (k_now > d_now and k_past3 < d_past3) and - # Moving Average - ( - tick.last > moving_average['moving_average_result'] - ) - + (tick.last > moving_average["moving_average_result"]) ) # End of buy logic. # -------------------------------------------------------------------- # @@ -92,24 +87,16 @@ # It is the sell logic. sell = ( # Stochastic - ( - k_now < d_now - and - k_past3 > d_past3 - ) - + (k_now < d_now and k_past3 > d_past3) and - # Moving Average - ( - tick.last < moving_average['moving_average_result'] - ) + (tick.last < moving_average["moving_average_result"]) ) # End of sell logic. # -------------------------------------------------------------------- # # When buy or sell are true, it open a position. - trade.open_position(buy, sell, 'Example Advisor Comment, the comment here can be seen in MetaTrader5') + trade.open_position(buy, sell, "Example Advisor Comment, the comment here can be seen in MetaTrader5") except TypeError: pass @@ -117,8 +104,8 @@ time = tick.time_msc if trade.days_end(): - trade.close_position('End of the trading day reached.') + trade.close_position("End of the trading day reached.") break -print('Finishing the program.') -print('Program finished.') +logger.info("Finishing the program.") +logger.info("Program finished.") diff --git a/examples_of_expert_advisor/fimathe/README.md b/examples_of_expert_advisor/fimathe/README.md index 4a996bc..1068aaa 100644 --- a/examples_of_expert_advisor/fimathe/README.md +++ b/examples_of_expert_advisor/fimathe/README.md @@ -4,7 +4,7 @@ This is a simple version that I made for Fimathe strategy to be used as a base for your strategy and your improvements. -This strategy does not cover price reversion. +This strategy does not cover price reversion. It covers only continuity. # Table of contents: @@ -16,9 +16,9 @@ It covers only continuity. - [EURUSD](#eurusd) - [B3 - Brazilian Stock Exchange:](#b3---brazilian-stock-exchange) - [WIN](#win) - - + + ## Strategy: @@ -36,7 +36,7 @@ The period starts after the space_to_trade. (5 + 15) The trading zones are calculated with the difference from high and low price inside of period, in this example -the period is 15 after the freedom movement. +the period is 15 after the freedom movement. The difference is multiplied by their percentage amount desired to find the zone. ```python @@ -66,16 +66,16 @@ The strategy looks for the minimum and maximum values and identifies their array With the array position, it is possible to identify the slop direction. -Example 1: -Low position = 8 -High position = 10 -8 - 10 = -2, +Example 1: +Low position = 8 +High position = 10 +8 - 10 = -2, which means bull trend, because the high price are farther than the low price. -Example 2: -Low position = 12 -High position = 7 -12 - 7 = 5, +Example 2: +Low position = 12 +High position = 7 +12 - 7 = 5, which meas bear trend, because the low price are farther than the high price. For open a BUY position the strategy waits the price goes 38.2% above the highest price in 15 period. @@ -90,7 +90,7 @@ lowest price in 15 periods. sell = tick.last < np.amin(rates.low) - zone_382 and \ util.minutes_counter_after_trade(trade.symbol, delay_after_trade) ``` -Also, for buy and sell, it checks if some operation recently happened. +Also, for buy and sell, it checks if some operation recently happened. When a recent operation has happened, it will wait for the number of minutes to return True to trade again. ```python util.minutes_counter_after_trade(trade.symbol, delay_after_trade) @@ -109,7 +109,7 @@ trade.take_profit = zone_618 ``` Also, the stop is moved when the price goes to right direction, when the price moved more than 23.6% for the right -direction, the stop is moved to the nearest price to zero. +direction, the stop is moved to the nearest price to zero. ```python if len(Mt5.positions_get(symbol=trade.symbol)) == 1: diff --git a/examples_of_expert_advisor/fimathe/__init__.py b/examples_of_expert_advisor/fimathe/__init__.py new file mode 100644 index 0000000..d750c8e --- /dev/null +++ b/examples_of_expert_advisor/fimathe/__init__.py @@ -0,0 +1,4 @@ +"""FiMathe strategy examples for MetaTrader 5. + +This package contains examples of Expert Advisors using Fibonacci retracement levels. +""" diff --git a/examples_of_expert_advisor/fimathe/eurusd_fimathe.py b/examples_of_expert_advisor/fimathe/eurusd_fimathe.py index d722570..26519c8 100644 --- a/examples_of_expert_advisor/fimathe/eurusd_fimathe.py +++ b/examples_of_expert_advisor/fimathe/eurusd_fimathe.py @@ -1,26 +1,32 @@ -import numpy as np +"""EURUSD FiMathe Expert Advisor for MetaTrader 5. + +This Expert Advisor uses Fibonacci retracement levels to determine entry and exit points. +""" + import MetaTrader5 as Mt5 -from include.trade import Trade -from include.tick import Tick +import numpy as np from include.rates import Rates +from include.tick import Tick +from include.trade import Trade from include.utilities import Utilities util = Utilities() -trade = Trade('Example', # Expert name - 0.1, # Expert Version - 'EURUSD', # symbol - 567, # Magic number - 0.01, # lot, it is a floating point. - 25, # stop loss - 300, # emergency stop loss - 25, # take profit - 300, # emergency take profit - '00:10', # It is allowed to trade after that hour. Do not use zeros, like: 09 - '23:50', # It is not allowed to trade after that hour but let open all the position already opened. - '23:50', # It closes all the position opened. Do not use zeros, like: 09 - 0.0, # average fee - ) +trade = Trade( + "Example", # Expert name + 0.1, # Expert Version + "EURUSD", # symbol + 567, # Magic number + 0.01, # lot, it is a floating point. + 25, # stop loss + 300, # emergency stop loss + 25, # take profit + 300, # emergency take profit + "00:10", # It is allowed to trade after that hour. Do not use zeros, like: 09 + "23:50", # It is not allowed to trade after that hour but let open all the position already opened. + "23:50", # It closes all the position opened. Do not use zeros, like: 09 + 0.0, # average fee +) buy = False sell = False @@ -31,14 +37,12 @@ time = 0 while True: - tick = Tick(trade.symbol) rates = Rates(trade.symbol, Mt5.TIMEFRAME_M1, space_to_trade, period) util.minutes_counter_after_trade(trade.symbol, delay_after_trade) if tick.time_msc != time: - # Zones: zone_236 = round(((np.amax(rates.high) - np.amin(rates.low)) * 23.6) * 1000) # 23.60% @@ -50,35 +54,35 @@ # Bull trend: if (np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0]) < 0: - # Buy - buy = tick.ask > np.amax(rates.high) + (zone_382 / 100000) and \ - util.minutes_counter_after_trade(trade.symbol, delay_after_trade) + buy = tick.ask > np.amax(rates.high) + (zone_382 / 100000) and util.minutes_counter_after_trade( + trade.symbol, delay_after_trade + ) if buy: - trade.stop_loss = zone_236 trade.take_profit = zone_618 # Bear trend: if (np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0]) > 0: - # Sell - sell = tick.bid < np.amin(rates.low) - (zone_382 / 100000) and \ - util.minutes_counter_after_trade(trade.symbol, delay_after_trade) + sell = tick.bid < np.amin(rates.low) - (zone_382 / 100000) and util.minutes_counter_after_trade( + trade.symbol, delay_after_trade + ) if sell: - trade.stop_loss = zone_236 trade.take_profit = zone_618 - if len(Mt5.positions_get(symbol=trade.symbol)) == 1: - - if Mt5.positions_get(symbol=trade.symbol)[0].type == 0: # if Buy - if tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236: - trade.stop_loss = trade.sl_tp_steps - - elif Mt5.positions_get(symbol=trade.symbol)[0].type == 1: # if Sell - if tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236: - trade.stop_loss = trade.sl_tp_steps + if len(Mt5.positions_get(symbol=trade.symbol)) == 1 and ( + ( + Mt5.positions_get(symbol=trade.symbol)[0].type == 0 + and tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236 + ) + or ( + Mt5.positions_get(symbol=trade.symbol)[0].type == 1 + and tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236 + ) + ): + trade.stop_loss = trade.sl_tp_steps trade.emergency_stop_loss = trade.stop_loss + zone_236 trade.emergency_take_profit = trade.take_profit + zone_236 @@ -87,8 +91,5 @@ time = tick.time_msc if trade.days_end(): - trade.close_position('End of the trading day reached.') + trade.close_position("End of the trading day reached.") break - -print('Finishing the program.') -print('Program finished.') diff --git a/examples_of_expert_advisor/fimathe/win_fimathe.py b/examples_of_expert_advisor/fimathe/win_fimathe.py index 9f5d44a..569a96d 100644 --- a/examples_of_expert_advisor/fimathe/win_fimathe.py +++ b/examples_of_expert_advisor/fimathe/win_fimathe.py @@ -1,27 +1,33 @@ -import numpy as np +"""WIN FiMathe Expert Advisor for MetaTrader 5. + +This Expert Advisor is designed for WING21 futures, using Fibonacci retracement levels to +determine entry and exit points. +""" + import MetaTrader5 as Mt5 -from include.trade import Trade -from include.tick import Tick +import numpy as np from include.rates import Rates +from include.tick import Tick +from include.trade import Trade from include.utilities import Utilities - util = Utilities() -trade = Trade('Example', # Expert name - 0.1, # Expert Version - 'WING21', # symbol - 567, # Magic number - 1.0, # lot, it is a floating point. - 25, # stop loss - 300, # emergency stop loss - 25, # take profit - 300, # emergency take profit - '9:25', # It is allowed to trade after that hour. Do not use zeros, like: 09 - '17:45', # It is not allowed to trade after that hour but let open all the position already opened. - '17:50', # It closes all the position opened. Do not use zeros, like: 09 - 0.0, # average fee - ) +trade = Trade( + "Example", # Expert name + 0.1, # Expert Version + "WING21", # symbol + 567, # Magic number + 1.0, # lot, it is a floating point. + 25, # stop loss + 300, # emergency stop loss + 25, # take profit + 300, # emergency take profit + "9:25", # It is allowed to trade after that hour. Do not use zeros, like: 09 + "17:45", # It is not allowed to trade after that hour but let open all the position already opened. + "17:50", # It closes all the position opened. Do not use zeros, like: 09 + 0.0, # average fee +) buy = False sell = False @@ -32,33 +38,35 @@ time = 0 while True: - tick = Tick(trade.symbol) rates = Rates(trade.symbol, Mt5.TIMEFRAME_M1, space_to_trade, period) if tick.time_msc != time: - util.minutes_counter_after_trade(trade.symbol, delay_after_trade) # Zones: - zone_236 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - - np.amin(rates.low)) * 0.236) / int(trade.sl_tp_steps)) # 23.60% + zone_236 = int(trade.sl_tp_steps) * round( + ((np.amax(rates.high) - np.amin(rates.low)) * 0.236) / int(trade.sl_tp_steps) + ) # 23.60% - zone_382 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - - np.amin(rates.low)) * 0.381) / int(trade.sl_tp_steps)) # 38.20% + zone_382 = int(trade.sl_tp_steps) * round( + ((np.amax(rates.high) - np.amin(rates.low)) * 0.381) / int(trade.sl_tp_steps) + ) # 38.20% - zone_500 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - - np.amin(rates.low)) * 0.500) / int(trade.sl_tp_steps)) # 50.00% + zone_500 = int(trade.sl_tp_steps) * round( + ((np.amax(rates.high) - np.amin(rates.low)) * 0.500) / int(trade.sl_tp_steps) + ) # 50.00% - zone_618 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - - np.amin(rates.low)) * 0.618) / int(trade.sl_tp_steps)) # 61.80% + zone_618 = int(trade.sl_tp_steps) * round( + ((np.amax(rates.high) - np.amin(rates.low)) * 0.618) / int(trade.sl_tp_steps) + ) # 61.80% # Bull trend: if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] < 0: - # Buy - buy = tick.last > np.amax(rates.high) + zone_382 and \ - util.minutes_counter_after_trade(trade.symbol, delay_after_trade) + buy = tick.last > np.amax(rates.high) + zone_382 and util.minutes_counter_after_trade( + trade.symbol, delay_after_trade + ) if buy: trade.stop_loss = zone_382 @@ -66,33 +74,32 @@ # Bear trend: if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] > 0: - # Sell - sell = tick.last < np.amin(rates.low) - zone_382 and \ - util.minutes_counter_after_trade(trade.symbol, delay_after_trade) + sell = tick.last < np.amin(rates.low) - zone_382 and util.minutes_counter_after_trade( + trade.symbol, delay_after_trade + ) if sell: trade.stop_loss = zone_382 trade.take_profit = zone_618 - if len(Mt5.positions_get(symbol=trade.symbol)) == 1: - - if Mt5.positions_get(symbol=trade.symbol)[0].type == 0: # if Buy - if tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236: - trade.stop_loss = trade.sl_tp_steps - - elif Mt5.positions_get(symbol=trade.symbol)[0].type == 1: # if Sell - if tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236: - trade.stop_loss = trade.sl_tp_steps + if len(Mt5.positions_get(symbol=trade.symbol)) == 1 and ( + ( + Mt5.positions_get(symbol=trade.symbol)[0].type == 0 + and tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236 + ) + or ( + Mt5.positions_get(symbol=trade.symbol)[0].type == 1 + and tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236 + ) + ): + trade.stop_loss = trade.sl_tp_steps trade.emergency_stop_loss = trade.stop_loss trade.emergency_take_profit = trade.take_profit - trade.open_position(buy, sell, '') + trade.open_position(buy, sell, "") time = tick.time_msc if trade.days_end(): - trade.close_position('End of the trading day reached.') + trade.close_position("End of the trading day reached.") break - -print('Finishing the program.') -print('Program finished.') diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 0000000..7c72efa --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,133 @@ +site_name: MQPy +repo_url: https://github.com/Joaopeuko/Mql5-Python-Integration +repo_name: MQPy + +# Enable GitHub repository statistics +edit_uri: "" # Disable edit button by setting to empty string +# Settings for GitHub stars and forks display +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Joaopeuko/Mql5-Python-Integration + name: GitHub + # analytics: + # provider: google + # property: !ENV GOOGLE_ANALYTICS_KEY + status: + new: Recently added + deprecated: Deprecated +markdown_extensions: + - pymdownx.snippets + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + - admonition + - pymdownx.details + - toc: + permalink: "#" + - pymdownx.magiclink: + repo_url_shorthand: true + user: Joaopeuko + repo: Mql5-Python-Integration + - attr_list: + - md_in_html: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite: + - markdown.extensions.attr_list: + - pymdownx.keys: + - pymdownx.tabbed: + alternate_style: true + +theme: + name: material + favicon: assets/favicon.svg + logo: assets/logo.svg + features: + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.sections + - navigation.indexes + - navigation.tracking + - content.code.annotate + - toc.follow + - navigation.footer + - navigation.top + - content.code.copy + - content.tabs.link + - content.action.edit + - content.action.view + - content.tooltips + # Enable GitHub repository statistics + - content.action.edit + - content.action.view + - navigation.footer + # Shows the GitHub icon with repository stats + - header.autohide + icon: + repo: fontawesome/brands/github + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: astral-light + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: astral-dark + toggle: + icon: material/brightness-4 + name: Switch to system preference +extra_css: + - extra.css +extra_javascript: + - extra.js +plugins: + - search + - gen-files: + scripts: + - scripts/gen_ref_pages.py + - literate-nav: + nav_file: SUMMARY.md + - section-index + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [.] + options: + show_root_heading: false + show_root_full_path: false + show_object_full_path: false + show_category_heading: false + show_if_no_docstring: false + show_source: true + show_bases: true + show_signature: true + heading_level: 2 + members_order: source + docstring_style: google + docstring_section_style: table + separate_signature: true + merge_init_into_class: true + show_submodules: false + filters: ["!^_[^_]", "!^__init__"] + show_inherited_members: false + annotations_path: source + docstring_options: + ignore_init_summary: true + line_length: 80 + show_root_members_full_path: false + show_module_member_docstring: true + - mkdocs-jupyter +nav: +- Home: index.md +- Contributing: contributing.md +- Code documentation: reference/ diff --git a/mqpy/__init__.py b/mqpy/__init__.py index e69de29..925fab0 100644 --- a/mqpy/__init__.py +++ b/mqpy/__init__.py @@ -0,0 +1,5 @@ +"""Python integration package. + +This package provides a bridge between Python and MetaTrader 5, allowing users to +create Expert Advisors and implement trading strategies in Python. +""" diff --git a/mqpy/__main__.py b/mqpy/__main__.py index 3b3def2..736961e 100644 --- a/mqpy/__main__.py +++ b/mqpy/__main__.py @@ -1,3 +1,5 @@ +"""Main entry point for the MQL5-Python integration package.""" + from .template import main if __name__ == "__main__": diff --git a/mqpy/book.py b/mqpy/book.py index 44bb66c..828d0a5 100644 --- a/mqpy/book.py +++ b/mqpy/book.py @@ -1,14 +1,31 @@ -from typing import Optional +"""Module for managing a market book for a financial instrument. + +Provides the Book class for accessing market depth information. +""" + +from __future__ import annotations + +import logging +from typing import Any import MetaTrader5 as Mt5 +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Create console handler with formatting +console_handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + class Book: """Represents a market book for a financial instrument.""" def __init__(self, symbol: str) -> None: - """ - Initialize the Book object. + """Initialize a Book object. Args: symbol (str): The financial instrument symbol. @@ -18,22 +35,20 @@ def __init__(self, symbol: str) -> None: """ self.symbol: str = symbol if Mt5.market_book_add(self.symbol): - print(f"The symbol {self.symbol} was successfully added to the market book.") + logger.info(f"The symbol {self.symbol} was successfully added to the market book.") else: - print(f"Error adding {self.symbol} to the market book. Error: {Mt5.last_error()}") + logger.error(f"Error adding {self.symbol} to the market book. Error: {Mt5.last_error()}") - def get(self) -> Optional[dict]: - """ - Get the market book for the financial instrument. + def get(self) -> dict[str, Any] | None: + """Get the market book for the financial instrument. Returns: - Optional[dict]: A dictionary representing the market book, or None if unsuccessful. + dict[str, Any] | None: The market book data if successful, None otherwise. """ return Mt5.market_book_get(self.symbol) def release(self) -> bool: - """ - Release the market book for the financial instrument. + """Release the market book for the financial instrument. Returns: bool: True if successful, False otherwise. diff --git a/mqpy/indicator_connector.py b/mqpy/indicator_connector.py index d74ee80..24fc282 100644 --- a/mqpy/indicator_connector.py +++ b/mqpy/indicator_connector.py @@ -1,5 +1,25 @@ +"""Python integration module for technical indicators. + +This module provides a bridge between Python and MT5 indicators through socket connections. +""" + +from __future__ import annotations + +import ast import json +import logging import socket +from typing import Any + +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Create console handler with formatting +console_handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) # To be able to use it you need the MQL5 Service to send the data, it is possible to found it here: # -------------------------------------------------------------------- # @@ -55,7 +75,24 @@ class Indicator: - def __init__(self, address="localhost", port=9090, listen=1): + """A class for connecting to and retrieving data from MetaTrader 5 technical indicators. + + This class provides methods to connect to various technical indicators in MetaTrader 5 + through a socket connection. Each method corresponds to a specific technical indicator + and returns its calculated values. + """ + + def __init__(self, address: str = "localhost", port: int = 9090, listen: int = 1) -> None: + """Initialize the Indicator connector. + + Args: + address (str): The address to bind to. Defaults to "localhost". + port (int): The port to bind to. Defaults to 9090. + listen (int): The number of connections to accept. Defaults to 1. + + Returns: + None + """ self.address = address self.port = port self.listen = listen @@ -68,11 +105,21 @@ def __init__(self, address="localhost", port=9090, listen=1): # -------------------------------------------------------------------- # def accelerator_oscillator( - self, symbol, time_frame=1, start_position=0 - ): # Change it if you want past values, zero is the most recent. + self, symbol: str, time_frame: int = 1, start_position: int = 0 + ) -> dict[str, Any] | None: # Change it if you want past values, zero is the most recent. + """Calculate the Accelerator Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() - message = f"accelerator_oscillator," f"{symbol}," f"{time_frame}," f"{start_position}" + message = f"accelerator_oscillator,{symbol},{time_frame},{start_position}" client_socket.send(bytes(message, "utf-8")) data = client_socket.recv(1024) @@ -80,29 +127,41 @@ def accelerator_oscillator( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def accumulation_distribution( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Accumulation/Distribution. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -115,27 +174,28 @@ def accumulation_distribution( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def adaptive_moving_average( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ama_period=9, - fast_ma_period=2, - slow_ma_period=30, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ama_period: int = 9, + fast_ma_period: int = 2, + slow_ma_period: int = 30, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -144,8 +204,22 @@ def adaptive_moving_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=4, - ): + applied_price: int = 4, + ) -> dict[str, Any] | None: + """Calculate the Adaptive Moving Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ama_period (int): The period for the Adaptive Moving Average. Defaults to 9. + fast_ma_period (int): The fast moving average period. Defaults to 2. + slow_ma_period (int): The slow moving average period. Defaults to 30. + applied_price (int): The price type for calculations. Defaults to 4. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -165,33 +239,34 @@ def adaptive_moving_average( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def alligator( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - jaw_period=13, - teeth_period=8, - lips_period=5, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + jaw_period: int = 13, + teeth_period: int = 8, + lips_period: int = 5, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=2, + ma_method: int = 2, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -200,8 +275,23 @@ def alligator( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=4, - ): + applied_price: int = 4, + ) -> dict[str, Any] | None: + """Calculate the Alligator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + jaw_period (int): The period for the Alligator's jaw. Defaults to 13. + teeth_period (int): The period for the Alligator's teeth. Defaults to 8. + lips_period (int): The period for the Alligator's lips. Defaults to 5. + ma_method (int): The method for the Alligator's moving average. Defaults to 2. + applied_price (int): The price type for calculations. Defaults to 4. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -222,22 +312,38 @@ def alligator( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def average_directional_index( - self, symbol, time_frame=1, period=14, start_position=0 - ): # Change it if you want past values, zero is the most recent. + self, + symbol: str, + time_frame: int = 1, + period: int = 14, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ) -> dict[str, Any] | None: + """Calculate the Average Directional Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + period (int): The period for the Average Directional Index. Defaults to 14. + start_position (int): Starting position for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"average_directional_index," f"{symbol}," f"{time_frame}," f"{period}," f"{start_position}" @@ -248,24 +354,38 @@ def average_directional_index( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() - # -------------------------------------------------------------------- # + return None - def average_directional_index_wilder( - self, symbol, time_frame=1, period=14, start_position=0 - ): # Change it if you want past values, zero is the most - # recent. + # -------------------------------------------------------------------- # + def average_directional_index_wilder( + self, + symbol: str, + time_frame: int = 1, + period: int = 14, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ) -> dict[str, Any] | None: + """Calculate the Average Directional Index Wilder. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + period (int): The period for the Average Directional Index Wilder. Defaults to 14. + start_position (int): Starting position for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -278,26 +398,38 @@ def average_directional_index_wilder( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def average_true_range( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=14, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, + ) -> dict[str, Any] | None: + """Calculate the Average True Range. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Average True Range. Defaults to 14. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"average_true_range," f"{symbol}," f"{time_frame}," f"{start_position}," f"{ma_period}" @@ -308,22 +440,33 @@ def average_true_range( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def awesome_oscillator( - self, symbol, time_frame=1, start_position=0 - ): # Change it if you want past values, zero is the most recent. + self, symbol: str, time_frame: int = 1, start_position: int = 0 + ) -> dict[str, Any] | None: # Change it if you want past values, zero is the most recent. + """Calculate the Awesome Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"awesome_oscillator," f"{symbol}," f"{time_frame}," f"{start_position}" @@ -334,27 +477,28 @@ def awesome_oscillator( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # # Free def bollinger_bands( self, - symbol, - time_frame=1, - period=20, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_shift=0, - deviation=2.000, + symbol: str, + time_frame: int = 1, + period: int = 20, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_shift: int = 0, + deviation: float = 2.000, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -363,8 +507,22 @@ def bollinger_bands( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Bollinger Bands. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + period (int): The period for the Bollinger Bands. Defaults to 20. + start_position (int): Starting position for calculations. Defaults to 0. + ma_shift (int): The shift for the moving average. Defaults to 0. + deviation (float): The deviation for the Bollinger Bands. Defaults to 2.000. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -384,26 +542,38 @@ def bollinger_bands( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def bears_power( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=13, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 13, + ) -> dict[str, Any] | None: + """Calculate the Bears Power. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Bears Power. Defaults to 13. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"bears_power," f"{symbol}," f"{time_frame}," f"{start_position}," f"{ma_period}" @@ -414,26 +584,38 @@ def bears_power( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def bulls_power( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=13, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 13, + ) -> dict[str, Any] | None: + """Calculate the Bulls Power. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Bulls Power. Defaults to 13. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"bulls_power," f"{symbol}," f"{time_frame}," f"{start_position}," f"{ma_period}" @@ -444,37 +626,52 @@ def bulls_power( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def chaikin_oscillator( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - fast_ma_period=3, - slow_ma_period=10, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + fast_ma_period: int = 3, + slow_ma_period: int = 10, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=1, + ma_method: int = 1, # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Chaikin Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + fast_ma_period (int): The period for the fast moving average. Defaults to 3. + slow_ma_period (int): The period for the slow moving average. Defaults to 10. + ma_method (int): The method for the moving average. Defaults to 1. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -494,25 +691,26 @@ def chaikin_oscillator( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def commodity_channel_index( self, - symbol, - time_frame=1, - start_position=1, # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 1, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # For this Indicator, the applied price is shifted. # applied_price: # 1 - PRICE_CLOSE @@ -522,8 +720,20 @@ def commodity_channel_index( # 5 - PRICE_MEDIAN # 6 - PRICE_TYPICAL # 7 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Commodity Channel Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 1. + ma_period (int): The period for the Commodity Channel Index. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -541,26 +751,38 @@ def commodity_channel_index( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() - # -------------------------------------------------------------------- # + return None + + # -------------------------------------------------------------------- # def demarker( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - period=14, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + period: int = 14, + ) -> dict[str, Any] | None: + """Calculate the DeMarker. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + period (int): The period for the DeMarker. Defaults to 14. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"demarker," f"{symbol}," f"{time_frame}," f"{start_position}," f"{period}" @@ -571,26 +793,26 @@ def demarker( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection aborted by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() + + return None # -------------------------------------------------------------------- # def double_exponential_moving_average( self, - symbol, - time_frame=1, - start_position=0, - # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -599,8 +821,20 @@ def double_exponential_moving_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Double Exponential Moving Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Double Exponential Moving Average. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -620,29 +854,28 @@ def double_exponential_moving_average( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def envelopes( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=0, + ma_method: int = 0, # For this Indicator, the applied price is shifted. # applied_price: # 1 - PRICE_CLOSE @@ -652,9 +885,23 @@ def envelopes( # 5 - PRICE_MEDIAN # 6 - PRICE_TYPICAL # 7 - PRICE_WEIGHTED - applied_price=1, - deviation=0.100, - ): + applied_price: int = 1, + deviation: float = 0.100, + ) -> dict[str, Any] | None: + """Calculate the Envelopes. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Envelopes. Defaults to 14. + ma_method (int): The method for the Envelopes. Defaults to 0. + applied_price (int): The price type for calculations. Defaults to 1. + deviation (float): The deviation for the Envelopes. Defaults to 0.100. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -676,32 +923,44 @@ def envelopes( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") def force_index( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=13, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 13, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=0, + ma_method: int = 0, # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Force Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Force Index. Defaults to 13. + ma_method (int): The method for the Force Index. Defaults to 0. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -722,23 +981,22 @@ def force_index( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def fractal_adaptive_moving_average( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -747,8 +1005,20 @@ def fractal_adaptive_moving_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Fractal Adaptive Moving Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Fractal Adaptive Moving Average. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -768,20 +1038,32 @@ def fractal_adaptive_moving_average( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def fractals( - self, symbol, time_frame=1, start_position=0 - ): # Change it if you want past values, zero is the most recent. + self, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ) -> dict[str, Any] | None: + """Calculate the Fractals. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"fractals," f"{symbol}," f"{time_frame}," f"{start_position}" @@ -794,34 +1076,33 @@ def fractals( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # # https://www.mql5.com/en/forum/41357 def gator_oscillator( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - jaw_period=13, - jaw_shift=8, - teeth_period=8, - teeth_shift=5, - lips_period=5, - lips_shift=3, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + jaw_period: int = 13, + jaw_shift: int = 8, + teeth_period: int = 8, + teeth_shift: int = 5, + lips_period: int = 5, + lips_shift: int = 3, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=2, + ma_method: int = 2, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -830,8 +1111,26 @@ def gator_oscillator( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=4, - ): + applied_price: int = 4, + ) -> dict[str, Any] | None: + """Calculate the Gator Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + jaw_period (int): The period for the jaw. Defaults to 13. + jaw_shift (int): The shift for the jaw. Defaults to 8. + teeth_period (int): The period for the teeth. Defaults to 8. + teeth_shift (int): The shift for the teeth. Defaults to 5. + lips_period (int): The period for the lips. Defaults to 5. + lips_shift (int): The shift for the lips. Defaults to 3. + ma_method (int): The method for the moving average. Defaults to 2. + applied_price (int): The price type for calculations. Defaults to 4. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -857,26 +1156,38 @@ def gator_oscillator( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def ichimoku_kinko_hyo( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - tenkan_sen=9, - kijun_sen=26, - senkou_span_b=52, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + tenkan_sen: int = 9, + kijun_sen: int = 26, + senkou_span_b: int = 52, + ) -> dict[str, Any] | None: + """Calculate the Ichimoku Kinko Hyo. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + tenkan_sen (int): The period for the Tenkan-sen. Defaults to 9. + kijun_sen (int): The period for the Kijun-sen. Defaults to 26. + senkou_span_b (int): The period for the Senkou Span B. Defaults to 52. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -897,25 +1208,24 @@ def ichimoku_kinko_hyo( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # # Free def macd( self, - symbol, - time_frame=1, - fast_ema_period=12, - slow_ema_period=26, - signal_period=9, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + fast_ema_period: int = 12, + slow_ema_period: int = 26, + signal_period: int = 9, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -924,8 +1234,22 @@ def macd( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Moving Average Convergence Divergence. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + fast_ema_period (int): The period for the fast EMA. Defaults to 12. + slow_ema_period (int): The period for the slow EMA. Defaults to 26. + signal_period (int): The period for the signal line. Defaults to 9. + start_position (int): Starting position for calculations. Defaults to 0. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -947,27 +1271,37 @@ def macd( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def market_facilitation_index( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Market Facilitation Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + applied_volume (int): Volume type to use in calculations. Defaults to 0 (VOLUME_TICK). + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -980,25 +1314,24 @@ def market_facilitation_index( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() - except ConnectionAbortedError: - pass + return None # -------------------------------------------------------------------- # def momentum( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - mom_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + mom_period: int = 14, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1007,8 +1340,20 @@ def momentum( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Momentum. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + mom_period (int): The period for the Momentum. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1023,28 +1368,39 @@ def momentum( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def money_flow_index( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Money Flow Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Money Flow Index. Defaults to 14. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1062,31 +1418,30 @@ def money_flow_index( result = data.decode("utf-8") try: return json.loads(result) - except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") + finally: + if "client_socket" in locals(): + client_socket.close() - except ConnectionAbortedError: - pass + return None # -------------------------------------------------------------------- # - # Free + def moving_average( self, - symbol, - time_frame=1, - period=20, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + period: int = 20, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - method=0, + method: int = 0, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1095,8 +1450,21 @@ def moving_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Moving Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + period (int): The period for the Moving Average. Defaults to 20. + start_position (int): Starting position for calculations. Defaults to 0. + method (int): The method for the Moving Average. Defaults to 0. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1117,25 +1485,24 @@ def moving_average( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def moving_average_of_oscillator( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - fast_ema_period=12, - slow_ema_period=26, - macd_sma_period=9, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + fast_ema_period: int = 12, + slow_ema_period: int = 26, + macd_sma_period: int = 9, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1144,8 +1511,22 @@ def moving_average_of_oscillator( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Moving Average of Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + fast_ema_period (int): The period for the fast EMA. Defaults to 12. + slow_ema_period (int): The period for the slow EMA. Defaults to 26. + macd_sma_period (int): The period for the MACD SMA. Defaults to 9. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1167,27 +1548,37 @@ def moving_average_of_oscillator( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # # Free def obv( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the On Balance Volume. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"obv," f"{symbol}," f"{time_frame}," f"{start_position}," f"{applied_volume}" @@ -1200,25 +1591,36 @@ def obv( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def parabolic_sar( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - step=0.02, - maximum=0.2, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + step: float = 0.02, + maximum: float = 0.2, + ) -> dict[str, Any] | None: + """Calculate the Parabolic SAR. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + step (float): The step for the Parabolic SAR. Defaults to 0.02. + maximum (float): The maximum for the Parabolic SAR. Defaults to 0.2. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"parabolic_sar," f"{symbol}," f"{time_frame}," f"{start_position}," f"{step}," f"{maximum}" @@ -1231,23 +1633,22 @@ def parabolic_sar( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def relative_strength_index( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=14, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 14, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1256,8 +1657,20 @@ def relative_strength_index( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Relative Strength Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Relative Strength Index. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1277,24 +1690,34 @@ def relative_strength_index( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def relative_vigor_index( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=10, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 10, + ) -> dict[str, Any] | None: + """Calculate the Relative Vigor Index. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Relative Vigor Index. Defaults to 10. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"relative_vigor_index," f"{symbol}," f"{time_frame}," f"{start_position}," f"{ma_period}" @@ -1307,29 +1730,28 @@ def relative_vigor_index( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def standard_deviation( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - ma_period=20, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + ma_period: int = 20, # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - ma_method=0, + ma_method: int = 0, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1338,8 +1760,21 @@ def standard_deviation( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Standard Deviation. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Standard Deviation. Defaults to 20. + ma_method (int): The method for the Standard Deviation. Defaults to 0. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1360,36 +1795,50 @@ def standard_deviation( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # # Free def stochastic( self, - symbol, - time_frame=1, - k_period=5, - d_period=3, - slowing=3, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + k_period: int = 5, + d_period: int = 3, + slowing: int = 3, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # method: # 0 - MODE_SMA # 1 - MODE_EMA # 2 - MODE_SMMA # 3 - MODE_LWMA - method=0, + method: int = 0, # applied_price # 0 - STO_LOWHIGH # 1 - STO_CLOSECLOSE - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Stochastic. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + k_period (int): The period for the Stochastic. Defaults to 5. + d_period (int): The period for the Stochastic. Defaults to 3. + slowing (int): The slowing for the Stochastic. Defaults to 3. + start_position (int): Starting position for calculations. Defaults to 0. + method (int): The method for the Stochastic. Defaults to 0. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1412,25 +1861,23 @@ def stochastic( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def triple_exponential_ma_oscillator( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, - # zero is the most recent. - ma_period=14, - # applied_price: + symbol: str, + time_frame: int, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + count: int = 100, # Number of elements to be calculated. + ma_period: int = 14, # 0 - PRICE_CLOSE # 1 - PRICE_OPEN # 2 - PRICE_HIGH @@ -1438,45 +1885,45 @@ def triple_exponential_ma_oscillator( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Triple Exponential Moving Average Oscillator. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. + start_position (int): Starting position for calculations. Defaults to 0. + count (int): Number of elements to calculate. Defaults to 100. + ma_period (int): Moving average period. Defaults to 14. + applied_price (int): Price type to use in calculations. Defaults to 0 (PRICE_CLOSE). + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() - message = ( - f"triple_exponential_ma_oscillator," - f"{symbol}," - f"{time_frame}," - f"{start_position}," - f"{ma_period}," - f"{applied_price}" - ) - - client_socket.send(bytes(message, "utf-8")) - data = client_socket.recv(1024) - - result = data.decode("utf-8") try: - return json.loads(result) - + data = f"TEMA_OSC,{symbol},{time_frame},{start_position},{count},{ma_period},{applied_price}" + client_socket.send(data.encode()) + response = client_socket.recv(1024).decode() + return ast.literal_eval(response) except ValueError: - print("Connection lost to MQL5 Service") - pass - + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass - - except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") + finally: + client_socket.close() + return None # -------------------------------------------------------------------- # def triple_exponential_moving_average( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, # zero is the most recent. - ma_period=14, + ma_period: int = 14, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1485,8 +1932,20 @@ def triple_exponential_moving_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Triple Exponential Moving Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + ma_period (int): The period for the Triple Exponential Moving Average. Defaults to 14. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1506,24 +1965,23 @@ def triple_exponential_moving_average( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def variable_index_dynamic_average( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - cmo_period=9, - ema_period=12, + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + cmo_period: int = 9, + ema_period: int = 12, # applied_price: # 0 - PRICE_CLOSE # 1 - PRICE_OPEN @@ -1532,8 +1990,21 @@ def variable_index_dynamic_average( # 4 - PRICE_MEDIAN # 5 - PRICE_TYPICAL # 6 - PRICE_WEIGHTED - applied_price=0, - ): + applied_price: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Variable Index Dynamic Average. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + cmo_period (int): The period for the CMO. Defaults to 9. + ema_period (int): The period for the EMA. Defaults to 12. + applied_price (int): The price type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = ( @@ -1554,27 +2025,37 @@ def variable_index_dynamic_average( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def volumes( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. # applied_volume: # 0 - VOLUME_TICK # 1 - VOLUME_REAL - applied_volume=0, - ): + applied_volume: int = 0, + ) -> dict[str, Any] | None: + """Calculate the Volumes. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + applied_volume (int): The volume type for calculations. Defaults to 0. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"volumes," f"{symbol}," f"{time_frame}," f"{start_position}," f"{applied_volume}" @@ -1587,24 +2068,34 @@ def volumes( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") # -------------------------------------------------------------------- # def williams_percent_range( self, - symbol, - time_frame=1, - start_position=0, # Change it if you want past values, zero is the most recent. - calc_period=14, - ): + symbol: str, + time_frame: int = 1, + start_position: int = 0, # Change it if you want past values, zero is the most recent. + calc_period: int = 14, + ) -> dict[str, Any] | None: + """Calculate the Williams Percent Range. + + Args: + symbol (str): The financial instrument symbol. + time_frame (int): The time frame for calculations. Defaults to 1. + start_position (int): Starting position for calculations. Defaults to 0. + calc_period (int): The period for the Williams Percent Range. Defaults to 14. + + Returns: + dict[str, Any] | None: The calculated values if successful, None otherwise. + """ try: client_socket, address = self.s.accept() message = f"williams_percent_range," f"{symbol}," f"{time_frame}," f"{start_position}," f"{calc_period}" @@ -1617,11 +2108,10 @@ def williams_percent_range( return json.loads(result) except ValueError: - print("Connection lost to MQL5 Service") - pass + logger.exception("Connection lost to MQL5 Service") except ConnectionResetError: - pass + logger.exception("Connection reset by MQL5 Service") except ConnectionAbortedError: - pass + logger.exception("Connection reset by MQL5 Service") diff --git a/mqpy/rates.py b/mqpy/rates.py index c827f5c..56c7c9a 100644 --- a/mqpy/rates.py +++ b/mqpy/rates.py @@ -1,80 +1,84 @@ -from typing import List, Union +"""Module for retrieving and managing historical price data from MetaTrader 5. + +Provides the Rates class for accessing historical price information. +""" + +from __future__ import annotations import MetaTrader5 as Mt5 class Rates: - """Represents historical rates data for a financial instrument.""" + """Represents historical price data for a financial instrument.""" - def __init__(self, symbol: str, time_frame: int, start_pos: int, period: int) -> None: - """ - Initializes a Rates object. + def __init__(self, symbol: str, time_frame: int, start_position: int, count: int) -> None: + """Initializes a Rates object. Args: symbol (str): The financial instrument symbol. - time_frame (int): The time frame of the rates data. - start_pos (int): The starting position for data retrieval. - period (int): The number of rates to retrieve. + time_frame (int): The time frame for the rates. + start_position (int): The starting position for the rates. + count (int): The number of rates to retrieve. Returns: None """ - self._symbol = symbol + + def _raise_value_error(msg: str) -> None: + raise ValueError(msg) + try: - rates_data = Mt5.copy_rates_from_pos(self._symbol, time_frame, start_pos, period) - - self._time = rates_data["time"] - self._open = rates_data["open"] - self._high = rates_data["high"] - self._low = rates_data["low"] - self._close = rates_data["close"] - self._tick_volume = rates_data["tick_volume"] - self._spread = rates_data["spread"] - self._real_volume = rates_data["real_volume"] - - # Optionally, you can print statements here for debugging - # print(f"Rates object created for symbol: {self._symbol}") - except Exception as e: - # Optionally, you can print statements here for debugging - # print(f"Failed to create Rates object for symbol {self._symbol}. Error: {e}") - raise + rates = Mt5.copy_rates_from_pos(symbol, time_frame, start_position, count) + if rates is None: + _raise_value_error(f"Failed to retrieve rates for {symbol}") + + self._time = [rate[0] for rate in rates] + self._open = [rate[1] for rate in rates] + self._high = [rate[2] for rate in rates] + self._low = [rate[3] for rate in rates] + self._close = [rate[4] for rate in rates] + self._tick_volume = [rate[5] for rate in rates] + self._spread = [rate[6] for rate in rates] + self._real_volume = [rate[7] for rate in rates] + except Mt5.Error as e: + raise ValueError(f"Failed to create Rates object for symbol {symbol}") from e @property - def time(self) -> List[Union[int, float]]: + def time(self) -> list[int | float]: """List of timestamps.""" return self._time @property - def open(self) -> List[float]: + def open(self) -> list[float]: """List of open prices.""" return self._open @property - def high(self) -> List[float]: + def high(self) -> list[float]: """List of high prices.""" return self._high @property - def low(self) -> List[float]: + def low(self) -> list[float]: """List of low prices.""" return self._low @property - def close(self) -> List[float]: + def close(self) -> list[float]: """List of close prices.""" return self._close @property - def tick_volume(self) -> List[int]: + def tick_volume(self) -> list[int]: """List of tick volumes.""" return self._tick_volume @property - def spread(self) -> List[int]: + def spread(self) -> list[int]: """List of spreads.""" return self._spread @property - def real_volume(self) -> List[int]: + def real_volume(self) -> list[int]: """List of real volumes.""" return self._real_volume diff --git a/mqpy/template.py b/mqpy/template.py index fa8fe13..66dfcd2 100644 --- a/mqpy/template.py +++ b/mqpy/template.py @@ -1,18 +1,33 @@ +"""Module for generating MT5 expert advisor template files. + +Provides functionality to create template files for trading strategies. +""" + +from __future__ import annotations + import argparse +from pathlib import Path +from typing import Any -def get_arguments(): +def get_arguments() -> dict[str, Any]: + """Parse command line arguments. + + Returns: + dict[str, Any]: Dictionary containing the parsed arguments. + """ parser = argparse.ArgumentParser() parser.add_argument("--file_name", type=str, action="store", default="demo") parser.add_argument("--symbol", type=str, action="store", default="EURUSD") return vars(parser.parse_args()) -def main(): +def main() -> None: + """Generate a template file for a trading strategy.""" file_name = get_arguments()["file_name"] symbol = get_arguments()["symbol"] - with open(f"{file_name}.py", "w") as file: + with Path(f"{file_name}.py").open("w") as file: file.write( f"""from mqpy.rates import Rates from mqpy.tick import Tick @@ -21,7 +36,7 @@ def main(): # Initialize the trading strategy trade = Trade( expert_name="Moving Average Crossover", - version=1.0, + version="1.0", symbol="{symbol}", magic_number=567, lot=1.0, @@ -69,3 +84,7 @@ def main(): print("Program finished.") """ ) + + +if __name__ == "__main__": + main() diff --git a/mqpy/tick.py b/mqpy/tick.py index a1fb5e6..a6b097a 100644 --- a/mqpy/tick.py +++ b/mqpy/tick.py @@ -1,4 +1,9 @@ -from typing import Optional +"""Module for retrieving and managing real-time tick data from MetaTrader 5. + +Provides the Tick class for accessing current market price information. +""" + +from __future__ import annotations import MetaTrader5 as Mt5 @@ -7,8 +12,7 @@ class Tick: """Represents real-time tick data for a financial instrument.""" def __init__(self, symbol: str) -> None: - """ - Initializes a Tick object. + """Initializes a Tick object. Args: symbol (str): The financial instrument symbol. @@ -69,6 +73,10 @@ def flags(self) -> int: return self._flags @property - def volume_real(self) -> Optional[float]: + def volume_real(self) -> float | None: """Real volume (if available).""" return self._volume_real + + def real_volume(self) -> list[int]: + """List of real volumes.""" + return self._real_volume diff --git a/mqpy/trade.py b/mqpy/trade.py index 328678b..907be72 100644 --- a/mqpy/trade.py +++ b/mqpy/trade.py @@ -1,11 +1,27 @@ -from datetime import datetime, timedelta +"""Module for trading operations with MetaTrader 5. + +Provides a Trade class for managing trading operations. +""" + +import logging +import sys +from datetime import datetime, timedelta, timezone import MetaTrader5 as Mt5 +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Create console handler with formatting +console_handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + class Trade: - """ - Represents a trading strategy for a financial instrument. + """Represents a trading strategy for a financial instrument. Args: expert_name (str): The name of the expert advisor. @@ -39,8 +55,7 @@ def __init__( ending_time: str = "17:50", fee: float = 0.0, ) -> None: - """ - Initialize the Trade object. + """Initialize the Trade object. Returns: None @@ -66,73 +81,70 @@ def __init__( self.ticket: int = 0 - print("\nInitializing the basics.") + logger.info("Initializing the basics.") self.initialize() self.select_symbol() self.prepare_symbol() self.sl_tp_steps: float = Mt5.symbol_info(self.symbol).trade_tick_size / Mt5.symbol_info(self.symbol).point - print("Initialization successfully completed.") - - print() + logger.info("Initialization successfully completed.") + logger.info("") self.summary() - print("Running") - print() + logger.info("Running") + logger.info("") def initialize(self) -> None: - """ - Initialize the MetaTrader 5 instance. + """Initialize the MetaTrader 5 instance. Returns: None """ if not Mt5.initialize(): - print("Initialization failed, check internet connection. You must have Meta Trader 5 installed.") + logger.error("Initialization failed, check internet connection. You must have Meta Trader 5 installed.") Mt5.shutdown() else: - print( + logger.info( f"You are running the {self.expert_name} expert advisor," f" version {self.version}, on symbol {self.symbol}." ) def select_symbol(self) -> None: - """ - Select the trading symbol. + """Select the trading symbol. Returns: None """ - Mt5.symbol_select(self.symbol, True) + Mt5.symbol_select(self.symbol, enable=True) def prepare_symbol(self) -> None: - """ - Prepare the trading symbol for opening positions. + """Prepare the trading symbol for opening positions. Returns: None """ symbol_info = Mt5.symbol_info(self.symbol) if symbol_info is None: - print(f"It was not possible to find {self.symbol}") + logger.error(f"It was not possible to find {self.symbol}") Mt5.shutdown() - print("Turned off") - quit() + logger.error("Turned off") + sys.exit(1) if not symbol_info.visible: - print(f"The {self.symbol} is not visible, needed to be switched on.") - if not Mt5.symbol_select(self.symbol, True): - print(f"The expert advisor {self.expert_name} failed in select the symbol {self.symbol}, turning off.") + logger.warning(f"The {self.symbol} is not visible, needed to be switched on.") + if not Mt5.symbol_select(self.symbol, enable=True): + logger.error( + f"The expert advisor {self.expert_name} failed in select the symbol {self.symbol}, turning off." + ) Mt5.shutdown() - print("Turned off") - quit() + logger.error("Turned off") + sys.exit(1) def summary(self) -> None: - """ - Print a summary of the expert advisor parameters. + """Print a summary of the expert advisor parameters. Returns: None """ - print( + logger.info( f"Summary:\n" f"ExpertAdvisor name: {self.expert_name}\n" f"ExpertAdvisor version: {self.version}\n" @@ -151,23 +163,21 @@ def summary(self) -> None: ) def statistics(self) -> None: - """ - Print statistics of the expert advisor. + """Print statistics of the expert advisor. Returns: None """ - print(f"Total of deals: {self.total_deals}, {self.profit_deals} gain, {self.loss_deals} loss.") - print( + logger.info(f"Total of deals: {self.total_deals}, {self.profit_deals} gain, {self.loss_deals} loss.") + logger.info( f"Balance: {self.balance}, fee: {self.total_deals * self.fee}, final balance:" f" {self.balance - (self.total_deals * self.fee)}." ) if self.total_deals != 0: - print(f"Accuracy: {round((self.profit_deals / self.total_deals) * 100, 2)}%.\n") + logger.info(f"Accuracy: {round((self.profit_deals / self.total_deals) * 100, 2)}%.\n") def open_buy_position(self, comment: str = "") -> None: - """ - Open a Buy position. + """Open a Buy position. Args: comment (str): A comment for the trade. @@ -199,8 +209,7 @@ def open_buy_position(self, comment: str = "") -> None: self.request_result(price, result) def open_sell_position(self, comment: str = "") -> None: - """ - Open a Sell position. + """Open a Sell position. Args: comment (str): A comment for the trade. @@ -231,61 +240,56 @@ def open_sell_position(self, comment: str = "") -> None: result = Mt5.order_send(request) self.request_result(price, result) - def request_result(self, price: float, result) -> None: - """ - Process the result of a trading request. + def request_result(self, price: float, result: Mt5.TradeResult) -> None: + """Process the result of a trading request. Args: price (float): The price of the trade. result (Mt5.TradeResult): The result of the trading request. - Returns: + + Returns: None """ - # Send a trading request - # Check the execution result - print(f"Order sent: {self.symbol}, {self.lot} lot(s), at {price}.") + logger.info(f"Order sent: {self.symbol}, {self.lot} lot(s), at {price}.") if result.retcode != Mt5.TRADE_RETCODE_DONE: - print(f"Something went wrong while retrieving ret_code, error: {result.retcode}") + logger.error(f"Something went wrong while retrieving ret_code, error: {result.retcode}") - # Print the result if result.retcode == Mt5.TRADE_RETCODE_DONE: if len(Mt5.positions_get(symbol=self.symbol)) == 1: order_type = "Buy" if Mt5.positions_get(symbol=self.symbol)[0].type == 0 else "Sell" - print(order_type, "Position Opened:", result.price) + logger.info(f"{order_type} Position Opened: {result.price}") else: - print(f"Position Closed: {result.price}") + logger.info(f"Position Closed: {result.price}") - def open_position(self, buy: bool, sell: bool, comment: str = "") -> None: - """ - Open a position based on buy and sell conditions. + def open_position(self, *, should_buy: bool, should_sell: bool, comment: str = "") -> None: + """Open a position based on buy and sell conditions. Args: - buy (bool): True if a Buy position should be opened, False otherwise. - sell (bool): True if a Sell position should be opened, False otherwise. + should_buy (bool): True if a Buy position should be opened, False otherwise. + should_sell (bool): True if a Sell position should be opened, False otherwise. comment (str): A comment for the trade. Returns: None """ if (len(Mt5.positions_get(symbol=self.symbol)) == 0) and self.trading_time(): - if buy and not sell: + if should_buy and not should_sell: self.open_buy_position(comment) self.total_deals += 1 - if sell and not buy: + if should_sell and not should_buy: self.open_sell_position(comment) self.total_deals += 1 self.stop_and_gain(comment) if self.days_end(): - print("It is the end of trading the day.") - print("Closing all positions.") + logger.info("It is the end of trading the day.") + logger.info("Closing all positions.") self.close_position(comment) self.summary() def close_position(self, comment: str = "") -> None: - """ - Close an open position. + """Close an open position. Args: comment (str): A comment for the trade. @@ -293,17 +297,14 @@ def close_position(self, comment: str = "") -> None: Returns: None """ - # buy (0) and sell(1) if len(Mt5.positions_get(symbol=self.symbol)) == 1: - if Mt5.positions_get(symbol=self.symbol)[0].type == 0: # if Buy + if Mt5.positions_get(symbol=self.symbol)[0].type == 0: # Buy position self.open_sell_position(comment) - - elif Mt5.positions_get(symbol=self.symbol)[0].type == 1: # if Sell + elif Mt5.positions_get(symbol=self.symbol)[0].type == 1: # Sell position self.open_buy_position(comment) def stop_and_gain(self, comment: str = "") -> None: - """ - Check for stop loss and take profit conditions and close positions accordingly. + """Check for stop loss and take profit conditions and close positions accordingly. Args: comment (str): A comment for the trade. @@ -321,61 +322,45 @@ def stop_and_gain(self, comment: str = "") -> None: if points / Mt5.symbol_info(self.symbol).point >= self.take_profit: self.profit_deals += 1 self.close_position(comment) - print( - f"Take profit reached. (" - f"{Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[-1].profit}" - f")\n" - ) - if ( - Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[-1].symbol - == self.symbol - ): - self.balance += Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[ - -1 - ].profit + start_time = datetime.now(timezone.utc) - timedelta(days=1) + end_time = datetime.now(timezone.utc) + profit = Mt5.history_deals_get(start_time, end_time)[-1].profit + logger.info(f"Take profit reached. ({profit})\n") + if Mt5.history_deals_get(start_time, end_time)[-1].symbol == self.symbol: + self.balance += profit self.statistics() elif ((points / Mt5.symbol_info(self.symbol).point) * -1) >= self.stop_loss: self.loss_deals += 1 self.close_position(comment) - print( - f"Stop loss reached. (" - f"{Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[-1].profit}" - f")\n" - ) - if ( - Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[-1].symbol - == self.symbol - ): - self.balance += Mt5.history_deals_get((datetime.today() - timedelta(days=1)), datetime.now())[ - -1 - ].profit + start_time = datetime.now(timezone.utc) - timedelta(days=1) + end_time = datetime.now(timezone.utc) + profit = Mt5.history_deals_get(start_time, end_time)[-1].profit + logger.info(f"Stop loss reached. ({profit})\n") + if Mt5.history_deals_get(start_time, end_time)[-1].symbol == self.symbol: + self.balance += profit self.statistics() def days_end(self) -> bool: - """ - Check if it is the end of trading for the day. + """Check if it is the end of trading for the day. Returns: bool: True if it is the end of trading for the day, False otherwise. """ - if datetime.now().hour >= int(self.ending_time_hour) and datetime.now().minute >= int(self.ending_time_minutes): - return True - return False + now = datetime.now(timezone.utc) + return now.hour >= int(self.ending_time_hour) and now.minute >= int(self.ending_time_minutes) def trading_time(self) -> bool: - """ - Check if it is within the allowed trading time. + """Check if it is within the allowed trading time. Returns: bool: True if it is within the allowed trading time, False otherwise. """ - if int(self.start_time_hour) < datetime.now().hour < int(self.finishing_time_hour): + now = datetime.now(timezone.utc) + if int(self.start_time_hour) < now.hour < int(self.finishing_time_hour): return True - elif datetime.now().hour == int(self.start_time_hour): - if datetime.now().minute >= int(self.start_time_minutes): - return True - elif datetime.now().hour == int(self.finishing_time_hour): - if datetime.now().minute < int(self.finishing_time_minutes): - return True + if now.hour == int(self.start_time_hour): + return now.minute >= int(self.start_time_minutes) + if now.hour == int(self.finishing_time_hour): + return now.minute < int(self.finishing_time_minutes) return False diff --git a/mqpy/utilities.py b/mqpy/utilities.py index 073308e..daf0d3b 100644 --- a/mqpy/utilities.py +++ b/mqpy/utilities.py @@ -1,7 +1,23 @@ -from datetime import datetime +"""Utility module for MetaTrader 5 integration. + +Provides helper functions and classes for trading operations. +""" + +import logging +from datetime import datetime, timezone import MetaTrader5 as Mt5 +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Create console handler with formatting +console_handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + class Utilities: """A utility class for handling trading-related functionalities.""" @@ -16,8 +32,7 @@ def __init__(self) -> None: self.__recent_trade: bool = False def check_trade_availability(self, symbol: str, count_until: int) -> bool: - """ - Check if trading is allowed based on specified conditions. + """Check if trading is allowed based on specified conditions. Args: symbol (str): The financial instrument symbol. @@ -29,22 +44,20 @@ def check_trade_availability(self, symbol: str, count_until: int) -> bool: if len(Mt5.positions_get(symbol=symbol)) == 1: self.__recent_trade = True - if len(Mt5.positions_get(symbol=symbol)) != 1 and self.__recent_trade: - if not self.__allow_to_count: - self.__allow_to_count = True - self.__allowed_to_trade = False - self.__recent_trade = False + if len(Mt5.positions_get(symbol=symbol)) != 1 and self.__recent_trade and not self.__allow_to_count: + self.__allow_to_count = True + self.__allowed_to_trade = False - if datetime.now().second == 0 and self.__counter_flag and self.__allow_to_count: - print(f"Trading will be allowed in {count_until - self.__minutes_counter} minutes.") + if datetime.now(timezone.utc).second == 0 and self.__counter_flag and self.__allow_to_count: + logger.info(f"Trading will be allowed in {count_until - self.__minutes_counter} minutes.") self.__minutes_counter += 1 self.__counter_flag = False - if datetime.now().second == 59: + if datetime.now(timezone.utc).second == 59: self.__counter_flag = True if self.__minutes_counter == count_until: - print("Trading is allowed.\n") + logger.info("Trading is allowed.\n") self.__reset_counters() return self.__allowed_to_trade diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 5ea6440..0000000 --- a/poetry.lock +++ /dev/null @@ -1,422 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. - -[[package]] -name = "black" -version = "24.8.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "filelock" -version = "3.13.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] - -[[package]] -name = "identify" -version = "2.5.33" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "metatrader5" -version = "5.0.4874" -description = "API Connector to MetaTrader 5 Terminal" -optional = false -python-versions = "<4,>3.5" -groups = ["main"] -files = [ - {file = "MetaTrader5-5.0.4874-cp310-cp310-win_amd64.whl", hash = "sha256:921b4336919f8a8beb6397fff20e1277aaaada7be7207d5d8429035e5cd18ee2"}, - {file = "MetaTrader5-5.0.4874-cp311-cp311-win_amd64.whl", hash = "sha256:3628f2b1b5e6fb33deb0801513ffc3b98f33ee2b358bcf00d8f3d4413e8a1572"}, - {file = "MetaTrader5-5.0.4874-cp312-cp312-win_amd64.whl", hash = "sha256:857d681b2d92b8a6531bc91995fb3f6834d3ec8c50afc975c996638f3484e6ae"}, - {file = "MetaTrader5-5.0.4874-cp313-cp313-win_amd64.whl", hash = "sha256:11191437c5032d3c9df37d73b50d1b3b5f0e21ee5d3004aeee388526a38c652e"}, - {file = "MetaTrader5-5.0.4874-cp36-cp36m-win_amd64.whl", hash = "sha256:3a9ee9d8639eba242d09add1f0e615c144fd21c0fa6584468c60c81bb104d7c1"}, - {file = "MetaTrader5-5.0.4874-cp37-cp37m-win_amd64.whl", hash = "sha256:9d48bb46be71d6859b4678d814417bfb0607ec5cec89171a9069de7513adb43f"}, - {file = "MetaTrader5-5.0.4874-cp38-cp38-win_amd64.whl", hash = "sha256:9b132e90f301656d3ffb4192b85a8f129ae1d18cb095da0dbedcae585ebfdf88"}, - {file = "MetaTrader5-5.0.4874-cp39-cp39-win_amd64.whl", hash = "sha256:ad231186e98fd12ff19368d6fc475144c2c39a89fed6ac25dbc03d7a0c952a28"}, -] - -[package.dependencies] -numpy = ">=1.7" - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -groups = ["dev"] -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, -] - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - -[[package]] -name = "platformdirs" -version = "4.1.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pre-commit" -version = "3.5.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "setuptools" -version = "69.0.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, - {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "typing-extensions" -version = "4.8.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, -] - -[[package]] -name = "virtualenv" -version = "20.25.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - -[metadata] -lock-version = "2.1" -python-versions = "^3.8" -content-hash = "5d742933c1f0b19836eae52fe9e4cbfccdd183aeb1f9b3b0afb3f46635bd639f" diff --git a/pyproject.toml b/pyproject.toml index bfaff11..2b8d5b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,134 @@ -[tool.poetry] +[build-system] +requires = [ + "setuptools", + "wheel", +] +build-backend = "setuptools.build_meta" + +[project] name = "mqpy" +authors = [ + {name = "Joao Euko"} +] version = "v0.6.9" description = "I developed this library to simplify the process of creating an Expert Advisor in MQL5. While developing in MQL5 can be complex, the same task is more streamlined in Python." -authors = ["Joao Paulo Euko"] -license = "MIT" +requires-python = ">=3.9" +dependencies = [] + readme = "README.md" +license = "MIT" -[tool.poetry.dependencies] -python = "^3.8" -metatrader5 = "^5.0.45" +[tool.setuptools.packages.find] +where = [] -[tool.poetry.group.dev.dependencies] -pre-commit = "^3.5.0" -black = ">=23.11,<25.0" -isort = "^5.12.0" +[tool.semantic_release] +version_variable = [ + "mqpy/version.py:__version__", +] +version_toml = [ + "pyproject.toml:project.version", +] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.semantic_release.changelog] +retain_old_entries = true + +[dependency-groups] +dev = [ + "pre-commit>=4.0.1", + "pylint>=3.3.3", + "pytest>=8.3.4", + "pytest-cov>=6.0.0", +] +docs = [ + "mkdocs>=1.6.1", + "mkdocs-gen-files>=0.5.0", + "mkdocs-jupyter>=0.25.1", + "mkdocs-literate-nav>=0.6.2", + "mkdocs-material>=9.6.12", + "mkdocs-section-index>=0.3.10", + "mkdocstrings[python]>=0.29.1", +] + +# pre-commit +[tool.pytest.ini_options] +markers = [] + +[tool.mypy] +warn_unused_configs = true +ignore_missing_imports = true +disable_error_code = [ + "misc", + "attr-defined", + "call-arg", + "name-defined", +] +show_error_codes = true +files = "**/*.py" + +[tool.ruff] +line-length = 120 +# Enable Pyflakes `E` and `F` codes by default. +lint.select = ["ALL"] +lint.ignore = [ + "COM812", # Missing trailing comma, conflicting with the formatter + "ISC001", # Single line string concatenation, conflicting with the formatter that does it automatically + "ANN002", # MissingTypeArgs + "ANN003", # MissingTypeKwargs + "ANN101", # MissingTypeSelf + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use an f-string literal, assign to variable first + "RET504", # Unnecessary variable assignment before `return` statement + "S301", # `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue + "PLR0913", # Too many arguments to function call + "PLR0915", # Too many statements + "PLE0605", # Invalid format for `__all__`, must be `tuple` or `list` + "PLR0912", # Too many branches + "G004", # Logging statement uses an f-string + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following this TODO + "TRY003", # Long messages outside exception class + "FIX", # Not allowed to use TODO + "DTZ007", # Naive datetime constructed without %z +] +# Exclude a variety of commonly ignored directories. +lint.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +lint.fixable = ["ALL"] +# Allow unused variables when underscore-prefixed. +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + +[tool.ruff.lint.per-file-ignores] +"conftest.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001"] +"test_*.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pylint] +allow-magic-value-types = [ + "int", + "str", +] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..a94caef --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts for the project.""" diff --git a/scripts/build_docs.sh b/scripts/build_docs.sh new file mode 100755 index 0000000..65e0093 --- /dev/null +++ b/scripts/build_docs.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Script to build documentation for MQPy + +# Export Python path to include the project root directory +export PYTHONPATH=$(pwd) + +# Install required packages if needed +if [ "$1" == "--install" ]; then + echo "Installing required packages..." + pip install mkdocs mkdocs-material mkdocstrings mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index mkdocstrings-python mkdocs-jupyter +fi + +# Create necessary directories +mkdir -p docs/reference +mkdir -p docs/css + +# Build the documentation +echo "Building documentation..." +mkdocs build + +# If the build was successful, optionally serve +if [ $? -eq 0 ]; then + echo "Documentation built successfully." + + if [ "$1" == "--serve" ] || [ "$2" == "--serve" ]; then + echo "Serving documentation at http://localhost:8000" + mkdocs serve + fi +else + echo "Error building documentation." +fi diff --git a/scripts/gen_ref_pages.py b/scripts/gen_ref_pages.py new file mode 100755 index 0000000..4888d70 --- /dev/null +++ b/scripts/gen_ref_pages.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +"""Generate the API reference pages for the MQPy package.""" + +import sys +from pathlib import Path + +import mkdocs_gen_files + +# Add the project root to the Python path so we can import mqpy +project_dir = Path(__file__).parent.parent +sys.path.insert(0, str(project_dir)) + +package_dir = project_dir / "mqpy" +package_name = "mqpy" + +# Create a navigation structure +nav = mkdocs_gen_files.Nav() + +# Ensure the reference directory exists +reference_dir = project_dir / "docs" / "reference" +reference_dir.mkdir(parents=True, exist_ok=True) + +# Create an index page with a better layout +index_path = Path("reference", "index.md") +with mkdocs_gen_files.open(index_path, "w") as index_file: + index_file.write("# API Reference\n\n") + index_file.write( + f"This section contains the complete API reference for all public modules and classes in {package_name}.\n\n" + ) + index_file.write("## Available Modules\n\n") + +# Create documentation for each module +for path in sorted(package_dir.glob("**/*.py")): + module_path = path.relative_to(project_dir).with_suffix("") + doc_path = path.relative_to(project_dir).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + # Skip __init__.py and __main__.py for individual pages + parts = module_path.parts + if parts[-1] in ["__init__", "__main__"]: + continue + + # Generate proper import path + import_path = ".".join(parts) + + # Create directory for the documentation + full_doc_path.parent.mkdir(parents=True, exist_ok=True) + + # Write the page content + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + fd.write("\n\n") + + # Write documentation for classes and functions + fd.write(f"::: {import_path}\n") + + # Create title case version of the module name for navigation + title_case_parts = list(parts) + title_case_parts[-1] = parts[-1].replace("_", " ").title() + + # Add to navigation with title case name + nav[title_case_parts] = doc_path.as_posix() + + # Update index file with simple links + with mkdocs_gen_files.open(index_path, "a") as index_file: + rel_path = doc_path.as_posix() + module_name = parts[-1] + title_case_name = module_name.replace("_", " ").title() + index_file.write(f"- [{title_case_name}]({rel_path})\n") + +# Generate and write the navigation file +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.write("# API Reference\n\n") + nav_file.writelines(nav.build_literate_nav()) diff --git a/setup.py b/setup.py index c99f6c6..70f70d4 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,9 @@ +"""Setup script for the mqpy package. + +This module contains the setup configuration for the mqpy package, which provides a Python interface +for creating Expert Advisors in MetaTrader 5. +""" + import pathlib import setuptools @@ -7,7 +13,10 @@ setuptools.setup( name="mqpy", version="v0.6.9", - description="I developed this library to simplify the process of creating an Expert Advisor in MQL5. While developing in MQL5 can be complex, the same task is more streamlined in Python.", + description=( + "A library to simplify the process of creating an Expert Advisor in MQL5. " + "It makes Python development more streamlined than MQL5." + ), author="Joao Paulo Euko", license="MIT", keywords=["metatrader5", "algotrading", "stock market"], diff --git a/tests/integration/test_mt5_connection.py b/tests/integration/test_mt5_connection.py new file mode 100644 index 0000000..e017f5c --- /dev/null +++ b/tests/integration/test_mt5_connection.py @@ -0,0 +1,43 @@ +"""Integration test for MetaTrader5 connection. + +This module tests the ability to establish a connection with MT5 platform. +""" + +import logging +import os +import sys +import time + +import MetaTrader5 as mt5 # noqa: N813 + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +console_handler = logging.StreamHandler(sys.stdout) +console_handler.setLevel(logging.INFO) + +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + +logger.info("Testing MT5 initialization...") + +success = False +for attempt in range(10): + if mt5.initialize( + login=int(os.getenv("MT5_LOGIN")), # type: ignore[arg-type] + password=os.getenv("MT5_PASSWORD"), + server=os.getenv("MT5_SERVER"), + path=os.getenv("MT5_PATH"), + ): + logger.info("MT5 initialized successfully") + mt5.shutdown() + success = True + break + else: + logger.info(f"Attempt {attempt+1}: Not ready yet, sleeping...") + time.sleep(5) + +if not success: + logger.info("Failed to initialize MT5 after waiting.") + mt5.shutdown() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..57d6fb5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1979 @@ +version = 1 +revision = 2 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload_time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload_time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/33/536530122a22a7504b159bccaf30a1f76aa19d23028bd8b5009eb9b2efea/astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550", size = 398731, upload_time = "2025-03-09T11:54:36.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/80/c749efbd8eef5ea77c7d6f1956e8fbfb51963b7f93ef79647afd4d9886e3/astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248", size = 275339, upload_time = "2025-03-09T11:54:34.489Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload_time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload_time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload_time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload_time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload_time = "2025-02-25T18:15:32.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload_time = "2025-02-25T16:53:14.607Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload_time = "2025-02-25T16:53:17.266Z" }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload_time = "2025-02-25T16:53:26.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload_time = "2025-02-25T16:53:28.167Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload_time = "2025-02-25T16:53:29.858Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload_time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload_time = "2024-10-29T18:30:38.186Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload_time = "2025-01-31T02:16:47.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload_time = "2025-01-31T02:16:45.015Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload_time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload_time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload_time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload_time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload_time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload_time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload_time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload_time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload_time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload_time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload_time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload_time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload_time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload_time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload_time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload_time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload_time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload_time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload_time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload_time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload_time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload_time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload_time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload_time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload_time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload_time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload_time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload_time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload_time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload_time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload_time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload_time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload_time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload_time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload_time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload_time = "2024-09-04T20:45:20.226Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload_time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload_time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload_time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload_time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload_time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload_time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload_time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload_time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload_time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload_time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload_time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload_time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload_time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload_time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload_time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload_time = "2024-12-24T18:10:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload_time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload_time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload_time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload_time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload_time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload_time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload_time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload_time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload_time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload_time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload_time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload_time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload_time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload_time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload_time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload_time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload_time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload_time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload_time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload_time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload_time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload_time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload_time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload_time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload_time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload_time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload_time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload_time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload_time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload_time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload_time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload_time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload_time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload_time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload_time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload_time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload_time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload_time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload_time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867, upload_time = "2024-12-24T18:12:10.438Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385, upload_time = "2024-12-24T18:12:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367, upload_time = "2024-12-24T18:12:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928, upload_time = "2024-12-24T18:12:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203, upload_time = "2024-12-24T18:12:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082, upload_time = "2024-12-24T18:12:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053, upload_time = "2024-12-24T18:12:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625, upload_time = "2024-12-24T18:12:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549, upload_time = "2024-12-24T18:12:24.163Z" }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945, upload_time = "2024-12-24T18:12:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595, upload_time = "2024-12-24T18:12:28.03Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453, upload_time = "2024-12-24T18:12:29.569Z" }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811, upload_time = "2024-12-24T18:12:30.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload_time = "2024-12-24T18:12:32.852Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210, upload_time = "2024-03-12T16:53:41.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload_time = "2024-03-12T16:53:39.226Z" }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload_time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload_time = "2025-03-30T20:34:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload_time = "2025-03-30T20:34:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload_time = "2025-03-30T20:34:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload_time = "2025-03-30T20:35:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload_time = "2025-03-30T20:35:02.307Z" }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload_time = "2025-03-30T20:35:04.141Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload_time = "2025-03-30T20:35:05.889Z" }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload_time = "2025-03-30T20:35:07.76Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload_time = "2025-03-30T20:35:09.144Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload_time = "2025-03-30T20:35:10.734Z" }, + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload_time = "2025-03-30T20:35:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload_time = "2025-03-30T20:35:14.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload_time = "2025-03-30T20:35:15.616Z" }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload_time = "2025-03-30T20:35:18.648Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload_time = "2025-03-30T20:35:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload_time = "2025-03-30T20:35:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload_time = "2025-03-30T20:35:23.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload_time = "2025-03-30T20:35:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload_time = "2025-03-30T20:35:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload_time = "2025-03-30T20:35:28.498Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload_time = "2025-03-30T20:35:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload_time = "2025-03-30T20:35:31.912Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload_time = "2025-03-30T20:35:33.455Z" }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload_time = "2025-03-30T20:35:35.354Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload_time = "2025-03-30T20:35:37.121Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload_time = "2025-03-30T20:35:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload_time = "2025-03-30T20:35:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload_time = "2025-03-30T20:35:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload_time = "2025-03-30T20:35:44.216Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload_time = "2025-03-30T20:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload_time = "2025-03-30T20:35:47.417Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload_time = "2025-03-30T20:35:49.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload_time = "2025-03-30T20:35:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload_time = "2025-03-30T20:35:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload_time = "2025-03-30T20:35:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload_time = "2025-03-30T20:35:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload_time = "2025-03-30T20:35:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload_time = "2025-03-30T20:35:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload_time = "2025-03-30T20:36:01.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload_time = "2025-03-30T20:36:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload_time = "2025-03-30T20:36:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload_time = "2025-03-30T20:36:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload_time = "2025-03-30T20:36:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload_time = "2025-03-30T20:36:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload_time = "2025-03-30T20:36:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload_time = "2025-03-30T20:36:13.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload_time = "2025-03-30T20:36:16.074Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload_time = "2025-03-30T20:36:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload_time = "2025-03-30T20:36:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload_time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377, upload_time = "2025-03-30T20:36:23.298Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803, upload_time = "2025-03-30T20:36:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561, upload_time = "2025-03-30T20:36:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488, upload_time = "2025-03-30T20:36:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589, upload_time = "2025-03-30T20:36:30.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366, upload_time = "2025-03-30T20:36:32.563Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591, upload_time = "2025-03-30T20:36:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572, upload_time = "2025-03-30T20:36:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966, upload_time = "2025-03-30T20:36:38.551Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852, upload_time = "2025-03-30T20:36:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload_time = "2025-03-30T20:36:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload_time = "2025-03-30T20:36:43.61Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload_time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/df/156df75a41aaebd97cee9d3870fe68f8001b6c1c4ca023e221cfce69bece/debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339", size = 2076510, upload_time = "2025-04-10T19:46:13.315Z" }, + { url = "https://files.pythonhosted.org/packages/69/cd/4fc391607bca0996db5f3658762106e3d2427beaef9bfd363fd370a3c054/debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79", size = 3559614, upload_time = "2025-04-10T19:46:14.647Z" }, + { url = "https://files.pythonhosted.org/packages/1a/42/4e6d2b9d63e002db79edfd0cb5656f1c403958915e0e73ab3e9220012eec/debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987", size = 5208588, upload_time = "2025-04-10T19:46:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/cc9e4e5faadc9d00df1a64a3c2d5c5f4b9df28196c39ada06361c5141f89/debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84", size = 5241043, upload_time = "2025-04-10T19:46:17.768Z" }, + { url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064, upload_time = "2025-04-10T19:46:19.486Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359, upload_time = "2025-04-10T19:46:21.192Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269, upload_time = "2025-04-10T19:46:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156, upload_time = "2025-04-10T19:46:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload_time = "2025-04-10T19:46:26.044Z" }, + { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload_time = "2025-04-10T19:46:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload_time = "2025-04-10T19:46:29.467Z" }, + { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload_time = "2025-04-10T19:46:31.538Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload_time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload_time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload_time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload_time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/6f/96ba96545f55b6a675afa08c96b42810de9b18c7ad17446bbec82762127a/debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f", size = 2077696, upload_time = "2025-04-10T19:46:46.817Z" }, + { url = "https://files.pythonhosted.org/packages/fa/84/f378a2dd837d94de3c85bca14f1db79f8fcad7e20b108b40d59da56a6d22/debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea", size = 3554846, upload_time = "2025-04-10T19:46:48.72Z" }, + { url = "https://files.pythonhosted.org/packages/db/52/88824fe5d6893f59933f664c6e12783749ab537a2101baf5c713164d8aa2/debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d", size = 5209350, upload_time = "2025-04-10T19:46:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/35/72e9399be24a04cb72cfe1284572c9fcd1d742c7fa23786925c18fa54ad8/debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123", size = 5241852, upload_time = "2025-04-10T19:46:52.022Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload_time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload_time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload_time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload_time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload_time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload_time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload_time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload_time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload_time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload_time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload_time = "2024-07-12T22:25:58.476Z" }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload_time = "2025-01-22T15:41:29.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload_time = "2025-01-22T15:41:25.929Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload_time = "2024-12-02T10:55:15.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload_time = "2024-12-02T10:55:07.599Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload_time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload_time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload_time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload_time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload_time = "2025-04-23T11:29:09.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload_time = "2025-04-23T11:29:07.145Z" }, +] + +[[package]] +name = "identify" +version = "2.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload_time = "2025-04-19T15:10:38.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload_time = "2025-04-19T15:10:36.701Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload_time = "2025-01-20T22:21:30.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload_time = "2025-01-20T22:21:29.177Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload_time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload_time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.36.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload_time = "2024-07-01T14:07:22.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload_time = "2024-07-01T14:07:19.603Z" }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "jedi", marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "stack-data", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload_time = "2023-11-27T09:58:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload_time = "2023-11-27T09:58:30.538Z" }, +] + +[[package]] +name = "ipython" +version = "8.36.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.10.*'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "jedi", marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, + { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "stack-data", marker = "python_full_version == '3.10.*'" }, + { name = "traitlets", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/9f/d9a73710df947b7804bd9d93509463fb3a89e0ddc99c9fcc67279cddbeb6/ipython-8.36.0.tar.gz", hash = "sha256:24658e9fe5c5c819455043235ba59cfffded4a35936eefceceab6b192f7092ff", size = 5604997, upload_time = "2025-04-25T18:03:38.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/d7/c1c9f371790b3a181e343c4815a361e5a0cc7d90ef6642d64ba5d05de289/ipython-8.36.0-py3-none-any.whl", hash = "sha256:12b913914d010dcffa2711505ec8be4bf0180742d97f1e5175e51f22086428c1", size = 831074, upload_time = "2025-04-25T18:03:34.951Z" }, +] + +[[package]] +name = "ipython" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/02/63a84444a7409b3c0acd1de9ffe524660e0e5d82ee473e78b45e5bfb64a4/ipython-9.2.0.tar.gz", hash = "sha256:62a9373dbc12f28f9feaf4700d052195bf89806279fc8ca11f3f54017d04751b", size = 4424394, upload_time = "2025-04-25T17:55:40.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/ce/5e897ee51b7d26ab4e47e5105e7368d40ce6cfae2367acdf3165396d50be/ipython-9.2.0-py3-none-any.whl", hash = "sha256:fef5e33c4a1ae0759e0bba5917c9db4eb8c53fee917b6a526bd973e1ca5159f6", size = 604277, upload_time = "2025-04-25T17:55:37.625Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload_time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload_time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload_time = "2025-02-26T21:13:16.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload_time = "2025-02-26T21:13:14.911Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload_time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload_time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload_time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload_time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload_time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload_time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload_time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload_time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload_time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629, upload_time = "2024-03-12T12:37:35.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965, upload_time = "2024-03-12T12:37:32.36Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload_time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload_time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupytext" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b5/b04008ae2f3b37f0a955d9232c729589bcf40819732cd08a54bce08feb83/jupytext-1.17.0.tar.gz", hash = "sha256:c74adf6d205a778f481189e986b6066af79bdeb69099f4138d933cc15c398fb6", size = 3746613, upload_time = "2025-04-05T02:38:06.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/46/c2fb92e01eb0423bae7fe91c3bf2ca994069f299a6455919f4a9a12960ed/jupytext-1.17.0-py3-none-any.whl", hash = "sha256:d75b7cd198b3640a12f9cdf4d610bb80c9f27a8c3318b00372f90d21466d40e1", size = 164106, upload_time = "2025-04-05T02:38:04.795Z" }, +] + +[[package]] +name = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload_time = "2025-04-11T14:42:50.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload_time = "2025-04-11T14:42:49.178Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload_time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload_time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload_time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload_time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload_time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload_time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload_time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload_time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload_time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload_time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload_time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload_time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload_time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload_time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload_time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload_time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload_time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload_time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload_time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload_time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload_time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload_time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload_time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload_time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload_time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload_time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload_time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload_time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload_time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload_time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload_time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload_time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload_time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload_time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload_time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload_time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload_time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload_time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload_time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload_time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload_time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload_time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload_time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload_time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload_time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload_time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload_time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload_time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload_time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload_time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload_time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload_time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload_time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload_time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload_time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload_time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload_time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload_time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload_time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload_time = "2024-10-18T15:21:52.974Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload_time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload_time = "2024-04-15T13:44:43.265Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload_time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload_time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload_time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload_time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload_time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload_time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload_time = "2025-03-19T14:27:24.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload_time = "2025-03-19T14:27:23.451Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload_time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload_time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355, upload_time = "2025-03-08T13:35:21.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047, upload_time = "2025-03-08T13:35:18.889Z" }, +] + +[[package]] +name = "mkdocs-gen-files" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/85/2d634462fd59136197d3126ca431ffb666f412e3db38fd5ce3a60566303e/mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc", size = 7539, upload_time = "2023-04-27T19:48:04.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/0f/1e55b3fd490ad2cecb6e7b31892d27cb9fc4218ec1dab780440ba8579e74/mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea", size = 8380, upload_time = "2023-04-27T19:48:07.059Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload_time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload_time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-jupyter" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupytext" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "nbconvert" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/23/6ffb8d2fd2117aa860a04c6fe2510b21bc3c3c085907ffdd851caba53152/mkdocs_jupyter-0.25.1.tar.gz", hash = "sha256:0e9272ff4947e0ec683c92423a4bfb42a26477c103ab1a6ab8277e2dcc8f7afe", size = 1626747, upload_time = "2024-10-15T14:56:32.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.1-py3-none-any.whl", hash = "sha256:3f679a857609885d322880e72533ef5255561bbfdb13cfee2a1e92ef4d4ad8d8", size = 1456197, upload_time = "2024-10-15T14:56:29.854Z" }, +] + +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload_time = "2025-03-18T21:53:09.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload_time = "2025-03-18T21:53:08.1Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/ef/25fc10dbbb8faeeeb10ed7734d84a347cd2ec5d7200733f11c5553c02608/mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473", size = 3951532, upload_time = "2025-04-17T10:40:41.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/00/592940f4d150327a4f455171b2c9d4c3be7779a88e18b0a086183fcd8f06/mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e", size = 8703654, upload_time = "2025-04-17T10:40:38.304Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload_time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload_time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-section-index" +version = "0.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/40/4aa9d3cfa2ac6528b91048847a35f005b97ec293204c02b179762a85b7f2/mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", size = 14446, upload_time = "2025-04-05T20:56:45.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/53/76c109e6f822a6d19befb0450c87330b9a6ce52353de6a9dda7892060a1f/mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776", size = 8796, upload_time = "2025-04-05T20:56:43.975Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload_time = "2025-03-31T08:33:11.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload_time = "2025-03-31T08:33:09.661Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771, upload_time = "2025-04-03T14:24:48.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112, upload_time = "2025-04-03T14:24:46.561Z" }, +] + +[[package]] +name = "mqpy" +version = "0.6.9" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-literate-nav" }, + { name = "mkdocs-material" }, + { name = "mkdocs-section-index" }, + { name = "mkdocstrings", extra = ["python"] }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pylint", specifier = ">=3.3.3" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, +] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, + { name = "mkdocs-literate-nav", specifier = ">=0.6.2" }, + { name = "mkdocs-material", specifier = ">=9.6.12" }, + { name = "mkdocs-section-index", specifier = ">=0.3.10" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.1" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload_time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload_time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload_time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload_time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload_time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload_time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload_time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload_time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload_time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload_time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload_time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload_time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload_time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload_time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload_time = "2024-04-05T09:43:55.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload_time = "2024-04-05T09:43:53.299Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload_time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload_time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload_time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload_time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload_time = "2025-03-19T20:36:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload_time = "2025-03-19T20:36:09.038Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload_time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload_time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload_time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload_time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload_time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload_time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload_time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload_time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload_time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload_time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload_time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload_time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload_time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload_time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload_time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload_time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload_time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pylint" +version = "3.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomlkit" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/a7/113d02340afb9dcbb0c8b25454e9538cd08f0ebf3e510df4ed916caa1a89/pylint-3.3.6.tar.gz", hash = "sha256:b634a041aac33706d56a0d217e6587228c66427e20ec21a019bc4cdee48c040a", size = 1519586, upload_time = "2025-03-20T11:25:38.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/21/9537fc94aee9ec7316a230a49895266cf02d78aa29b0a2efbc39566e0935/pylint-3.3.6-py3-none-any.whl", hash = "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6", size = 522462, upload_time = "2025-03-20T11:25:36.13Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846, upload_time = "2025-02-01T15:43:15.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467, upload_time = "2025-02-01T15:43:13.995Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload_time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload_time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload_time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload_time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload_time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload_time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240, upload_time = "2025-03-17T00:55:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854, upload_time = "2025-03-17T00:55:48.783Z" }, + { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963, upload_time = "2025-03-17T00:55:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload_time = "2025-03-17T00:55:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload_time = "2025-03-17T00:55:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload_time = "2025-03-17T00:55:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload_time = "2025-03-17T00:55:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload_time = "2025-03-17T00:56:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload_time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload_time = "2025-03-17T00:56:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload_time = "2025-03-17T00:56:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload_time = "2025-03-17T00:56:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cd/d09d434630edb6a0c44ad5079611279a67530296cfe0451e003de7f449ff/pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a", size = 8848099, upload_time = "2025-03-17T00:55:42.415Z" }, + { url = "https://files.pythonhosted.org/packages/93/ff/2a8c10315ffbdee7b3883ac0d1667e267ca8b3f6f640d81d43b87a82c0c7/pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475", size = 9602031, upload_time = "2025-03-17T00:55:44.512Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload_time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload_time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload_time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload_time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload_time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload_time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload_time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload_time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload_time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload_time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload_time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload_time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload_time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload_time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload_time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload_time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload_time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload_time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload_time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload_time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload_time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload_time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload_time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload_time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload_time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload_time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload_time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload_time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload_time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload_time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload_time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload_time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload_time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload_time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload_time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload_time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload_time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload_time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload_time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload_time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload_time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload_time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload_time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload_time = "2024-08-06T20:33:49.073Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload_time = "2020-11-12T02:38:26.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload_time = "2020-11-12T02:38:24.638Z" }, +] + +[[package]] +name = "pyzmq" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload_time = "2025-04-04T12:05:44.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b8/af1d814ffc3ff9730f9a970cbf216b6f078e5d251a25ef5201d7bc32a37c/pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918", size = 1339238, upload_time = "2025-04-04T12:03:07.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e4/5aafed4886c264f2ea6064601ad39c5fc4e9b6539c6ebe598a859832eeee/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315", size = 672848, upload_time = "2025-04-04T12:03:08.591Z" }, + { url = "https://files.pythonhosted.org/packages/79/39/026bf49c721cb42f1ef3ae0ee3d348212a7621d2adb739ba97599b6e4d50/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b", size = 911299, upload_time = "2025-04-04T12:03:10Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/b41f936a9403b8f92325c823c0f264c6102a0687a99c820f1aaeb99c1def/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4", size = 867920, upload_time = "2025-04-04T12:03:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3e/2de5928cdadc2105e7c8f890cc5f404136b41ce5b6eae5902167f1d5641c/pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f", size = 862514, upload_time = "2025-04-04T12:03:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/ce/57/109569514dd32e05a61d4382bc88980c95bfd2f02e58fea47ec0ccd96de1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5", size = 1204494, upload_time = "2025-04-04T12:03:14.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/dc51068ff2ca70350d1151833643a598625feac7b632372d229ceb4de3e1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a", size = 1514525, upload_time = "2025-04-04T12:03:16.246Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/a7d81873fff0645eb60afaec2b7c78a85a377af8f1d911aff045d8955bc7/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b", size = 1414659, upload_time = "2025-04-04T12:03:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/813af9c42ae21845c1ccfe495bd29c067622a621e85d7cda6bc437de8101/pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980", size = 580348, upload_time = "2025-04-04T12:03:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/20/68/318666a89a565252c81d3fed7f3b4c54bd80fd55c6095988dfa2cd04a62b/pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b", size = 643838, upload_time = "2025-04-04T12:03:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/91/f8/fb1a15b5f4ecd3e588bfde40c17d32ed84b735195b5c7d1d7ce88301a16f/pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5", size = 559565, upload_time = "2025-04-04T12:03:22.676Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/234e3b0aa82fd0290b1896e9992f56bdddf1f97266110be54d0177a9d2d9/pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54", size = 1339723, upload_time = "2025-04-04T12:03:24.358Z" }, + { url = "https://files.pythonhosted.org/packages/4f/11/6d561efe29ad83f7149a7cd48e498e539ed09019c6cd7ecc73f4cc725028/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030", size = 672645, upload_time = "2025-04-04T12:03:25.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/fd/81bfe3e23f418644660bad1a90f0d22f0b3eebe33dd65a79385530bceb3d/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01", size = 910133, upload_time = "2025-04-04T12:03:27.625Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/321b9c775595ea3df832a9516252b653fe32818db66fdc8fa31c9b9fce37/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e", size = 867428, upload_time = "2025-04-04T12:03:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/159cbf2055ef36aa2aa297e01b24523176e5b48ead283c23a94179fb2ba2/pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88", size = 862409, upload_time = "2025-04-04T12:03:31.032Z" }, + { url = "https://files.pythonhosted.org/packages/05/1c/45fb8db7be5a7d0cadea1070a9cbded5199a2d578de2208197e592f219bd/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6", size = 1205007, upload_time = "2025-04-04T12:03:32.687Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fa/658c7f583af6498b463f2fa600f34e298e1b330886f82f1feba0dc2dd6c3/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df", size = 1514599, upload_time = "2025-04-04T12:03:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/44d641522353ce0a2bbd150379cb5ec32f7120944e6bfba4846586945658/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef", size = 1414546, upload_time = "2025-04-04T12:03:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/c8ed7263218b3d1e9bce07b9058502024188bd52cc0b0a267a9513b431fc/pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca", size = 579247, upload_time = "2025-04-04T12:03:36.846Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d0/2d9abfa2571a0b1a67c0ada79a8aa1ba1cce57992d80f771abcdf99bb32c/pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896", size = 644727, upload_time = "2025-04-04T12:03:38.578Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d1/c8ad82393be6ccedfc3c9f3adb07f8f3976e3c4802640fe3f71441941e70/pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3", size = 559942, upload_time = "2025-04-04T12:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586, upload_time = "2025-04-04T12:03:41.954Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880, upload_time = "2025-04-04T12:03:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216, upload_time = "2025-04-04T12:03:45.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814, upload_time = "2025-04-04T12:03:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889, upload_time = "2025-04-04T12:03:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153, upload_time = "2025-04-04T12:03:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352, upload_time = "2025-04-04T12:03:52.473Z" }, + { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834, upload_time = "2025-04-04T12:03:54Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992, upload_time = "2025-04-04T12:03:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466, upload_time = "2025-04-04T12:03:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342, upload_time = "2025-04-04T12:03:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484, upload_time = "2025-04-04T12:04:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106, upload_time = "2025-04-04T12:04:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056, upload_time = "2025-04-04T12:04:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148, upload_time = "2025-04-04T12:04:05.581Z" }, + { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983, upload_time = "2025-04-04T12:04:07.096Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274, upload_time = "2025-04-04T12:04:08.523Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120, upload_time = "2025-04-04T12:04:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738, upload_time = "2025-04-04T12:04:12.509Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826, upload_time = "2025-04-04T12:04:14.289Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406, upload_time = "2025-04-04T12:04:15.757Z" }, + { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216, upload_time = "2025-04-04T12:04:17.212Z" }, + { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769, upload_time = "2025-04-04T12:04:18.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826, upload_time = "2025-04-04T12:04:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650, upload_time = "2025-04-04T12:04:22.413Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776, upload_time = "2025-04-04T12:04:23.959Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516, upload_time = "2025-04-04T12:04:25.449Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183, upload_time = "2025-04-04T12:04:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501, upload_time = "2025-04-04T12:04:28.833Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540, upload_time = "2025-04-04T12:04:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/06/91/21d3af57bc77e86e9d1e5384f256fd25cdb4c8eed4c45c8119da8120915f/pyzmq-26.4.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:a88643de8abd000ce99ca72056a1a2ae15881ee365ecb24dd1d9111e43d57842", size = 1340634, upload_time = "2025-04-04T12:04:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/58cd825023e998a0e49db7322b3211e6cf93f0796710b77d1496304c10d1/pyzmq-26.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a744ce209ecb557406fb928f3c8c55ce79b16c3eeb682da38ef5059a9af0848", size = 907880, upload_time = "2025-04-04T12:04:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/619e44a766ef738cb7e8ed8e5a54565627801bdb027ca6dfb70762385617/pyzmq-26.4.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9434540f333332224ecb02ee6278b6c6f11ea1266b48526e73c903119b2f420f", size = 863003, upload_time = "2025-04-04T12:04:51Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/a59af31320598bdc63d2c5a3181d14a89673c2c794540678285482e8a342/pyzmq-26.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c6f0a23e55cd38d27d4c89add963294ea091ebcb104d7fdab0f093bc5abb1c", size = 673432, upload_time = "2025-04-04T12:04:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/29/ae/64dd6c18b08ce2cb009c60f11cf01c87f323acd80344d8b059c0304a7370/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6145df55dc2309f6ef72d70576dcd5aabb0fd373311613fe85a5e547c722b780", size = 1205221, upload_time = "2025-04-04T12:04:54.31Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0b/c583ab750957b025244a66948831bc9ca486d11c820da4626caf6480ee1a/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2ea81823840ef8c56e5d2f9918e4d571236294fea4d1842b302aebffb9e40997", size = 1515299, upload_time = "2025-04-04T12:04:56.063Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/95ba76292c49dd9c6dff1f127b4867033020b708d101cba6e4fc5a3d166d/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc2abc385dc37835445abe206524fbc0c9e3fce87631dfaa90918a1ba8f425eb", size = 1415366, upload_time = "2025-04-04T12:04:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/6e/65/51abe36169effda26ac7400ffac96f463e09dff40d344cdc2629d9a59162/pyzmq-26.4.0-cp39-cp39-win32.whl", hash = "sha256:41a2508fe7bed4c76b4cf55aacfb8733926f59d440d9ae2b81ee8220633b4d12", size = 580773, upload_time = "2025-04-04T12:04:59.786Z" }, + { url = "https://files.pythonhosted.org/packages/89/68/d9ac94086c63a0ed8d73e9e8aec54b39f481696698a5a939a7207629fb30/pyzmq-26.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4000e8255d6cbce38982e5622ebb90823f3409b7ffe8aeae4337ef7d6d2612a", size = 644340, upload_time = "2025-04-04T12:05:01.389Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8f/66c261d657c1b0791ee5b372c90b1646b453adb581fcdc1dc5c94e5b03e3/pyzmq-26.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f6919d9c120488246bdc2a2f96662fa80d67b35bd6d66218f457e722b3ff64", size = 560075, upload_time = "2025-04-04T12:05:02.975Z" }, + { url = "https://files.pythonhosted.org/packages/47/03/96004704a84095f493be8d2b476641f5c967b269390173f85488a53c1c13/pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba", size = 834408, upload_time = "2025-04-04T12:05:04.569Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7f/68d8f3034a20505db7551cb2260248be28ca66d537a1ac9a257913d778e4/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b", size = 569580, upload_time = "2025-04-04T12:05:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a6/2b0d6801ec33f2b2a19dd8d02e0a1e8701000fec72926e6787363567d30c/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94", size = 798250, upload_time = "2025-04-04T12:05:07.88Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/0322b3437de977dcac8a755d6d7ce6ec5238de78e2e2d9353730b297cf12/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a", size = 756758, upload_time = "2025-04-04T12:05:09.483Z" }, + { url = "https://files.pythonhosted.org/packages/c2/33/43704f066369416d65549ccee366cc19153911bec0154da7c6b41fca7e78/pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb", size = 555371, upload_time = "2025-04-04T12:05:11.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/52/a70fcd5592715702248306d8e1729c10742c2eac44529984413b05c68658/pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb", size = 834405, upload_time = "2025-04-04T12:05:13.3Z" }, + { url = "https://files.pythonhosted.org/packages/25/f9/1a03f1accff16b3af1a6fa22cbf7ced074776abbf688b2e9cb4629700c62/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1", size = 569578, upload_time = "2025-04-04T12:05:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/76/0c/3a633acd762aa6655fcb71fa841907eae0ab1e8582ff494b137266de341d/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494", size = 798248, upload_time = "2025-04-04T12:05:17.376Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/6c99c84aa60ac1cc56747bed6be8ce6305b9b861d7475772e7a25ce019d3/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9", size = 756757, upload_time = "2025-04-04T12:05:19.19Z" }, + { url = "https://files.pythonhosted.org/packages/13/9c/d8073bd898eb896e94c679abe82e47506e2b750eb261cf6010ced869797c/pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", size = 555371, upload_time = "2025-04-04T12:05:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/71a644b629e1a93ccae9e22a45aec9d23065dfcc24c399cb837f81cd08c2/pyzmq-26.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:552b0d2e39987733e1e9e948a0ced6ff75e0ea39ab1a1db2fc36eb60fd8760db", size = 834397, upload_time = "2025-04-04T12:05:31.217Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dd/052a25651eaaff8f5fd652fb40a3abb400e71207db2d605cf6faf0eac598/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd670a8aa843f2ee637039bbd412e0d7294a5e588e1ecc9ad98b0cdc050259a4", size = 569571, upload_time = "2025-04-04T12:05:32.877Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5d/201ca10b5d12ab187a418352c06d70c3e2087310af038b11056aba1359be/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d367b7b775a0e1e54a59a2ba3ed4d5e0a31566af97cc9154e34262777dab95ed", size = 798243, upload_time = "2025-04-04T12:05:34.91Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d4/2c64e54749536ad1633400f28d71e71e19375d00ce1fe9bb1123364dc927/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112af16c406e4a93df2caef49f884f4c2bb2b558b0b5577ef0b2465d15c1abc", size = 756751, upload_time = "2025-04-04T12:05:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/34d119af43d06a8dcd88bf7a62dac69597eaba52b49ecce76ff06b40f1fd/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76c298683f82669cab0b6da59071f55238c039738297c69f187a542c6d40099", size = 745400, upload_time = "2025-04-04T12:05:40.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/b5e471d74a63318e51f30d329b17d2550bdededaab55baed2e2499de7ce4/pyzmq-26.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:49b6ca2e625b46f499fb081aaf7819a177f41eeb555acb05758aa97f4f95d147", size = 555367, upload_time = "2025-04-04T12:05:42.356Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload_time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload_time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863, upload_time = "2025-03-26T14:56:01.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/cbc43b220c9deb536b07fbd598c97d463bbb7afb788851891252fc920742/rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724", size = 377531, upload_time = "2025-03-26T14:52:41.754Z" }, + { url = "https://files.pythonhosted.org/packages/42/15/cc4b09ef160483e49c3aab3b56f3d375eadf19c87c48718fb0147e86a446/rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b", size = 362273, upload_time = "2025-03-26T14:52:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a2/67718a188a88dbd5138d959bed6efe1cc7413a4caa8283bd46477ed0d1ad/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727", size = 388111, upload_time = "2025-03-26T14:52:46.944Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e6/cbf1d3163405ad5f4a1a6d23f80245f2204d0c743b18525f34982dec7f4d/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964", size = 394447, upload_time = "2025-03-26T14:52:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/21/bb/4fe220ccc8a549b38b9e9cec66212dc3385a82a5ee9e37b54411cce4c898/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5", size = 448028, upload_time = "2025-03-26T14:52:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/a5/41/d2d6e0fd774818c4cadb94185d30cf3768de1c2a9e0143fc8bc6ce59389e/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664", size = 447410, upload_time = "2025-03-26T14:52:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/6d04d438f53d8bb2356bb000bea9cf5c96a9315e405b577117e344cc7404/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc", size = 389531, upload_time = "2025-03-26T14:52:54.233Z" }, + { url = "https://files.pythonhosted.org/packages/23/be/72e6df39bd7ca5a66799762bf54d8e702483fdad246585af96723109d486/rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0", size = 420099, upload_time = "2025-03-26T14:52:56.135Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c9/ca100cd4688ee0aa266197a5cb9f685231676dd7d573041ca53787b23f4e/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f", size = 564950, upload_time = "2025-03-26T14:52:57.583Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/908cd95686d33b3ac8ac2e582d7ae38e2c3aa2c0377bf1f5663bafd1ffb2/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f", size = 591778, upload_time = "2025-03-26T14:52:59.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/e143726f1dd3215efcb974b50b03bd08a8a1556b404a0a7872af6d197e57/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875", size = 560421, upload_time = "2025-03-26T14:53:01.422Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/add1c1d2fcd5aa354f7225d036d4492261759a22d449cff14841ef36a514/rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07", size = 222089, upload_time = "2025-03-26T14:53:02.859Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/81f8066c6de44c507caca488ba336ae30d35d57f61fe10578824d1a70196/rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052", size = 234622, upload_time = "2025-03-26T14:53:04.676Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/c1458bbfb257448fdb2528071f1f4e19e26798ed5ef6d47d7aab0cb69661/rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef", size = 377679, upload_time = "2025-03-26T14:53:06.557Z" }, + { url = "https://files.pythonhosted.org/packages/dd/26/ea4181ef78f58b2c167548c6a833d7dc22408e5b3b181bda9dda440bb92d/rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97", size = 362571, upload_time = "2025-03-26T14:53:08.439Z" }, + { url = "https://files.pythonhosted.org/packages/56/fa/1ec54dd492c64c280a2249a047fc3369e2789dc474eac20445ebfc72934b/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e", size = 388012, upload_time = "2025-03-26T14:53:10.314Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/bad8b0e0f7e58ef4973bb75e91c472a7d51da1977ed43b09989264bf065c/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d", size = 394730, upload_time = "2025-03-26T14:53:11.953Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/ab417fc90c21826df048fc16e55316ac40876e4b790104ececcbce813d8f/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586", size = 448264, upload_time = "2025-03-26T14:53:13.42Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/4c63862d5c05408589196c8440a35a14ea4ae337fa70ded1f03638373f06/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4", size = 446813, upload_time = "2025-03-26T14:53:15.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0c/91cf17dffa9a38835869797a9f041056091ebba6a53963d3641207e3d467/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae", size = 389438, upload_time = "2025-03-26T14:53:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b0/60e6c72727c978276e02851819f3986bc40668f115be72c1bc4d922c950f/rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc", size = 420416, upload_time = "2025-03-26T14:53:18.671Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d7/f46f85b9f863fb59fd3c534b5c874c48bee86b19e93423b9da8784605415/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c", size = 565236, upload_time = "2025-03-26T14:53:20.357Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d1/1467620ded6dd70afc45ec822cdf8dfe7139537780d1f3905de143deb6fd/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c", size = 592016, upload_time = "2025-03-26T14:53:22.216Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/fb1ded2e6adfaa0c0833106c42feb290973f665300f4facd5bf5d7891d9c/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718", size = 560123, upload_time = "2025-03-26T14:53:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/df/09fc1857ac7cc2eb16465a7199c314cbce7edde53c8ef21d615410d7335b/rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a", size = 222256, upload_time = "2025-03-26T14:53:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/ff/25/939b40bc4d54bf910e5ee60fb5af99262c92458f4948239e8c06b0b750e7/rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6", size = 234718, upload_time = "2025-03-26T14:53:26.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945, upload_time = "2025-03-26T14:53:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935, upload_time = "2025-03-26T14:53:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817, upload_time = "2025-03-26T14:53:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983, upload_time = "2025-03-26T14:53:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719, upload_time = "2025-03-26T14:53:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546, upload_time = "2025-03-26T14:53:36.26Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695, upload_time = "2025-03-26T14:53:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218, upload_time = "2025-03-26T14:53:39.326Z" }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062, upload_time = "2025-03-26T14:53:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262, upload_time = "2025-03-26T14:53:42.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306, upload_time = "2025-03-26T14:53:44.2Z" }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281, upload_time = "2025-03-26T14:53:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719, upload_time = "2025-03-26T14:53:47.187Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072, upload_time = "2025-03-26T14:53:48.686Z" }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919, upload_time = "2025-03-26T14:53:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360, upload_time = "2025-03-26T14:53:51.909Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704, upload_time = "2025-03-26T14:53:53.47Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839, upload_time = "2025-03-26T14:53:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494, upload_time = "2025-03-26T14:53:57.047Z" }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185, upload_time = "2025-03-26T14:53:59.032Z" }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168, upload_time = "2025-03-26T14:54:00.661Z" }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622, upload_time = "2025-03-26T14:54:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435, upload_time = "2025-03-26T14:54:04.388Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762, upload_time = "2025-03-26T14:54:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510, upload_time = "2025-03-26T14:54:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075, upload_time = "2025-03-26T14:54:09.992Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974, upload_time = "2025-03-26T14:54:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730, upload_time = "2025-03-26T14:54:13.145Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627, upload_time = "2025-03-26T14:54:14.711Z" }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094, upload_time = "2025-03-26T14:54:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639, upload_time = "2025-03-26T14:54:19.047Z" }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584, upload_time = "2025-03-26T14:54:20.722Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047, upload_time = "2025-03-26T14:54:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085, upload_time = "2025-03-26T14:54:23.949Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498, upload_time = "2025-03-26T14:54:25.573Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202, upload_time = "2025-03-26T14:54:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771, upload_time = "2025-03-26T14:54:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195, upload_time = "2025-03-26T14:54:31.581Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354, upload_time = "2025-03-26T14:54:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/22/ef/a194eaef0d0f2cd3f4c893c5b809a7458aaa7c0a64e60a45a72a04835ed4/rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d", size = 378126, upload_time = "2025-03-26T14:54:35.094Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/9a07f69933204c098760c884f03835ab8fb66e28d2d5f3dd6741720cf29c/rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e", size = 362887, upload_time = "2025-03-26T14:54:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/29/74/315f42060f2e3cedd77d382a98484a68ef727bd3b5fd7b91825b859a3e85/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65", size = 388661, upload_time = "2025-03-26T14:54:38.323Z" }, + { url = "https://files.pythonhosted.org/packages/29/22/7ee7bb2b25ecdfcf1265d5a51472814fe60b580f9e1e2746eed9c476310a/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b", size = 394993, upload_time = "2025-03-26T14:54:39.924Z" }, + { url = "https://files.pythonhosted.org/packages/46/7b/5f40e278d81cd23eea6b88bbac62bacc27ed19412051a1fc4229e8f9367a/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791", size = 448706, upload_time = "2025-03-26T14:54:41.673Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7a/06aada7ecdb0d02fbc041daee998ae841882fcc8ed3c0f84e72d6832fef1/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9", size = 447369, upload_time = "2025-03-26T14:54:43.308Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f3/428a9367077268f852db9b3b68b6eda6ee4594ab7dc2d603a2c370619cc0/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c", size = 390012, upload_time = "2025-03-26T14:54:45.109Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/24b61f14cd54e525583404afe6e3c221b309d1abd4b0b597a566dd8ee42d/rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58", size = 421576, upload_time = "2025-03-26T14:54:47.125Z" }, + { url = "https://files.pythonhosted.org/packages/22/56/18b81a4f0550e0d4be700cdcf1415ebf250fd21f9a5a775843dd3588dbf6/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124", size = 565562, upload_time = "2025-03-26T14:54:48.785Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/82a935d78f74974f82d38e83fb02430f8e8cc09ad35e06d9a5d2e9b907a7/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149", size = 592924, upload_time = "2025-03-26T14:54:50.493Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/b717e7b93c2ca881d2dac8b23b3a87a4c30f7c762bfd3df0b3953e655f13/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45", size = 560847, upload_time = "2025-03-26T14:54:52.238Z" }, + { url = "https://files.pythonhosted.org/packages/1e/26/ba630a291238e7f42d25bc5569d152623f18c21e9183e506585b23325c48/rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103", size = 222570, upload_time = "2025-03-26T14:54:54.713Z" }, + { url = "https://files.pythonhosted.org/packages/2d/84/01126e25e21f2ed6e63ec4030f78793dfee1a21aff1842136353c9caaed9/rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f", size = 234931, upload_time = "2025-03-26T14:54:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/99/48/11dae46d0c7f7e156ca0971a83f89c510af0316cd5d42c771b7cef945f0c/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a", size = 378224, upload_time = "2025-03-26T14:54:58.78Z" }, + { url = "https://files.pythonhosted.org/packages/33/18/e8398d255369e35d312942f3bb8ecaff013c44968904891be2ab63b3aa94/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399", size = 363252, upload_time = "2025-03-26T14:55:00.359Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/dd73ba691f4df3e6834bf982de214086ac3359ab3ac035adfb30041570e3/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098", size = 388871, upload_time = "2025-03-26T14:55:02.253Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/da0530b25cabd0feca2a759b899d2df325069a94281eeea8ac44c6cfeff7/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d", size = 394766, upload_time = "2025-03-26T14:55:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ee/dd1c5040a431beb40fad4a5d7868acf343444b0bc43e627c71df2506538b/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e", size = 448712, upload_time = "2025-03-26T14:55:06.03Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ec/6b93ffbb686be948e4d91ec76f4e6757f8551034b2a8176dd848103a1e34/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1", size = 447150, upload_time = "2025-03-26T14:55:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/55/d5/a1c23760adad85b432df074ced6f910dd28f222b8c60aeace5aeb9a6654e/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb", size = 390662, upload_time = "2025-03-26T14:55:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f3/419cb1f9bfbd3a48c256528c156e00f3349e3edce5ad50cbc141e71f66a5/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44", size = 421351, upload_time = "2025-03-26T14:55:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/98/8e/62d1a55078e5ede0b3b09f35e751fa35924a34a0d44d7c760743383cd54a/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33", size = 566074, upload_time = "2025-03-26T14:55:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/fc/69/b7d1003166d78685da032b3c4ff1599fa536a3cfe6e5ce2da87c9c431906/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164", size = 592398, upload_time = "2025-03-26T14:55:15.202Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/1c98bc99338c37faadd28dd667d336df7409d77b4da999506a0b6b1c0aa2/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc", size = 561114, upload_time = "2025-03-26T14:55:17.072Z" }, + { url = "https://files.pythonhosted.org/packages/2b/41/65c91443685a4c7b5f1dd271beadc4a3e063d57c3269221548dd9416e15c/rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5", size = 235548, upload_time = "2025-03-26T14:55:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/40bcc246a8354530d51a26d2b5b9afd1deacfb0d79e67295cc74df362f52/rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d", size = 378386, upload_time = "2025-03-26T14:55:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/80/b0/5ea97dd2f53e3618560aa1f9674e896e63dff95a9b796879a201bc4c1f00/rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a", size = 363440, upload_time = "2025-03-26T14:55:22.121Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/259b6eada6f747cdd60c9a5eb3efab15f6704c182547149926c38e5bd0d5/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5", size = 388816, upload_time = "2025-03-26T14:55:23.737Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/faafc7183712f89f4b7620c3c15979ada13df137d35ef3011ae83e93b005/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d", size = 395058, upload_time = "2025-03-26T14:55:25.468Z" }, + { url = "https://files.pythonhosted.org/packages/6c/96/d7fa9d2a7b7604a61da201cc0306a355006254942093779d7121c64700ce/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793", size = 448692, upload_time = "2025-03-26T14:55:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/96/37/a3146c6eebc65d6d8c96cc5ffdcdb6af2987412c789004213227fbe52467/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba", size = 446462, upload_time = "2025-03-26T14:55:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/6481dfd9ac7de43acdaaa416e3a7da40bc4bb8f5c6ca85e794100aa54596/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea", size = 390460, upload_time = "2025-03-26T14:55:31.017Z" }, + { url = "https://files.pythonhosted.org/packages/61/e1/37e36bce65e109543cc4ff8d23206908649023549604fa2e7fbeba5342f7/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032", size = 421609, upload_time = "2025-03-26T14:55:32.84Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/1f1a923d6cd798b8582176aca8a0784676f1a0449fb6f07fce6ac1cdbfb6/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d", size = 565818, upload_time = "2025-03-26T14:55:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d8da6df6a1eb3a418944a17b1cb38dd430b9e5a2e972eafd2b06f10c7c46/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25", size = 592627, upload_time = "2025-03-26T14:55:36.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/14/c492b9c7d5dd133e13f211ddea6bb9870f99e4f73932f11aa00bc09a9be9/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba", size = 560885, upload_time = "2025-03-26T14:55:38Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/16cbbd7aaa4deaaeef5c90fee8b485c8b3312094cdad31e8006f5a3e5e08/rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6", size = 378245, upload_time = "2025-03-26T14:55:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8c/5024dd105bf0a515576b7df8aeeba6556ffdbe2d636dee172c1a30497dd1/rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb", size = 363461, upload_time = "2025-03-26T14:55:41.441Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6f/3a4efcfa2f4391b69f5d0ed3e6be5d2c5468c24fd2d15b712d2dbefc1749/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1", size = 388839, upload_time = "2025-03-26T14:55:43.566Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/b8e5f0a0e97d295a0ebceb5265ef2e44c3d55e0d0f938d64a5ecfffa715e/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83", size = 394860, upload_time = "2025-03-26T14:55:45.301Z" }, + { url = "https://files.pythonhosted.org/packages/90/e9/9f1f297bdbc5b871826ad790b6641fc40532d97917916e6bd9f87fdd128d/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046", size = 449314, upload_time = "2025-03-26T14:55:47.043Z" }, + { url = "https://files.pythonhosted.org/packages/06/ad/62ddbbaead31a1a22f0332958d0ea7c7aeed1b2536c6a51dd66dfae321a2/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391", size = 446376, upload_time = "2025-03-26T14:55:48.757Z" }, + { url = "https://files.pythonhosted.org/packages/82/a7/05b660d2f3789506e98be69aaf2ccde94e0fc49cd26cd78d7069bc5ba1b8/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3", size = 390560, upload_time = "2025-03-26T14:55:50.489Z" }, + { url = "https://files.pythonhosted.org/packages/66/1b/79fa0abffb802ff817821a148ce752eaaab87ba3a6a5e6b9f244c00c73d0/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78", size = 421225, upload_time = "2025-03-26T14:55:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/368893ad2f7b2ece42cad87c7ec71309b5d93188db28b307eadb48cd28e5/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3", size = 566071, upload_time = "2025-03-26T14:55:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/1cd0a654d300449411e6fd0821f83c1cfc7223da2e8109f586b4d9b89054/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd", size = 592334, upload_time = "2025-03-26T14:55:56.547Z" }, + { url = "https://files.pythonhosted.org/packages/31/33/5905e2a2e7612218e25307a9255fc8671b977449d40d62fe317775fe4939/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796", size = 561111, upload_time = "2025-03-26T14:55:58.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/bd/f4cc34ac2261a7cb8a48bc90ce1e36dc05f1ec5ac3b4537def20be5df555/rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f", size = 235168, upload_time = "2025-03-26T14:56:00.035Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload_time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload_time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload_time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload_time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload_time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload_time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload_time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload_time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload_time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload_time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload_time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload_time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload_time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload_time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload_time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload_time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload_time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload_time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload_time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload_time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload_time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload_time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload_time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload_time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload_time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload_time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload_time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload_time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload_time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload_time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload_time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload_time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload_time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload_time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload_time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload_time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload_time = "2024-08-14T08:19:41.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload_time = "2024-08-14T08:19:40.05Z" }, +] + +[[package]] +name = "tornado" +version = "6.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135, upload_time = "2024-11-22T03:06:38.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299, upload_time = "2024-11-22T03:06:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253, upload_time = "2024-11-22T03:06:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602, upload_time = "2024-11-22T03:06:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972, upload_time = "2024-11-22T03:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173, upload_time = "2024-11-22T03:06:27.584Z" }, + { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892, upload_time = "2024-11-22T03:06:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334, upload_time = "2024-11-22T03:06:30.428Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261, upload_time = "2024-11-22T03:06:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463, upload_time = "2024-11-22T03:06:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907, upload_time = "2024-11-22T03:06:36.71Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload_time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload_time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945, upload_time = "2025-03-31T16:33:29.185Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461, upload_time = "2025-03-31T16:33:26.758Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload_time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload_time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload_time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload_time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload_time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload_time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload_time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload_time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload_time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload_time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload_time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload_time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload_time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload_time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload_time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload_time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload_time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload_time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload_time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload_time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload_time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload_time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload_time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload_time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload_time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload_time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload_time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload_time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload_time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload_time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload_time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload_time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload_time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload_time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload_time = "2024-11-10T15:05:20.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload_time = "2024-11-10T15:05:19.275Z" }, +] From fa5d1fa1a949fc63b05b2fade35dd78756d6f928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Euko?= <50120357+Joaopeuko@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:38:01 +0100 Subject: [PATCH 2/4] fix: Fix all the broken changes from the last pr (#36) ### Description This PR improve documentation, Adds `unittest`, that, don't use mock data, to ensure it works. Make the code work again. Fixes #35 --- .github/workflows/deploy-pypi-packages.yaml | 175 ++++++ .../workflows/deploy-semantic-release.yaml | 154 ++++- .github/workflows/deploy.yaml | 43 -- ...on.yml => test-pytest-and-integration.yml} | 30 +- README.md | 49 +- docs/examples.md | 167 ++++++ docs/examples/README.md | 112 ++++ docs/examples/__init__.py | 1 + .../examples/basic_moving_average_strategy.py | 146 +++++ docs/examples/bollinger_bands_strategy.py | 145 +++++ example.py => docs/examples/example.py | 12 +- .../examples}/example_sockets_connection.py | 0 docs/examples/fibonacci_retracement_eurusd.py | 343 +++++++++++ .../examples}/fimathe/README.md | 0 .../examples}/fimathe/__init__.py | 0 .../examples}/fimathe/eurusd_fimathe.py | 0 .../examples}/fimathe/win_fimathe.py | 0 docs/examples/getting_started.py | 112 ++++ docs/examples/indicator_connector_strategy.py | 223 ++++++++ docs/examples/market_depth_analysis.py | 360 ++++++++++++ docs/examples/rate_converter_example.py | 146 +++++ docs/examples/rsi_strategy.py | 141 +++++ docs/index.md | 22 + docs/strategies/bollinger_bands.md | 307 ++++++++++ docs/strategies/fibonacci_retracement.md | 485 ++++++++++++++++ docs/strategies/market_depth_analysis.md | 484 ++++++++++++++++ docs/strategies/moving_average.md | 285 +++++++++ docs/strategies/rsi_strategy.md | 286 +++++++++ examples_of_expert_advisor/__init__.py | 4 - mkdocs.yaml | 7 + mqpy/__init__.py | 4 + mqpy/book.py | 19 +- mqpy/indicator_connector.py | 12 +- mqpy/logger.py | 50 ++ mqpy/rates.py | 12 +- mqpy/template.py | 541 ++++++++++++++++-- mqpy/trade.py | 170 +++++- mqpy/utilities.py | 70 ++- pyproject.toml | 84 ++- setup.py | 34 +- tests/conftest.py | 34 ++ tests/integration/test_mt5_connection.py | 31 +- tests/test_book.py | 149 +++++ tests/test_rates.py | 157 +++++ tests/test_template.py | 320 +++++++++++ tests/test_tick.py | 133 +++++ tests/test_trade.py | 441 ++++++++++++++ tests/test_utilities.py | 96 ++++ uv.lock | 2 +- 49 files changed, 6369 insertions(+), 229 deletions(-) create mode 100644 .github/workflows/deploy-pypi-packages.yaml delete mode 100644 .github/workflows/deploy.yaml rename .github/workflows/{test-metatrader5-integration.yml => test-pytest-and-integration.yml} (68%) create mode 100644 docs/examples.md create mode 100644 docs/examples/README.md create mode 100644 docs/examples/__init__.py create mode 100755 docs/examples/basic_moving_average_strategy.py create mode 100755 docs/examples/bollinger_bands_strategy.py rename example.py => docs/examples/example.py (85%) rename {examples_of_expert_advisor => docs/examples}/example_sockets_connection.py (100%) create mode 100755 docs/examples/fibonacci_retracement_eurusd.py rename {examples_of_expert_advisor => docs/examples}/fimathe/README.md (100%) rename {examples_of_expert_advisor => docs/examples}/fimathe/__init__.py (100%) rename {examples_of_expert_advisor => docs/examples}/fimathe/eurusd_fimathe.py (100%) rename {examples_of_expert_advisor => docs/examples}/fimathe/win_fimathe.py (100%) create mode 100755 docs/examples/getting_started.py create mode 100755 docs/examples/indicator_connector_strategy.py create mode 100755 docs/examples/market_depth_analysis.py create mode 100755 docs/examples/rate_converter_example.py create mode 100755 docs/examples/rsi_strategy.py create mode 100644 docs/strategies/bollinger_bands.md create mode 100644 docs/strategies/fibonacci_retracement.md create mode 100644 docs/strategies/market_depth_analysis.md create mode 100644 docs/strategies/moving_average.md create mode 100644 docs/strategies/rsi_strategy.md delete mode 100644 examples_of_expert_advisor/__init__.py create mode 100644 mqpy/logger.py create mode 100644 tests/conftest.py create mode 100644 tests/test_book.py create mode 100644 tests/test_rates.py create mode 100644 tests/test_template.py create mode 100644 tests/test_tick.py create mode 100644 tests/test_trade.py create mode 100644 tests/test_utilities.py diff --git a/.github/workflows/deploy-pypi-packages.yaml b/.github/workflows/deploy-pypi-packages.yaml new file mode 100644 index 0000000..07f34d6 --- /dev/null +++ b/.github/workflows/deploy-pypi-packages.yaml @@ -0,0 +1,175 @@ +name: Deploy | Publish Pypi Packages + +on: + push: + branches: + - '**' # All branches for Test PyPI + tags: + - "*" +jobs: + build-and-publish: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools wheel build twine + + - name: Clean dist directory + run: | + if (Test-Path -Path dist) { Remove-Item dist -Recurse -Force } + + - name: Extract issue number and suffix + id: issue + if: startsWith(github.ref, 'refs/heads/') + run: | + # Look for # in commit message + $match = git log -1 --pretty=%B | Select-String -Pattern '#(\d+)' + if ($match) { + $num = $match.Matches.Groups[1].Value + $suffix = "rc$num" + } else { + # No issue number => development build + $suffix = 'dev0' + } + echo "SUFFIX=$suffix" >> $env:GITHUB_ENV + echo "suffix=$suffix" >> $env:GITHUB_OUTPUT + + - name: Extract version from pyproject.toml + id: version + run: | + $verLine = Get-Content pyproject.toml | Select-String -Pattern 'version = "(.*)"' + $VERSION = $verLine.Matches.Groups[1].Value -replace '^v', '' + echo "VERSION=$VERSION" >> $env:GITHUB_ENV + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + if ("${{ github.ref }}".StartsWith('refs/tags/')) { + $TAG_VERSION = "${{ github.ref }}".Substring(10) -replace '^v', '' + echo "TAG_VERSION=$TAG_VERSION" >> $env:GITHUB_ENV + } + + - name: Create temporary pyproject.toml for test build + if: startsWith(github.ref, 'refs/heads/') + run: | + # Read the current pyproject.toml + $content = Get-Content pyproject.toml -Raw + + # Get the current version + $version = "${{ env.VERSION }}" + $suffix = "${{ env.SUFFIX }}" + + # Update the version with the suffix + $newVersion = "$version.$suffix" + + # Replace the version in the content + $updatedContent = $content -replace 'version = "(.*?)"', "version = `"$newVersion`"" + + # Save to a temporary file + $updatedContent | Out-File -FilePath pyproject.toml.temp -Encoding utf8 + + # Show the changes + Write-Host "Original version: $version" + Write-Host "Updated version: $newVersion" + + # Backup original and replace with temp version + Move-Item -Path pyproject.toml -Destination pyproject.toml.bak -Force + Move-Item -Path pyproject.toml.temp -Destination pyproject.toml -Force + + - name: Build package for Test PyPI + if: startsWith(github.ref, 'refs/heads/') + run: | + python -m build + + # After building, restore the original pyproject.toml + Move-Item -Path pyproject.toml.bak -Destination pyproject.toml -Force + + - name: Build package for PyPI + if: startsWith(github.ref, 'refs/tags/') + run: | + python -m build + + - name: Check distributions + run: | + twine check dist/* + + - name: Publish to Test PyPI (branch push) + if: startsWith(github.ref, 'refs/heads/') + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI }} + run: | + Write-Host "Files ready for upload:" + Get-ChildItem dist/* | ForEach-Object { Write-Host " $_" } + + # Upload with verbose output for debugging + twine upload --skip-existing --verbose --repository-url https://test.pypi.org/legacy/ dist/* + + - name: Publish to PyPI (new tag) + if: startsWith(github.ref, 'refs/tags/') + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + Write-Host "Files to upload to PyPI:" + Get-ChildItem dist/* | ForEach-Object { Write-Host " $_" } + twine upload --verbose dist/* + + - name: Create Step Summary + run: | + # Set the display version based on the ref + if ("${{ github.ref }}".StartsWith("refs/tags/")) { + $displayVersion = "${{ env.TAG_VERSION }}" + } else { + $displayVersion = "${{ env.VERSION }}.${{ env.SUFFIX }}" + } + + @" + # MQPy Package + + ## Installation Instructions + + ### Important Warning โš ๏ธ + **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** + + - Always use a **demo account** with fake money when testing strategies + - MQPy is provided for **educational purposes only** + - Past performance is not indicative of future results + - Never trade with money you cannot afford to lose + - The developers are not responsible for any financial losses + + ### Windows-Only Compatibility + This package is designed to work exclusively on Windows operating systems. + + ### Installation Steps + + $( if ("${{ github.ref }}".StartsWith("refs/tags/")) { + @" + #### Production Release + This is an official release version (${{ env.TAG_VERSION }}) published to PyPI. + + ``` + pip install mqpy==${{ env.TAG_VERSION }} + ``` + "@ + } else { + @" + #### Test/RC Version + This is a release candidate version published to Test PyPI. + + ``` + pip install mqpy==$displayVersion --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ + ``` + "@ + }) + + ### Documentation + For complete documentation, visit our [GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration). + "@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-semantic-release.yaml b/.github/workflows/deploy-semantic-release.yaml index 1082fbf..d3a3640 100644 --- a/.github/workflows/deploy-semantic-release.yaml +++ b/.github/workflows/deploy-semantic-release.yaml @@ -2,10 +2,26 @@ name: Deploy | Semantic Release on: workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (no changes will be committed)' + type: boolean + default: false + debug: + description: 'Enable verbose debugging output' + type: boolean + default: false + push: + branches: + - '**' + paths-ignore: + - 'docs/**' + - '*.md' + - '.github/workflows/deploy-pypi-packages.yaml' jobs: release: - runs-on: windows-latest + runs-on: ubuntu-latest concurrency: release permissions: contents: write @@ -16,9 +32,145 @@ jobs: with: fetch-depth: 0 + - name: Set run mode + id: set_mode + shell: bash + run: | + IS_DRY_RUN=$([ "${{ github.event_name }}" = "push" ] || [ "${{ inputs.dry_run }}" = "true" ] && echo "true" || echo "false") + echo "is_dry_run=$IS_DRY_RUN" >> $GITHUB_OUTPUT + echo "Mode: $([ "$IS_DRY_RUN" = "true" ] && echo "Dry run" || echo "Full release")" + + - name: Python Release - Dry Run + id: release_dryrun + if: steps.set_mode.outputs.is_dry_run == 'true' + uses: python-semantic-release/python-semantic-release@v9.20.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + push: "false" + commit: "false" + tag: "false" + changelog: "false" + root_options: ${{ inputs.debug && '-vv --noop' || '-v --noop' }} + + - name: Extract Next Version Info + id: extract_next_version + if: steps.set_mode.outputs.is_dry_run == 'true' && steps.release_dryrun.outputs.version == '' + shell: bash + run: | + # When no release is needed, semantic-release doesn't output the next version + # We need to determine it manually from the commit history + + # Check if we have commits that would trigger a version bump + FEAT_COMMITS=$(git log --grep="^feat:" -i --pretty=format:"%h" | wc -l) + FIX_COMMITS=$(git log --grep="^fix:" -i --pretty=format:"%h" | wc -l) + BREAKING_COMMITS=$(git log --grep="BREAKING CHANGE:" -i --pretty=format:"%h" | wc -l) + + # Get current version from pyproject.toml + CURRENT_VERSION=$(grep -m 1 'version = "' pyproject.toml | awk -F'"' '{print $2}' | sed 's/^v//') + echo "Current version: $CURRENT_VERSION" + + # Split current version into components + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + + # Determine the next version based on conventional commits + if [ "$BREAKING_COMMITS" -gt 0 ]; then + # Major version bump + NEXT_VERSION="$((MAJOR + 1)).0.0" + elif [ "$FEAT_COMMITS" -gt 0 ]; then + # Minor version bump + NEXT_VERSION="$MAJOR.$((MINOR + 1)).0" + elif [ "$FIX_COMMITS" -gt 0 ]; then + # Patch version bump + NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" + else + # No significant changes, use development version + NEXT_VERSION="${CURRENT_VERSION}.dev0" + fi + + echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "next_tag=v$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "Determined next version: $NEXT_VERSION" + - name: Python Release + id: release + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.dry_run }} uses: python-semantic-release/python-semantic-release@v9.20.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} push: "true" changelog: "true" + root_options: ${{ inputs.debug && '-vv' || '-v' }} + + - name: Create Step Summary + shell: bash + run: | + IS_DRY_RUN="${{ steps.set_mode.outputs.is_dry_run }}" + RELEASE_ID=$([ "$IS_DRY_RUN" = "true" ] && echo "release_dryrun" || echo "release") + WAS_RELEASED=$([ "${{ steps.release_dryrun.outputs.released || steps.release.outputs.released }}" = "true" ] && echo "Yes" || echo "No") + + # First try to get version from release outputs + VERSION="${{ steps.release_dryrun.outputs.version || steps.release.outputs.version }}" + TAG="${{ steps.release_dryrun.outputs.tag || steps.release.outputs.tag }}" + + # If no version from release outputs, try to get from extract_next_version step + if [ "$IS_DRY_RUN" = "true" ] && [ -z "$VERSION" ]; then + VERSION="${{ steps.extract_next_version.outputs.next_version }}" + TAG="${{ steps.extract_next_version.outputs.next_tag }}" + fi + + # Display trigger information + if [ "${{ github.event_name }}" = "push" ]; then + TRIGGER_INFO="Triggered by push to branch: ${{ github.ref_name }}" + else + TRIGGER_INFO="Triggered manually via workflow dispatch" + fi + + # Create warning text for dry run + if [ "$IS_DRY_RUN" = "true" ]; then + DRY_RUN_TEXT="โš ๏ธ This is a dry run - no changes were committed" + TITLE_SUFFIX=" (Dry Run)" + else + DRY_RUN_TEXT="" + TITLE_SUFFIX="" + fi + + cat > $GITHUB_STEP_SUMMARY << EOF + # MQPy Release$TITLE_SUFFIX + + ## Release Summary + + $TRIGGER_INFO + $DRY_RUN_TEXT + + Current/Next Version: $VERSION + Current/Next Tag: $TAG + Release required: $WAS_RELEASED + + ## Installation Instructions + + ### Important Warning โš ๏ธ + **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** + + - Always use a **demo account** with fake money when testing strategies + - MQPy is provided for **educational purposes only** + - Past performance is not indicative of future results + - Never trade with money you cannot afford to lose + - The developers are not responsible for any financial losses + + ### Windows-Only Compatibility + This package is designed to work exclusively on Windows operating systems. + + ### Installation Steps + + #### $([ "$IS_DRY_RUN" = "true" ] && echo "Test/RC Version" || echo "Production Version") + $([ "$IS_DRY_RUN" = "true" ] && echo "This is a release candidate version published to Test PyPI. + + \`\`\` + pip install mqpy==$VERSION --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ + \`\`\`" || echo "\`\`\` + pip install mqpy==$VERSION + \`\`\`") + + ### Documentation + For complete documentation, visit our [GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration). + EOF diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index 8599b7a..0000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish distributions - -on: - push: - branches: ['main'] - tags: - - v* - -jobs: - build-and-publish: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.8 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools - python -m pip install poetry - python -m pip install twine - python -m poetry install - - - name: Build and publish to Test PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI }} - run: | - python -m poetry build - twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/* - - - name: Publish to PyPI (if it's a new tag) - if: github.ref == 'refs/heads/master' - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - python -m poetry build - twine upload dist/* diff --git a/.github/workflows/test-metatrader5-integration.yml b/.github/workflows/test-pytest-and-integration.yml similarity index 68% rename from .github/workflows/test-metatrader5-integration.yml rename to .github/workflows/test-pytest-and-integration.yml index 450c149..a27cb7a 100644 --- a/.github/workflows/test-metatrader5-integration.yml +++ b/.github/workflows/test-pytest-and-integration.yml @@ -1,6 +1,12 @@ -name: Test | Integration Test +name: Test | Pytest and Integration Test -on: [push] +on: + workflow_dispatch: + pull_request: + push: + branches: ['*'] + schedule: + - cron: '0 12 * * 0' # Run every week monday at 12:00 jobs: build: @@ -63,14 +69,26 @@ jobs: throw "MT5 failed to start" } - - name: Install MetaTrader5 Python package - run: pip install MetaTrader5 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov MetaTrader5 + pip install -e . - - name: Run MT5 Test + - name: Run tests with coverage env: MT5_LOGIN: ${{ secrets.MT5_LOGIN }} MT5_PASSWORD: ${{ secrets.MT5_PASSWORD }} MT5_SERVER: "MetaQuotes-Demo" MT5_PATH: "C:\\Program Files\\MetaTrader 5\\terminal64.exe" run: | - python tests/integration/test_mt5_connection.py + pytest -k . --cov=mqpy --cov-append --junitxml=pytest.xml -x | tee pytest-coverage.txt + + - name: Generate coverage summary + shell: pwsh + run: | + "## Test Coverage Summary" | Out-File -FilePath summary.md -Encoding utf8 + '```' | Out-File -FilePath summary.md -Append -Encoding utf8 + Get-Content pytest-coverage.txt | Out-File -FilePath summary.md -Append -Encoding utf8 + '```' | Out-File -FilePath summary.md -Append -Encoding utf8 + Get-Content summary.md | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 diff --git a/README.md b/README.md index 9180e40..f1cbeae 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,41 @@ -
- MQPy Logo -
+

+ MQPy Logo +

+

+ PyPI - Downloads + PyPI + PyPI - Wheel + PyPI - License +

-![PyPI - Downloads](https://img.shields.io/pypi/dm/mqpy) -![PyPI](https://img.shields.io/pypi/v/mqpy) -![PyPI - Wheel](https://img.shields.io/pypi/wheel/mqpy) -![PyPI - License](https://img.shields.io/pypi/l/mqpy) +

Mql5-Python-Integration (MQPy)

-# Mql5-Python-Integration (MQPy) - -**Current Version: v0.6.9** +

Current Version: v0.6.9

Welcome to the Mql5-Python-Integration project! This project facilitates the integration between MetaTrader 5 (Mql5) and Python, allowing for efficient algorithmic trading strategies. +## โš ๏ธ TRADING RISK WARNING + +**IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** + +- Always use a **demo account** with fake money when testing strategies +- MQPy is provided for **educational purposes only** +- Past performance is not indicative of future results +- Never trade with money you cannot afford to lose +- The developers are not responsible for any financial losses incurred from using this software + ## Table of Contents -- [Mql5-Python-Integration (MQPy)](#mql5-python-integration-mqpy) - - [Table of Contents](#table-of-contents) - - [Project Update: Changes in Progress](#project-update-changes-in-progress) - - [Installation](#installation) - - [Usage](#usage) - - [Generate the File](#generate-the-file) - - [Missing Features/Good Practice](#missing-featuresgood-practice) - - [Delicate Metatrader5 Environment](#delicate-metatrader5-environment) - - [Alternative Libraries](#alternative-libraries) +- [โš ๏ธ TRADING RISK WARNING](#๏ธ-trading-risk-warning) +- [Table of Contents](#table-of-contents) +- [Project Update: Changes in Progress](#project-update-changes-in-progress) +- [Installation](#installation) +- [Usage](#usage) + - [Generate the File](#generate-the-file) +- [Missing Features/Good Practice](#missing-featuresgood-practice) + - [Delicate Metatrader5 Environment](#delicate-metatrader5-environment) + - [Alternative Libraries](#alternative-libraries) ## Project Update: Changes in Progress diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..860a495 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,167 @@ +# MQPy Examples + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + - Past performance is not indicative of future results + - The developers are not responsible for any financial losses + +MQPy provides a variety of example trading strategies to help you understand how to implement your own algorithmic trading solutions using MetaTrader 5. + +## Getting Started + +If you're new to MQPy, we recommend starting with the [Getting Started Example](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/getting_started.py) which introduces you to the basics of: + +- Initializing the trading environment +- Fetching market data +- Making trading decisions +- Executing trades + +## Basic Strategies + +### Moving Average Crossover + +The [Moving Average Crossover](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/basic_moving_average_strategy.py) strategy is a classic trading approach that: + +| Feature | Description | +|---------|-------------| +| Signal Generation | Uses crossovers between short and long moving averages | +| Implementation | Includes proper crossover detection logic | +| Error Handling | Comprehensive logging and exception handling | + +[Read detailed explanation of the Moving Average strategy โ†’](strategies/moving_average.md) + +```python +def calculate_sma(prices, period): + """Calculate Simple Moving Average.""" + if len(prices) < period: + return None + return sum(prices[-period:]) / period +``` + +## Technical Indicator Strategies + +### RSI Strategy + +The [RSI Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rsi_strategy.py) example demonstrates: + +| Feature | Description | +|---------|-------------| +| Indicator | Implementation of the Relative Strength Index (RSI) | +| Trading Approach | Entry/exit based on overbought and oversold conditions | +| Technical Analysis | Practical example of calculating and using indicators | + +[Read detailed explanation of the RSI strategy โ†’](strategies/rsi_strategy.md) + +### Bollinger Bands Strategy + +The [Bollinger Bands Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/bollinger_bands_strategy.py) shows: + +| Feature | Description | +|---------|-------------| +| Trading Approach | Using Bollinger Bands for trading range breakouts | +| Strategy Type | Mean reversion trading principles | +| Signal Generation | Volatility-based entry and exit logic | + +[Read detailed explanation of the Bollinger Bands strategy โ†’](strategies/bollinger_bands.md) + +## Advanced Strategies + +### Fibonacci Retracement Strategy + +The [Fibonacci Retracement Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/fibonacci_retracement_eurusd.py) for EURUSD: + +| Feature | Description | +|---------|-------------| +| Strategy Type | Implements the FiMathe strategy | +| Pattern Recognition | Uses Fibonacci retracement levels for entries and exits | +| Risk Management | Includes dynamic stop-loss adjustment based on price action | + +[Read detailed explanation of the Fibonacci Retracement strategy โ†’](strategies/fibonacci_retracement.md) + +### Market Depth Analysis + +The [Market Depth Analysis](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/market_depth_analysis.py) provides insights into order book data: + +| Feature | Description | +|---------|-------------| +| Order Book Analysis | Examines buy/sell order distribution and concentration | +| Support/Resistance | Identifies potential support and resistance levels from actual orders | +| Visualization | Creates horizontal bar charts showing bid/ask distribution with key levels | + +[Read detailed explanation of the Market Depth Analysis โ†’](strategies/market_depth_analysis.md) + +### Multi-Timeframe Analysis + +The [Rate Converter Example](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rate_converter_example.py) demonstrates: + +| Feature | Description | +|---------|-------------| +| Timeframe Conversion | How to convert between different timeframes using the RateConverter | +| Multi-timeframe Analysis | Calculating moving averages across different timeframes | +| Visualization | Creating charts for price data across 1-minute, 5-minute, and 1-hour timeframes | + +### Indicator Connector Strategy + +The [Indicator Connector Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/indicator_connector_strategy.py) shows: + +| Feature | Description | +|---------|-------------| +| Connectivity | How to connect to MetaTrader 5's custom indicators | +| Signal Combination | Combining multiple indicator signals (Stochastic and Moving Average) | +| Advanced Techniques | Advanced signal generation and filtering approaches | + +## Running the Examples + +To run any of these examples: + +1. Ensure you have MQPy installed: + ```bash + pip install mqpy + ``` + +2. Make sure MetaTrader 5 is installed and running on your system + +3. Run any example with Python: + ```bash + python getting_started.py + ``` + +## Contributing Your Own Examples + +If you've developed an interesting strategy using MQPy, consider contributing it to this examples collection by submitting a pull request! + +## Disclaimer + +These example strategies are for educational purposes only and are not financial advice. Always perform your own analysis and risk assessment before trading with real money. + +## All Example Files + +You can access these examples in several ways: + +1. **Clone the entire repository**: + ```bash + git clone https://github.com/Joaopeuko/Mql5-Python-Integration.git + cd Mql5-Python-Integration/docs/examples + ``` + +2. **Download individual files** by clicking on the links in the table below. + +3. **Copy the code** from the strategy explanations page for the strategies with detailed documentation. + +Here are direct links to all the example files in the MQPy repository: + +| Strategy | Description | Source Code | +|----------|-------------|-------------| +| Getting Started | Basic introduction to MQPy | [getting_started.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/getting_started.py) | +| Moving Average Crossover | Simple trend-following strategy | [basic_moving_average_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/basic_moving_average_strategy.py) | +| RSI Strategy | Momentum-based overbought/oversold strategy | [rsi_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rsi_strategy.py) | +| Bollinger Bands | Mean reversion volatility strategy | [bollinger_bands_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/bollinger_bands_strategy.py) | +| Fibonacci Retracement | Advanced Fibonacci pattern strategy | [fibonacci_retracement_eurusd.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/fibonacci_retracement_eurusd.py) | +| Market Depth Analysis | Order book and volume analysis | [market_depth_analysis.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/market_depth_analysis.py) | +| Rate Converter | Multi-timeframe analysis example | [rate_converter_example.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rate_converter_example.py) | +| Indicator Connector | Custom indicator integration | [indicator_connector_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/indicator_connector_strategy.py) | +| Sockets Connection | Advanced MetaTrader connectivity | [example_sockets_connection.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/example_sockets_connection.py) | diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 0000000..acbb549 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,112 @@ +# MQPy Trading Strategy Examples + +This directory contains various example trading strategies implemented using the MQPy framework for MetaTrader 5 integration. + +## Getting Started + +If you're new to MQPy, start with the `getting_started.py` example which demonstrates basic concepts: + +- Initializing the trading environment +- Fetching market data +- Making trading decisions +- Executing trades + +## Available Examples + +### Basic Strategies + +1. **Getting Started** (`getting_started.py`) + - A simple introduction to the MQPy framework + - Demonstrates basic data retrieval and trading operations + - Perfect for beginners + +2. **Moving Average Crossover** (`basic_moving_average_strategy.py`) + - Uses crossovers between short and long moving averages + - Implements proper crossover detection logic + - Includes logging and exception handling + +### Technical Indicator Strategies + +3. **RSI Strategy** (`rsi_strategy.py`) + - Implements the Relative Strength Index (RSI) indicator + - Trades based on overbought and oversold conditions + - Shows how to calculate and use technical indicators + +4. **Bollinger Bands Strategy** (`bollinger_bands_strategy.py`) + - Uses Bollinger Bands for trading range breakouts + - Demonstrates mean reversion trading principles + - Includes volatility-based entry and exit logic + +### Advanced Strategies + +5. **Fibonacci Retracement Strategy** (`fibonacci_retracement_eurusd.py`) + - Implements the FiMathe strategy for EURUSD + - Uses Fibonacci retracement levels for entries and exits + - Includes dynamic stop-loss adjustment based on price action + +6. **Multi-Timeframe Analysis** (`rate_converter_example.py`) + - Demonstrates how to convert between different timeframes using the RateConverter + - Implements multi-timeframe analysis by calculating moving averages across timeframes + - Visualizes price data and indicators across 1-minute, 5-minute, and 1-hour charts + +## Fibonacci Retracement Strategy + +The Fibonacci Retracement strategy (`fibonacci_retracement_eurusd.py`) demonstrates how to implement a trading system based on Fibonacci retracement levels. This strategy: + +1. **Identifies swing points**: The algorithm detects significant market swing highs and lows within a specified window. +2. **Calculates Fibonacci levels**: Standard Fibonacci ratios (0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0) are applied between swing points to generate potential support and resistance levels. +3. **Generates trading signals**: The strategy produces buy signals when price bounces off key retracement levels during uptrends and sell signals during downtrends. +4. **Visualizes analysis**: Creates charts showing price action with identified swing points and Fibonacci levels to aid in trading decisions. + +This approach is popular among technical traders who believe that markets frequently retrace a predictable portion of a move before continuing in the original direction. + +## Market Depth Analysis + +The Market Depth Analysis tool (`market_depth_analysis.py`) provides insights into order book data (DOM - Depth of Market) to understand supply and demand dynamics. Key features include: + +1. **Real-time market depth monitoring**: Captures and analyzes order book snapshots at regular intervals. +2. **Buy/sell pressure analysis**: Calculates metrics such as buy/sell volume ratio, percentage distribution, and order concentration. +3. **Support/resistance identification**: Detects potential support and resistance levels based on unusual volume concentration at specific price points. +4. **Visual representation**: Creates horizontal bar charts showing the distribution of buy (bid) and sell (ask) orders, with highlighted support/resistance zones. + +This analysis helps traders understand current market sentiment and identify price levels where significant buying or selling interest exists. The tool is particularly valuable for short-term traders and those interested in order flow analysis. + +## Detailed Strategy Documentation + +For an in-depth explanation of advanced strategies including theoretical background, implementation details, and potential customizations, see the [detailed strategy documentation](STRATEGY_DOCUMENTATION.md). + +## Running the Examples + +1. Make sure you have MQPy installed: + ```bash + pip install mqpy + ``` + +2. Ensure MetaTrader 5 is installed and running on your system + +3. Run any example with Python: + ```bash + python getting_started.py + ``` + +## Strategy Development Best Practices + +When developing your own strategies with MQPy, consider the following best practices: + +1. **Error Handling**: Implement proper exception handling to catch network issues, data problems, or unexpected errors + +2. **Logging**: Use Python's logging module to record important events and debug information + +3. **Testing**: Test your strategy on historical data before deploying with real money + +4. **Risk Management**: Always implement proper stop-loss and take-profit levels + +5. **Architecture**: Separate your trading logic, indicators, and execution code for better maintainability + +## Contributing + +If you've developed an interesting strategy using MQPy, consider contributing it to this examples collection by submitting a pull request. + +## Disclaimer + +These example strategies are for educational purposes only and are not financial advice. Always perform your own analysis and risk assessment before trading with real money. diff --git a/docs/examples/__init__.py b/docs/examples/__init__.py new file mode 100644 index 0000000..bdfe1c0 --- /dev/null +++ b/docs/examples/__init__.py @@ -0,0 +1 @@ +"""Example package for MQPy demonstration code.""" diff --git a/docs/examples/basic_moving_average_strategy.py b/docs/examples/basic_moving_average_strategy.py new file mode 100755 index 0000000..fa4acab --- /dev/null +++ b/docs/examples/basic_moving_average_strategy.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Basic Moving Average Crossover Strategy Example. + +This example demonstrates a simple moving average crossover strategy using the mqpy framework. +When a shorter-period moving average crosses above a longer-period moving average, +the strategy generates a buy signal. Conversely, when the shorter-period moving average +crosses below the longer-period moving average, the strategy generates a sell signal. +""" + +from __future__ import annotations + +import logging + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def calculate_sma(prices: list[float], period: int) -> float | None: + """Calculate Simple Moving Average. + + Args: + prices: A list of price values + period: The period for the moving average calculation + + Returns: + The simple moving average value or None if insufficient data + """ + if len(prices) < period: + return None + return sum(prices[-period:]) / period + + +def main() -> None: + """Main execution function for the Moving Average Crossover strategy.""" + # Initialize the trading strategy + trade = Trade( + expert_name="Moving Average Crossover", + version="1.0", + symbol="EURUSD", + magic_number=567, + lot=0.1, + stop_loss=25, + emergency_stop_loss=300, + take_profit=25, + emergency_take_profit=300, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting Moving Average Crossover strategy on {trade.symbol}") + + # Strategy parameters + prev_tick_time = 0 + short_period = 5 + long_period = 20 + + # Variables to track previous state for crossover detection + prev_short_ma = None + prev_long_ma = None + + try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, long_period + 10, 0, 1) # Get extra data for reliability + + # Only process if we have a new tick and enough historical data + has_new_tick = current_tick.time_msc != prev_tick_time + has_enough_data = len(historical_rates.close) >= long_period + + if has_new_tick and has_enough_data: + # Calculate moving averages + short_ma = calculate_sma(historical_rates.close, short_period) + long_ma = calculate_sma(historical_rates.close, long_period) + + # Check if we have enough data for comparison + has_short_ma = short_ma is not None + has_long_ma = long_ma is not None + has_prev_short_ma = prev_short_ma is not None + has_prev_long_ma = prev_long_ma is not None + has_valid_ma_values = has_short_ma and has_long_ma and has_prev_short_ma and has_prev_long_ma + + if has_valid_ma_values: + # Check short MA and long MA relationship for current and previous values + is_above_now = short_ma > long_ma + is_above_prev = prev_short_ma > prev_long_ma + + # Detect crossover (short MA crosses above long MA) + cross_above = is_above_now and not is_above_prev + + # Detect crossunder (short MA crosses below long MA) + cross_below = not is_above_now and is_above_prev + + # Log crossover events + if cross_above: + logger.info( + f"Bullish crossover detected: Short MA ({short_ma:.5f}) " + f"crossed above Long MA ({long_ma:.5f})" + ) + elif cross_below: + logger.info( + f"Bearish crossover detected: Short MA ({short_ma:.5f}) " + f"crossed below Long MA ({long_ma:.5f})" + ) + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=cross_above, should_sell=cross_below, comment="Moving Average Crossover Strategy" + ) + + # Update previous MA values for next comparison + prev_short_ma = short_ma + prev_long_ma = long_ma + + # Update trading statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + + except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") + except Exception: + logger.exception("Error in strategy execution") + finally: + logger.info("Finishing the program.") + + +if __name__ == "__main__": + main() diff --git a/docs/examples/bollinger_bands_strategy.py b/docs/examples/bollinger_bands_strategy.py new file mode 100755 index 0000000..bc9ae89 --- /dev/null +++ b/docs/examples/bollinger_bands_strategy.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Bollinger Bands Strategy Example. + +This example demonstrates a Bollinger Bands trading strategy using the mqpy framework. +The strategy enters long positions when price breaks below the lower band and enters +short positions when price breaks above the upper band. The strategy is designed to +trade price reversals from extreme movements. +""" + +from __future__ import annotations + +import logging + +import numpy as np + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def calculate_bollinger_bands( + prices: list[float], period: int = 20, num_std_dev: float = 2.0 +) -> tuple[float, float, float] | None: + """Calculate Bollinger Bands (middle, upper, lower). + + Args: + prices: A list of closing prices + period: The period for SMA calculation, default is 20 + num_std_dev: Number of standard deviations for bands, default is 2.0 + + Returns: + A tuple of (middle_band, upper_band, lower_band) or None if not enough data + """ + if len(prices) < period: + return None + + # Convert to numpy array for vectorized calculations + price_array = np.array(prices[-period:]) + + # Calculate SMA (middle band) + sma = np.mean(price_array) + + # Calculate standard deviation + std_dev = np.std(price_array) + + # Calculate upper and lower bands + upper_band = sma + (num_std_dev * std_dev) + lower_band = sma - (num_std_dev * std_dev) + + return (sma, upper_band, lower_band) + + +def main() -> None: + """Main execution function for the Bollinger Bands strategy.""" + # Initialize the trading strategy + trade = Trade( + expert_name="Bollinger Bands Strategy", + version="1.0", + symbol="EURUSD", + magic_number=569, + lot=0.1, + stop_loss=50, + emergency_stop_loss=150, + take_profit=100, + emergency_take_profit=300, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting Bollinger Bands strategy on {trade.symbol}") + + # Strategy parameters + prev_tick_time = 0 + bb_period = 20 + bb_std_dev = 2.0 + + try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, bb_period + 10, 0, 1) # Get extra data for reliability + + # Only process if we have a new tick + if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= bb_period: + # Calculate Bollinger Bands + bb_result = calculate_bollinger_bands(historical_rates.close, period=bb_period, num_std_dev=bb_std_dev) + + if bb_result: + middle_band, upper_band, lower_band = bb_result + current_price = current_tick.last + + # Generate signals based on price position relative to bands + # Buy when price crosses below lower band (potential bounce) + is_buy_signal = current_price < lower_band + + # Sell when price crosses above upper band (potential reversal) + is_sell_signal = current_price > upper_band + + # Log band data and signals + logger.info(f"Current price: {current_price:.5f}") + logger.info( + f"Bollinger Bands - Middle: {middle_band:.5f}, Upper: {upper_band:.5f}, Lower: {lower_band:.5f}" + ) + + if is_buy_signal: + logger.info(f"Buy signal: Price ({current_price:.5f}) below lower band ({lower_band:.5f})") + elif is_sell_signal: + logger.info(f"Sell signal: Price ({current_price:.5f}) above upper band ({upper_band:.5f})") + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=is_buy_signal, should_sell=is_sell_signal, comment="Bollinger Bands Strategy" + ) + + # Update trading statistics + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + + except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") + except Exception: + logger.exception("Error in strategy execution") + finally: + logger.info("Finishing the program.") + + +if __name__ == "__main__": + main() diff --git a/example.py b/docs/examples/example.py similarity index 85% rename from example.py rename to docs/examples/example.py index cee288a..4a5b5b0 100644 --- a/example.py +++ b/docs/examples/example.py @@ -3,14 +3,14 @@ This example demonstrates a Moving Average Crossover strategy. """ -from mqpy.src.rates import Rates -from mqpy.src.tick import Tick -from mqpy.src.trade import Trade +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade # Initialize the trading strategy trade = Trade( expert_name="Moving Average Crossover", - version=1.0, + version="1.0", symbol="EURUSD", magic_number=567, lot=0.1, @@ -45,7 +45,9 @@ is_cross_below = short_ma < long_ma and current_tick.last < short_ma # Execute trading positions based on signals - trade.open_position(is_cross_above, is_cross_below, "Moving Average Crossover Strategy") + trade.open_position( + should_buy=is_cross_above, should_sell=is_cross_below, comment="Moving Average Crossover Strategy" + ) prev_tick_time = current_tick.time_msc diff --git a/examples_of_expert_advisor/example_sockets_connection.py b/docs/examples/example_sockets_connection.py similarity index 100% rename from examples_of_expert_advisor/example_sockets_connection.py rename to docs/examples/example_sockets_connection.py diff --git a/docs/examples/fibonacci_retracement_eurusd.py b/docs/examples/fibonacci_retracement_eurusd.py new file mode 100755 index 0000000..8a2dab3 --- /dev/null +++ b/docs/examples/fibonacci_retracement_eurusd.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Fibonacci Retracement Trading Strategy Example. + +This example demonstrates a Fibonacci retracement-based trading strategy +using the mqpy framework. The strategy identifies swing highs and lows, +calculates Fibonacci retracement levels, and generates trading signals +when price interacts with key retracement levels. + +Strategy Logic: +-------------- +1. Swing Point Detection: + - Analyzes price action to identify significant swing highs and lows + - Uses a configurable window parameter to determine the significance of swing points + +2. Fibonacci Level Calculation: + - Applies standard Fibonacci ratios (0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0) + - Calculates retracement levels between the most recent significant swing points + - Adapts to both uptrends (retracements from low to high) and downtrends (retracements from high to low) + +3. Signal Generation: + - Monitors price proximity to Fibonacci levels using a configurable tolerance + - Generates buy signals when price touches key retracement levels during uptrends + - Generates sell signals when price touches key retracement levels during downtrends + - Focuses on the most reliable retracement levels (0.382, 0.5, 0.618) + +4. Visualization: + - Creates candlestick charts with marked swing points + - Displays horizontal lines at each Fibonacci retracement level + - Provides visual confirmation of the strategy logic + +Trading Assumptions: +------------------- +- Markets tend to retrace by predictable percentages during trends +- Key Fibonacci levels often act as support/resistance +- The 38.2%, 50%, and 61.8% retracement levels are considered the most reliable for trading signals +- Proper risk management using stop-loss and take-profit levels is essential + +This strategy should be further enhanced with additional filters (volume, other indicators) +for improved reliability in live trading environments. +""" + +from __future__ import annotations + +import logging + +import matplotlib.dates as mdates +import matplotlib.pyplot as plt +import pandas as pd +from mpl_finance import candlestick_ohlc + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +# Fibonacci retracement levels (standard) +FIBONACCI_LEVELS = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] + + +def find_swing_points(prices: list[float], window: int = 5) -> tuple[list[int], list[int]]: + """Find swing high and swing low points in price data. + + Args: + prices: List of price values + window: Window size for determining swing points + + Returns: + Tuple containing lists of swing high and swing low indices + """ + highs: list[int] = [] + lows: list[int] = [] + + if len(prices) < 2 * window + 1: + return highs, lows + + for i in range(window, len(prices) - window): + # Check for swing high + if all(prices[i] > prices[i - j] for j in range(1, window + 1)) and all( + prices[i] > prices[i + j] for j in range(1, window + 1) + ): + highs.append(i) + + # Check for swing low + if all(prices[i] < prices[i - j] for j in range(1, window + 1)) and all( + prices[i] < prices[i + j] for j in range(1, window + 1) + ): + lows.append(i) + + return highs, lows + + +def calculate_fibonacci_levels(start_price: float, end_price: float) -> dict[float, float]: + """Calculate Fibonacci retracement price levels. + + Args: + start_price: Starting price for retracement (swing high/low) + end_price: Ending price for retracement (swing low/high) + + Returns: + Dictionary mapping Fibonacci ratios to price levels + """ + diff = end_price - start_price + levels = {} + + for ratio in FIBONACCI_LEVELS: + levels[ratio] = start_price + diff * ratio + + return levels + + +def plot_fibonacci_levels( + rates: Rates, swing_highs: list[int], swing_lows: list[int], retracement_levels: dict[float, float] | None = None +) -> None: + """Plot price chart with swing points and Fibonacci retracement levels. + + Args: + rates: Rate data containing OHLC prices and times + swing_highs: Indices of swing high points + swing_lows: Indices of swing low points + retracement_levels: Dictionary of Fibonacci levels to plot + """ + try: + # Convert rates data to pandas DataFrame + data = [] + for i in range(len(rates.time)): + # Convert to matplotlib date format + date = mdates.date2num(pd.to_datetime(rates.time[i], unit="s")) + data.append([date, rates.open[i], rates.high[i], rates.low[i], rates.close[i]]) + + price_data = pd.DataFrame(data, columns=["Date", "Open", "High", "Low", "Close"]) + + # Create plot + fig, ax = plt.subplots(figsize=(12, 8)) + + # Plot candlestick chart + candlestick_ohlc(ax, price_data.values, width=0.001, colorup="green", colordown="red", alpha=0.8) + + # Format date axis + ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d %H:%M")) + plt.xticks(rotation=45) + + # Mark swing highs and lows + for idx in swing_highs: + ax.plot(mdates.date2num(pd.to_datetime(rates.time[idx], unit="s")), rates.high[idx], "ro", markersize=8) + + for idx in swing_lows: + ax.plot(mdates.date2num(pd.to_datetime(rates.time[idx], unit="s")), rates.low[idx], "go", markersize=8) + + # Plot Fibonacci retracement levels if provided + if retracement_levels: + colors = ["b", "g", "r", "c", "m", "y", "k"] + for i, (ratio, price) in enumerate(retracement_levels.items()): + ax.axhline( + y=price, color=colors[i % len(colors)], linestyle="--", label=f"Fib {ratio:.3f}: {price:.5f}" + ) + + plt.title("EURUSD with Fibonacci Retracement Levels") + plt.ylabel("Price") + plt.legend() + plt.tight_layout() + + # Save the chart + plt.savefig("fibonacci_retracement.png") + logger.info("Chart saved as 'fibonacci_retracement.png'") + + # Show the plot + plt.show() + + except Exception: + logger.exception("Error creating Fibonacci retracement chart") + + +def analyze_retracement_signals( + current_price: float, fib_levels: dict[float, float], tolerance: float = 0.0001 +) -> tuple[bool, bool, float | None]: + """Analyze price position relative to Fibonacci levels and generate signals. + + Args: + current_price: Current market price + fib_levels: Dictionary of Fibonacci retracement levels + tolerance: Price tolerance for level testing + + Returns: + Tuple containing (buy_signal, sell_signal, level_tested) + """ + buy_signal = False + sell_signal = False + level_tested = None + + # Determine trend direction from Fibonacci levels + uptrend = fib_levels[0.0] < fib_levels[1.0] + + for ratio, level in fib_levels.items(): + # Check if price is near a Fibonacci level + if abs(current_price - level) < tolerance: + level_tested = ratio + + # In uptrend, bounces off retracement levels can be buy signals + # Focus on the most reliable levels: 0.382, 0.5, 0.618 + if uptrend and ratio in [0.382, 0.5, 0.618]: + buy_signal = True + + # In downtrend, bounces off retracement levels can be sell signals + elif not uptrend and ratio in [0.382, 0.5, 0.618]: + sell_signal = True + + break + + return buy_signal, sell_signal, level_tested + + +def main() -> None: + """Main execution function for the Fibonacci retracement strategy.""" + # Initialize the trading strategy + trade = Trade( + expert_name="Fibonacci Retracement Strategy", + version="1.0", + symbol="EURUSD", + magic_number=571, + lot=0.1, + stop_loss=35, + emergency_stop_loss=100, + take_profit=70, + emergency_take_profit=200, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting Fibonacci Retracement strategy on {trade.symbol}") + + # Strategy parameters + prev_tick_time = 0 + lookback_period = 100 # Number of candles to analyze + swing_window = 5 # Window for swing high/low detection + price_tolerance = 0.0001 # Tolerance for testing price at Fibonacci levels + + # Variables to track state + current_fib_levels = None + last_swing_check_time = 0 + swing_recalculation_interval = 10 * 60 # Recalculate swings every 10 minutes + + try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data (H1 timeframe for identifying swings) + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, lookback_period, 0, 60) # H1 timeframe + + current_time = current_tick.time + + # Only recalculate swing points and Fibonacci levels periodically + if (current_time - last_swing_check_time) > swing_recalculation_interval: + # Find swing highs and lows + swing_highs, swing_lows = find_swing_points(historical_rates.close, window=swing_window) + + # Calculate Fibonacci levels if we have swing points + if len(swing_highs) > 0 and len(swing_lows) > 0: + # Use the most recent swing high and swing low + recent_high_idx = swing_highs[-1] + recent_low_idx = swing_lows[-1] + + # Determine which one is more recent + if recent_high_idx > recent_low_idx: + # Downtrend: from high to low + start_price = historical_rates.high[recent_high_idx] + end_price = historical_rates.low[recent_low_idx] + logger.info(f"Detected downtrend from {start_price:.5f} to {end_price:.5f}") + else: + # Uptrend: from low to high + start_price = historical_rates.low[recent_low_idx] + end_price = historical_rates.high[recent_high_idx] + logger.info(f"Detected uptrend from {start_price:.5f} to {end_price:.5f}") + + # Calculate Fibonacci retracement levels + current_fib_levels = calculate_fibonacci_levels(start_price, end_price) + + # Log Fibonacci levels + logger.info("Fibonacci retracement levels:") + for ratio, price in current_fib_levels.items(): + logger.info(f" {ratio:.3f}: {price:.5f}") + + # Create and save Fibonacci retracement chart + plot_fibonacci_levels(historical_rates, swing_highs, swing_lows, current_fib_levels) + + last_swing_check_time = current_time + + # Only process trading logic if we have new tick data and Fibonacci levels + if current_tick.time_msc != prev_tick_time and current_fib_levels: + # Current price + current_price = current_tick.ask # Use ask price for analysis + + # Analyze price relative to Fibonacci levels + buy_signal, sell_signal, level_tested = analyze_retracement_signals( + current_price, current_fib_levels, tolerance=price_tolerance + ) + + # Log signals + if level_tested is not None: + logger.info(f"Price {current_price:.5f} testing Fibonacci level {level_tested:.3f}") + + if buy_signal: + logger.info(f"Buy signal generated at Fibonacci level {level_tested:.3f}") + elif sell_signal: + logger.info(f"Sell signal generated at Fibonacci level {level_tested:.3f}") + + # Execute trading based on signals + if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=buy_signal, + should_sell=sell_signal, + comment=f"Fibonacci {level_tested:.3f} Strategy" + if level_tested is not None + else "Fibonacci Strategy", + ) + + # Update trading statistics + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + + except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") + except Exception: + logger.exception("Error in strategy execution") + finally: + logger.info("Finishing the program.") + + +if __name__ == "__main__": + main() diff --git a/examples_of_expert_advisor/fimathe/README.md b/docs/examples/fimathe/README.md similarity index 100% rename from examples_of_expert_advisor/fimathe/README.md rename to docs/examples/fimathe/README.md diff --git a/examples_of_expert_advisor/fimathe/__init__.py b/docs/examples/fimathe/__init__.py similarity index 100% rename from examples_of_expert_advisor/fimathe/__init__.py rename to docs/examples/fimathe/__init__.py diff --git a/examples_of_expert_advisor/fimathe/eurusd_fimathe.py b/docs/examples/fimathe/eurusd_fimathe.py similarity index 100% rename from examples_of_expert_advisor/fimathe/eurusd_fimathe.py rename to docs/examples/fimathe/eurusd_fimathe.py diff --git a/examples_of_expert_advisor/fimathe/win_fimathe.py b/docs/examples/fimathe/win_fimathe.py similarity index 100% rename from examples_of_expert_advisor/fimathe/win_fimathe.py rename to docs/examples/fimathe/win_fimathe.py diff --git a/docs/examples/getting_started.py b/docs/examples/getting_started.py new file mode 100755 index 0000000..36daad0 --- /dev/null +++ b/docs/examples/getting_started.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Getting Started with MQPy. + +This example demonstrates the basic usage of the MQPy framework for algorithmic trading +with MetaTrader 5. It shows how to fetch market data, access price information, and +execute basic trading operations. +""" + +import logging + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def main() -> None: + """Main execution function for the Getting Started example.""" + # Step 1: Initialize the trading strategy with basic parameters + # This creates a Trade object that will handle your trading operations + trade = Trade( + expert_name="Getting Started Example", # Name of your trading strategy + version="1.0", # Version of your strategy + symbol="EURUSD", # Trading symbol/instrument + magic_number=123, # Unique identifier for your strategy's trades + lot=0.01, # Trade size in lots (0.01 = micro lot) + stop_loss=20, # Stop loss in points + emergency_stop_loss=100, # Emergency stop loss in points + take_profit=40, # Take profit in points + emergency_take_profit=120, # Emergency take profit in points + start_time="9:00", # Trading session start time + finishing_time="17:00", # Time to stop opening new positions + ending_time="17:30", # Time to close all positions + fee=0.0, # Commission fee + ) + + # Log that we're starting the strategy + logger.info(f"Starting Getting Started example on {trade.symbol}") + + # Step 2: Get current market data + # Fetch the current tick data (latest price) for our trading symbol + current_tick = Tick(trade.symbol) + + # Display the current price information + logger.info(f"Current price for {trade.symbol}:") + logger.info(f" Bid price: {current_tick.bid}") # Price at which you can sell + logger.info(f" Ask price: {current_tick.ask}") # Price at which you can buy + logger.info(f" Last price: {current_tick.last}") # Last executed price + + # Step 3: Get historical price data + # Fetch the last 10 candles of price data + historical_rates = Rates(trade.symbol, 10, 0, 1) # 10 candles, starting from the most recent (0), timeframe M1 + + # Display the historical price data + logger.info(f"Last 10 candles for {trade.symbol}:") + for i in range(min(3, len(historical_rates.time))): # Show only first 3 candles for brevity + logger.info(f" Candle {i+1}:") + logger.info(f" Open: {historical_rates.open[i]}") + logger.info(f" High: {historical_rates.high[i]}") + logger.info(f" Low: {historical_rates.low[i]}") + logger.info(f" Close: {historical_rates.close[i]}") + logger.info(f" Volume: {historical_rates.tick_volume[i]}") + + # Step 4: Prepare the trading environment + # This ensures the symbol is ready for trading + trade.prepare_symbol() + + # Step 5: Implement a very simple trading logic + # For this example, we'll use a simple condition: + # Buy if the current price is higher than the average of the last 10 candles + # Sell if the current price is lower than the average of the last 10 candles + + # Calculate the average price of the last 10 candles + average_price = sum(historical_rates.close) / len(historical_rates.close) + logger.info(f"Average closing price of last 10 candles: {average_price}") + + # Set up our trading signals + should_buy = current_tick.ask > average_price + should_sell = current_tick.bid < average_price + + # Log our trading decision + if should_buy: + logger.info(f"Buy signal generated: Current price ({current_tick.ask}) > Average price ({average_price})") + elif should_sell: + logger.info(f"Sell signal generated: Current price ({current_tick.bid}) < Average price ({average_price})") + else: + logger.info("No trading signal generated") + + # Step 6: Execute a trade if we're within trading hours + if trade.trading_time(): + logger.info("Within trading hours, executing trade if signals are present") + trade.open_position(should_buy=should_buy, should_sell=should_sell, comment="Getting Started Example Trade") + else: + logger.info("Outside trading hours, not executing any trades") + + # Step 7: Update trading statistics and log current positions + trade.statistics() + + # Step 8: Demonstrate how to close all positions at the end of the trading day + logger.info("Demonstrating position closing (not actually closing any positions)") + + # NOTE: In a real strategy, you would check if it's the end of the day and close positions + # This would be implemented by checking days_end() and calling close_position() + + logger.info("Getting started example completed") + + +if __name__ == "__main__": + main() diff --git a/docs/examples/indicator_connector_strategy.py b/docs/examples/indicator_connector_strategy.py new file mode 100755 index 0000000..b3e940c --- /dev/null +++ b/docs/examples/indicator_connector_strategy.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Indicator Connector Strategy Example. + +This example demonstrates how to use the Indicator Connector to retrieve indicator +values from MT5 custom indicators. The strategy uses a simple moving average +crossover logic based on indicator values received through the connector. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +from mqpy.indicators import Indicators +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def get_indicator_values( + indicators: Indicators, indicator_name: str, indicator_args: dict[str, Any] +) -> list[float] | None: + """Get indicator values from MT5 using the IndicatorConnector. + + Args: + indicators: The Indicators connector instance + indicator_name: Name of the MT5 indicator + indicator_args: Dictionary of arguments for the indicator + + Returns: + List of indicator values or None if the request failed + """ + try: + # Request indicator data from MT5 + response = indicators.get_indicator_data( + indicator_name=indicator_name, + symbol=indicator_args.get("symbol", ""), + timeframe=indicator_args.get("timeframe", 0), + count=indicator_args.get("count", 0), + lines=indicator_args.get("lines", [0]), + args=indicator_args.get("args", []), + ) + except Exception: + logger.exception("Exception while getting indicator values") + return None + + # Check if the request was successful (outside the try/except block) + if response is not None and response.error_code == 0: + return response.data[0] # Return the first line's data + + # Handle error case + logger.error(f"Error getting indicator values: {response.error_message if response else 'No response'}") + return None + + +def wait_for_connection(indicators: Indicators, max_attempts: int = 10) -> bool: + """Wait for the indicator connector to establish a connection. + + Args: + indicators: The Indicators connector instance + max_attempts: Maximum number of connection attempts + + Returns: + True if connection was established, False otherwise + """ + for attempt in range(max_attempts): + if indicators.is_connected(): + logger.info("Indicator connector successfully connected") + return True + + logger.info(f"Waiting for indicator connector to connect... Attempt {attempt + 1}/{max_attempts}") + time.sleep(1) + + logger.error("Failed to connect to indicator connector after maximum attempts") + return False + + +def analyze_moving_averages(fast_ma: list[float], slow_ma: list[float]) -> tuple[bool, bool]: + """Analyze moving averages to generate trading signals. + + Args: + fast_ma: Fast moving average values + slow_ma: Slow moving average values + + Returns: + Tuple of (buy_signal, sell_signal) + """ + if len(fast_ma) < 2 or len(slow_ma) < 2: + return False, False + + # Current values + current_fast = fast_ma[-1] + current_slow = slow_ma[-1] + + # Previous values + previous_fast = fast_ma[-2] + previous_slow = slow_ma[-2] + + # Generate signals based on crossover + buy_signal = previous_fast <= previous_slow and current_fast > current_slow + sell_signal = previous_fast >= previous_slow and current_fast < current_slow + + return buy_signal, sell_signal + + +def main() -> None: + """Main execution function for the Indicator Connector strategy.""" + # Initialize the trading strategy + trade = Trade( + expert_name="Indicator Connector Strategy", + version="1.0", + symbol="EURUSD", + magic_number=572, + lot=0.1, + stop_loss=30, + emergency_stop_loss=90, + take_profit=60, + emergency_take_profit=180, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting Indicator Connector strategy on {trade.symbol}") + + # Initialize the indicator connector + indicators = Indicators() + + # Wait for connection to be established + if not wait_for_connection(indicators): + logger.error("Could not connect to indicator connector - exiting") + return + + # Strategy parameters + prev_tick_time = 0 + # We'll use Moving Average indicator with different periods + fast_ma_period = 14 + slow_ma_period = 50 + + try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch current tick data + current_tick = Tick(trade.symbol) + + # Only process if we have a new tick + if current_tick.time_msc != prev_tick_time: + # Set up indicator arguments for fast MA + fast_ma_args = { + "symbol": trade.symbol, + "timeframe": 1, # 1-minute timeframe + "count": 10, # Get 10 values + "lines": [0], # The first line of the indicator + "args": [fast_ma_period, 0, 0], # Period, shift, MA method + } + + # Set up indicator arguments for slow MA + slow_ma_args = { + "symbol": trade.symbol, + "timeframe": 1, # 1-minute timeframe + "count": 10, # Get 10 values + "lines": [0], # The first line of the indicator + "args": [slow_ma_period, 0, 0], # Period, shift, MA method + } + + # Get indicator values + fast_ma_values = get_indicator_values(indicators, "Moving Average", fast_ma_args) + slow_ma_values = get_indicator_values(indicators, "Moving Average", slow_ma_args) + + # Check if we got valid data + if fast_ma_values and slow_ma_values: + # Log the current indicator values + logger.info(f"Fast MA ({fast_ma_period}): {fast_ma_values[-1]:.5f}") + logger.info(f"Slow MA ({slow_ma_period}): {slow_ma_values[-1]:.5f}") + + # Generate signals based on moving average crossovers + buy_signal, sell_signal = analyze_moving_averages(fast_ma_values, slow_ma_values) + + # Log signals + if buy_signal: + logger.info("Buy signal: Fast MA crossed above Slow MA") + elif sell_signal: + logger.info("Sell signal: Fast MA crossed below Slow MA") + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=buy_signal, should_sell=sell_signal, comment="Indicator Connector Strategy" + ) + + # Update trading statistics + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + + # Add a short delay to avoid excessive CPU usage + time.sleep(0.1) + + except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") + except Exception: + logger.exception("Error in strategy execution") + finally: + logger.info("Finishing the program.") + # Make sure to disconnect from the indicator connector + indicators.disconnect() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/market_depth_analysis.py b/docs/examples/market_depth_analysis.py new file mode 100755 index 0000000..24ae541 --- /dev/null +++ b/docs/examples/market_depth_analysis.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +"""Market Depth Analysis Example. + +This example demonstrates how to access and analyze market depth (DOM) data +from MetaTrader 5 using the MQPy library. It captures order book snapshots, +analyzes buy/sell pressure, and identifies potential support/resistance levels. + +Analysis Components: +------------------ +1. Market Depth Data Collection: + - Connects to MetaTrader 5 to retrieve real-time order book data + - Captures snapshots of the market depth at regular time intervals (10 seconds) + - Maintains a rolling history of the last 10 snapshots for trend analysis + +2. Order Book Analytics: + - Calculates key metrics including buy/sell volume ratio, order count, and volume distribution + - Tracks volume concentration at specific price levels + - Computes buy/sell percentages to gauge market sentiment + - Identifies imbalances between buying and selling pressure + +3. Support/Resistance Detection: + - Identifies price levels with unusually high volume concentration + - Uses a configurable threshold (default: 1.5x average volume) to detect significant levels + - Classifies levels as either support or resistance based on their position relative to current price + - Calculates a "strength" metric for each level based on its volume relative to the average + +4. Visualization: + - Creates horizontal bar charts showing the distribution of buy and sell orders + - Highlights detected support and resistance levels + - Includes annotations with key metrics (buy/sell ratio, volume percentages) + - Saves images at regular intervals for monitoring market structure evolution + +Applications: +----------- +- Order flow analysis for short-term trading decisions +- Identifying potential price reversal zones based on order concentration +- Gauging market sentiment through buy/sell imbalances +- Confirming technical analysis levels with actual order data +- Monitoring changes in market structure over time + +This tool provides valuable insights into market microstructure that are not visible on +traditional price charts, offering traders an additional dimension for analysis. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +import matplotlib.pyplot as plt + +from mqpy.tick import MqlBookInfo, Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def analyze_market_depth(book_info: list[MqlBookInfo]) -> tuple[dict[str, float], dict[float, int]]: + """Analyze market depth information to extract trading insights. + + Args: + book_info: List of MqlBookInfo objects containing the market depth data + + Returns: + Tuple containing market metrics and volume distribution by price + """ + if not book_info: + logger.warning("Empty market depth data received") + return {}, {} + + # Initialize counters + buy_volume = 0 + sell_volume = 0 + buy_orders = 0 + sell_orders = 0 + + # Dictionary to track volume at each price level + volume_by_price: dict[float, int] = {} + + # Process each entry in the order book + for entry in book_info: + # Track volume by price + volume_by_price[entry.price] = volume_by_price.get(entry.price, 0) + entry.volume + + if entry.type == 1: # Buy orders (bids) + buy_volume += entry.volume + buy_orders += 1 + elif entry.type == 2: # Sell orders (asks) + sell_volume += entry.volume + sell_orders += 1 + + # Calculate market metrics + total_volume = buy_volume + sell_volume + buy_percentage = (buy_volume / total_volume) * 100 if total_volume > 0 else 0 + sell_percentage = (sell_volume / total_volume) * 100 if total_volume > 0 else 0 + + # Calculate buy/sell ratio + buy_sell_ratio = buy_volume / sell_volume if sell_volume > 0 else float("inf") + + # Package results + metrics = { + "buy_volume": buy_volume, + "sell_volume": sell_volume, + "buy_orders": buy_orders, + "sell_orders": sell_orders, + "buy_percentage": buy_percentage, + "sell_percentage": sell_percentage, + "buy_sell_ratio": buy_sell_ratio, + "total_orders": buy_orders + sell_orders, + "total_volume": total_volume, + } + + return metrics, volume_by_price + + +def identify_support_resistance( + volume_by_price: dict[float, int], threshold_factor: float = 1.5 +) -> list[dict[str, Any]]: + """Identify potential support and resistance levels based on volume concentration. + + Args: + volume_by_price: Dictionary mapping price levels to their volume + threshold_factor: Multiplier for the average volume to identify significant levels + + Returns: + List of dictionaries containing identified support/resistance levels + """ + if not volume_by_price: + return [] + + # Calculate average volume + volumes = list(volume_by_price.values()) + avg_volume = sum(volumes) / len(volumes) + threshold = avg_volume * threshold_factor + + # Find price levels with volume above threshold + significant_levels = [] + + # Sort price levels for easier analysis + sorted_prices = sorted(volume_by_price.keys()) + + for price in sorted_prices: + volume = volume_by_price[price] + if volume > threshold: + # Determine if this is likely support or resistance + # This is a simplistic approach - in reality, you'd need more context + # We'll use a simple heuristic based on position in the order book + median_price = sorted_prices[len(sorted_prices) // 2] + level_type = "support" if price < median_price else "resistance" + + significant_levels.append( + { + "price": price, + "volume": volume, + "type": level_type, + "strength": volume / avg_volume, # Relative strength factor + } + ) + + return significant_levels + + +def plot_market_depth(book_info: list[MqlBookInfo], metrics: dict[str, float], levels: list[dict[str, Any]]) -> None: + """Visualize market depth data and identified support/resistance levels. + + Args: + book_info: List of MqlBookInfo objects containing the market depth data + metrics: Dictionary of market metrics from analyze_market_depth + levels: List of identified support/resistance levels + """ + try: + # Extract data for plotting + buy_prices = [] + buy_volumes = [] + sell_prices = [] + sell_volumes = [] + + for entry in book_info: + if entry.type == 1: # Buy orders + buy_prices.append(entry.price) + buy_volumes.append(entry.volume) + elif entry.type == 2: # Sell orders + sell_prices.append(entry.price) + sell_volumes.append(entry.volume) + + # Create the plot + plt.figure(figsize=(12, 8)) + + # Plot buy orders (bids) in green + plt.barh(buy_prices, buy_volumes, color="green", alpha=0.6, label="Bids (Buy Orders)") + + # Plot sell orders (asks) in red + plt.barh(sell_prices, sell_volumes, color="red", alpha=0.6, label="Asks (Sell Orders)") + + # Highlight support/resistance levels + for level in levels: + plt.axhline( + y=level["price"], + color="purple", + linestyle="--", + alpha=0.7, + label=f"{level['type'].capitalize()} ({level['price']})", + ) + + # Add labels and title + plt.title(f"Market Depth Analysis\nBuy/Sell Ratio: {metrics['buy_sell_ratio']:.2f}", fontsize=14) + plt.xlabel("Volume", fontsize=12) + plt.ylabel("Price", fontsize=12) + plt.grid(visible=True, alpha=0.3) + + # Add buy/sell percentage annotation + annotation_text = ( + f"Buy Volume: {metrics['buy_volume']} ({metrics['buy_percentage']:.1f}%)\n" + f"Sell Volume: {metrics['sell_volume']} ({metrics['sell_percentage']:.1f}%)\n" + f"Total Orders: {metrics['total_orders']}" + ) + plt.annotate( + annotation_text, + xy=(0.02, 0.02), + xycoords="axes fraction", + bbox={"boxstyle": "round,pad=0.5", "fc": "white", "alpha": 0.8}, + ) + + # Clean up the legend (limit to unique entries) + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc="best") + + # Save the plot + plt.tight_layout() + plt.savefig("market_depth_analysis.png") + logger.info("Market depth visualization saved as 'market_depth_analysis.png'") + plt.close() + except Exception: + logger.exception("Error creating market depth visualization") + + +def main() -> None: + """Main execution function for the market depth analysis.""" + # Initialize the trade object + trade = Trade( + expert_name="Market Depth Analyzer", + version="1.0", + symbol="EURUSD", + magic_number=573, + lot=0.1, + stop_loss=30, + emergency_stop_loss=90, + take_profit=60, + emergency_take_profit=180, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting Market Depth Analysis for {trade.symbol}") + + # Previous tick time to track new ticks + prev_tick_time = 0 + + # Track depth changes + depth_snapshots: list[list[MqlBookInfo]] = [] + snapshot_timestamps: list[int] = [] + last_snapshot_time = 0 + + try: + # Main loop + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch current tick data + current_tick = Tick(trade.symbol) + + # Only process if we have a new tick + if current_tick.time_msc != prev_tick_time: + try: + # Get market depth data + book_info = current_tick.get_book() + + # Check if we have valid market depth data + if book_info and len(book_info) > 0: + # Take snapshots at regular intervals (every 10 seconds) + current_time = int(time.time()) + if current_time - last_snapshot_time >= 10: + # Store the snapshot + depth_snapshots.append(book_info) + snapshot_timestamps.append(current_time) + last_snapshot_time = current_time + + # Keep only the last 10 snapshots + if len(depth_snapshots) > 10: + depth_snapshots.pop(0) + snapshot_timestamps.pop(0) + + # Analyze the current market depth + metrics, volume_by_price = analyze_market_depth(book_info) + + # Log key metrics + logger.info( + f"Market depth snapshot at {time.strftime('%H:%M:%S', time.localtime(current_time))}" + ) + logger.info(f"Buy/Sell Ratio: {metrics['buy_sell_ratio']:.2f}") + logger.info(f"Buy Volume: {metrics['buy_volume']} ({metrics['buy_percentage']:.1f}%)") + logger.info(f"Sell Volume: {metrics['sell_volume']} ({metrics['sell_percentage']:.1f}%)") + + # Identify potential support and resistance levels + levels = identify_support_resistance(volume_by_price) + + if levels: + logger.info("Potential support/resistance levels:") + for level in levels: + logger.info( + f"{level['type'].capitalize()} at {level['price']} " + f"(strength: {level['strength']:.1f}x average)" + ) + + # Create a visualization every 30 seconds + if len(depth_snapshots) % 3 == 0: + plot_market_depth(book_info, metrics, levels) + + except Exception: + logger.exception("Error processing market depth data") + + # Update previous tick time + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + logger.info("End of trading day reached.") + break + + # Add a short delay to avoid excessive CPU usage + time.sleep(0.1) + + except KeyboardInterrupt: + logger.info("Analysis interrupted by user") + except Exception: + logger.exception("Error in market depth analysis") + finally: + logger.info("Finishing market depth analysis") + + # Create a final visualization if we have data + if depth_snapshots: + try: + # Analyze the most recent snapshot + metrics, volume_by_price = analyze_market_depth(depth_snapshots[-1]) + levels = identify_support_resistance(volume_by_price) + plot_market_depth(depth_snapshots[-1], metrics, levels) + logger.info("Final market depth visualization saved") + except Exception: + logger.exception("Error creating final visualization") + + +if __name__ == "__main__": + main() diff --git a/docs/examples/rate_converter_example.py b/docs/examples/rate_converter_example.py new file mode 100755 index 0000000..9dbf401 --- /dev/null +++ b/docs/examples/rate_converter_example.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Rate Converter Example. + +This example demonstrates how to use the MQPy rate converter to convert between different +timeframes in MetaTrader 5, which is essential for multi-timeframe analysis strategies. +""" + +import logging + +import matplotlib.pyplot as plt +import MetaTrader5 as Mt5 +import numpy as np +import pandas as pd + +from mqpy.rate_converter import RateConverter +from mqpy.rates import Rates + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def print_rates_info(rates: Rates, timeframe_name: str) -> None: + """Print information about the rates data. + + Args: + rates: The rates data object + timeframe_name: Name of the timeframe for display + """ + logger.info(f"{timeframe_name} Timeframe Data:") + logger.info(f" Number of candles: {len(rates.time)}") + + if len(rates.time) > 0: + # Convert first timestamp to readable format + first_time = pd.to_datetime(rates.time[0], unit="s") + last_time = pd.to_datetime(rates.time[-1], unit="s") + + logger.info(f" Time range: {first_time} to {last_time}") + logger.info( + f" First candle: Open={rates.open[0]}, High={rates.high[0]}, Low={rates.low[0]}, Close={rates.close[0]}" + ) + logger.info( + f" Last candle: Open={rates.open[-1]}, High={rates.high[-1]}, Low={rates.low[-1]}, Close={rates.close[-1]}" + ) + + +def plot_multi_timeframe_data(m1_rates: Rates, m5_rates: Rates, h1_rates: Rates) -> None: + """Create a simple visualization of multi-timeframe data. + + Args: + m1_rates: 1-minute timeframe rates + m5_rates: 5-minute timeframe rates + h1_rates: 1-hour timeframe rates + """ + try: + # Create figure with 3 subplots + fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=False) + + # Convert timestamps to datetime for better x-axis labels + m1_times = pd.to_datetime(m1_rates.time, unit="s") + m5_times = pd.to_datetime(m5_rates.time, unit="s") + h1_times = pd.to_datetime(h1_rates.time, unit="s") + + # Plot M1 data + axes[0].plot(m1_times, m1_rates.close) + axes[0].set_title("1-Minute Timeframe") + axes[0].set_ylabel("Price") + axes[0].grid(visible=True) + + # Plot M5 data + axes[1].plot(m5_times, m5_rates.close) + axes[1].set_title("5-Minute Timeframe") + axes[1].set_ylabel("Price") + axes[1].grid(visible=True) + + # Plot H1 data + axes[2].plot(h1_times, h1_rates.close) + axes[2].set_title("1-Hour Timeframe") + axes[2].set_ylabel("Price") + axes[2].set_xlabel("Time") + axes[2].grid(visible=True) + + # Adjust layout + plt.tight_layout() + + # Save the plot + plt.savefig("multi_timeframe_analysis.png") + logger.info("Saved visualization to 'multi_timeframe_analysis.png'") + + # Show the plot if in interactive mode + plt.show() + + except Exception: + logger.exception("Error creating visualization") + + +def main() -> None: + """Main execution function for the Rate Converter example.""" + # Define the symbol to analyze + symbol = "EURUSD" + logger.info(f"Starting Rate Converter example for {symbol}") + + # Step 1: Get 1-minute timeframe data (100 candles) + m1_rates = Rates(symbol, 100, 0, Mt5.TIMEFRAME_M1) + print_rates_info(m1_rates, "M1") + + # Step 2: Use RateConverter to convert M1 to M5 timeframe + logger.info("Converting M1 to M5 timeframe...") + rate_converter = RateConverter() + + # Convert M1 data to M5 timeframe + m5_rates = rate_converter.convert_rates(rates=m1_rates, new_timeframe=Mt5.TIMEFRAME_M5, price_type="close") + print_rates_info(m5_rates, "M5 (converted from M1)") + + # Step 3: Use RateConverter to convert M1 to H1 timeframe + logger.info("Converting M1 to H1 timeframe...") + h1_rates = rate_converter.convert_rates(rates=m1_rates, new_timeframe=Mt5.TIMEFRAME_H1, price_type="close") + print_rates_info(h1_rates, "H1 (converted from M1)") + + # Step 4: Demonstrate a simple multi-timeframe analysis + logger.info("Performing multi-timeframe analysis...") + + # Calculate simple moving averages for different timeframes + # For M1 data, calculate a 20-period SMA + m1_sma = np.mean(m1_rates.close[-20:]) if len(m1_rates.close) >= 20 else None + + # For M5 data, calculate a 10-period SMA + m5_sma = np.mean(m5_rates.close[-10:]) if len(m5_rates.close) >= 10 else None + + # For H1 data, calculate a 5-period SMA + h1_sma = np.mean(h1_rates.close[-5:]) if len(h1_rates.close) >= 5 else None + + logger.info("Moving Average Results:") + logger.info(f" M1 20-period SMA: {m1_sma:.5f}" if m1_sma is not None else " M1 SMA: Not enough data") + logger.info(f" M5 10-period SMA: {m5_sma:.5f}" if m5_sma is not None else " M5 SMA: Not enough data") + logger.info(f" H1 5-period SMA: {h1_sma:.5f}" if h1_sma is not None else " H1 SMA: Not enough data") + + # Step 5: Create a simple visualization + logger.info("Creating visualization of multi-timeframe data...") + plot_multi_timeframe_data(m1_rates, m5_rates, h1_rates) + + logger.info("Rate Converter example completed") + + +if __name__ == "__main__": + main() diff --git a/docs/examples/rsi_strategy.py b/docs/examples/rsi_strategy.py new file mode 100755 index 0000000..4188120 --- /dev/null +++ b/docs/examples/rsi_strategy.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""RSI (Relative Strength Index) Strategy Example. + +This example demonstrates an RSI-based trading strategy using the mqpy framework. +The strategy enters long positions when RSI is below the oversold threshold and +enters short positions when RSI is above the overbought threshold. +""" + +from __future__ import annotations + +import logging + +import numpy as np + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def calculate_rsi(prices: list[float], period: int = 14) -> float | None: + """Calculate the Relative Strength Index. + + Args: + prices: A list of closing prices + period: The RSI period (default: 14) + + Returns: + The RSI value (0-100) or None if insufficient data + """ + if len(prices) < period + 1: + return None + + # Calculate price changes + deltas = np.diff(prices) + + # Separate gains and losses + gains = np.where(deltas > 0, deltas, 0) + losses = np.where(deltas < 0, -deltas, 0) + + # Calculate initial average gain and loss + avg_gain = np.mean(gains[:period]) + avg_loss = np.mean(losses[:period]) + + # Avoid division by zero + if avg_loss == 0: + return 100 + + # Calculate RS and RSI + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + return rsi + + +def main() -> None: + """Main execution function for the RSI strategy.""" + # Initialize the trading strategy + trade = Trade( + expert_name="RSI Strategy", + version="1.0", + symbol="EURUSD", + magic_number=568, + lot=0.1, + stop_loss=30, + emergency_stop_loss=90, + take_profit=60, + emergency_take_profit=180, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, + ) + + logger.info(f"Starting RSI strategy on {trade.symbol}") + + # Strategy parameters + prev_tick_time = 0 + rsi_period = 14 + overbought_threshold = 70 + oversold_threshold = 30 + + try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, rsi_period + 20, 0, 1) # Get extra data for reliability + + # Only process if we have a new tick and enough data for RSI calculation + if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= rsi_period + 1: + # Calculate RSI + rsi_value = calculate_rsi(historical_rates.close, rsi_period) + + if rsi_value is not None: + # Generate signals based on RSI thresholds + is_buy_signal = rsi_value < oversold_threshold + is_sell_signal = rsi_value > overbought_threshold + + # Log RSI values and signals + if is_buy_signal: + logger.info(f"Oversold condition: RSI = {rsi_value:.2f} (< {oversold_threshold})") + elif is_sell_signal: + logger.info(f"Overbought condition: RSI = {rsi_value:.2f} (> {overbought_threshold})") + else: + logger.debug(f"Current RSI: {rsi_value:.2f}") + + # Execute trading positions based on signals during allowed trading hours + if trade.trading_time(): + trade.open_position( + should_buy=is_buy_signal, + should_sell=is_sell_signal, + comment=f"RSI Strategy: {rsi_value:.2f}", + ) + + # Update trading statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + + except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") + except Exception: + logger.exception("Error in strategy execution") + finally: + logger.info("Finishing the program.") + + +if __name__ == "__main__": + main() diff --git a/docs/index.md b/docs/index.md index 9d287a9..15a056e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,28 @@ MQPy is a Python library designed to simplify the process of creating Expert Advisors for MetaTrader 5. While developing directly in MQL5 can be complex, MQPy provides a more streamlined experience using Python. +!!! warning "Trading Risk Warning" + **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** + + - Always use a **demo account** with fake money when testing strategies + - MQPy is provided for **educational purposes only** + - Past performance is not indicative of future results + - Never trade with money you cannot afford to lose + - The developers are not responsible for any financial losses + +## Examples and Strategies + +MQPy comes with a variety of [example strategies](examples.md) to help you get started, including: + +- Basic Moving Average Crossover +- RSI-based trading +- Bollinger Bands strategies +- Fibonacci Retracement patterns +- Multi-timeframe analysis +- Custom indicator integration + +Check out the [Examples](examples.md) page to see all available strategies. + ## Need Help or Found an Issue? If you need help or have found an issue, you can: diff --git a/docs/strategies/bollinger_bands.md b/docs/strategies/bollinger_bands.md new file mode 100644 index 0000000..6914c0d --- /dev/null +++ b/docs/strategies/bollinger_bands.md @@ -0,0 +1,307 @@ +# Bollinger Bands Trading Strategy + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + +## Overview + +Bollinger Bands are a versatile technical indicator created by John Bollinger that consist of three lines: + +| Component | Description | +|-----------|-------------| +| **Middle Band** | A simple moving average (SMA) of the price | +| **Upper Band** | The middle band plus a specific number of standard deviations (typically 2) | +| **Lower Band** | The middle band minus the same number of standard deviations | + +This strategy uses a mean reversion approach, which assumes that when prices move significantly away from their average, they tend to return to more normal levels: + +| Signal Type | Description | +|-------------|-------------| +| **Buy signal** | When price breaks below the lower band (suggesting the market is oversold) | +| **Sell signal** | When price breaks above the upper band (suggesting the market is overbought) | + +## Strategy Logic + +| Step | Description | +|------|-------------| +| 1 | Calculate the Bollinger Bands (middle, upper, and lower bands) | +| 2 | Generate buy signals when price falls below the lower band | +| 3 | Generate sell signals when price rises above the upper band | +| 4 | Execute trades only during specified trading hours | + +### Strategy Flow + +```mermaid +flowchart TD + A[Start] --> B[Fetch Market Data] + B --> C[Calculate Simple Moving Average] + C --> D[Calculate Price Standard Deviation] + D --> E["Calculate Upper Band:
SMA + 2ร—StdDev"] + D --> F["Calculate Lower Band:
SMA - 2ร—StdDev"] + E --> G{"Price > Upper
Band?"} + F --> H{"Price < Lower
Band?"} + G -->|Yes| I[Generate Sell Signal] + G -->|No| J[No Sell Signal] + H -->|Yes| K[Generate Buy Signal] + H -->|No| L[No Buy Signal] + I --> M{"Within Trading
Hours?"} + J --> M + K --> M + L --> M + M -->|Yes| N[Execute Trade] + M -->|No| O[Skip Trade Execution] + N --> P[Update Statistics] + O --> P + P --> Q[Wait for Next Tick] + Q --> B +``` + +## Code Implementation + +Let's break down the implementation step by step: + +### Step 1: Required Imports + +```python +from __future__ import annotations + +import logging +import numpy as np + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) +``` + +We import the necessary modules: +- Core MQPy modules for trading operations and data access +- `numpy` for efficient calculations of means and standard deviations +- `logging` for detailed tracking of the strategy's operation + +### Step 2: Bollinger Bands Calculation Function + +```python +def calculate_bollinger_bands(prices: list[float], period: int = 20, num_std_dev: float = 2.0) -> tuple[float, float, float] | None: + """Calculate Bollinger Bands (middle, upper, lower).""" + if len(prices) < period: + return None + + # Convert to numpy array for vectorized calculations + price_array = np.array(prices[-period:]) + + # Calculate SMA (middle band) + sma = np.mean(price_array) + + # Calculate standard deviation + std_dev = np.std(price_array) + + # Calculate upper and lower bands + upper_band = sma + (num_std_dev * std_dev) + lower_band = sma - (num_std_dev * std_dev) + + return (sma, upper_band, lower_band) +``` + +This function calculates the three components of the Bollinger Bands: +1. First, it checks if we have enough price data for the specified period +2. It converts the price data into a numpy array for more efficient calculations +3. It calculates the middle band, which is just the simple moving average (SMA) of the prices +4. It calculates the standard deviation of the prices over the specified period +5. It calculates the upper and lower bands by adding/subtracting the standard deviation (multiplied by a factor) from the middle band +6. It returns all three values as a tuple, or `None` if there's not enough data + +### Step 3: Initialize the Trading Strategy + +```python +trade = Trade( + expert_name="Bollinger Bands Strategy", + version="1.0", + symbol="EURUSD", + magic_number=569, + lot=0.1, + stop_loss=50, + emergency_stop_loss=150, + take_profit=100, + emergency_take_profit=300, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) +``` + +We configure our trading strategy with: +- Identification parameters: name, version, magic number +- Trading parameters: symbol, lot size +- Risk management parameters: stop loss and take profit (notice the take profit is 2x the stop loss) +- Trading session times: when to start, when to stop opening new positions, and when to close all positions + +### Step 4: Set Strategy Parameters + +```python +# Strategy parameters +prev_tick_time = 0 +bb_period = 20 +bb_std_dev = 2.0 +``` + +The key parameters for our Bollinger Bands strategy are: +- `bb_period`: The period for the calculation of the SMA and standard deviation (standard is 20) +- `bb_std_dev`: The number of standard deviations for the bands (standard is 2.0) + +### Step 5: Main Trading Loop + +```python +try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, bb_period + 10, 0, 1) # Get extra data for reliability +``` + +In the main loop, we: +- Prepare the symbol for trading +- Get the current market price +- Retrieve historical price data (we get bb_period + 10 bars for reliable calculations) + +### Step 6: Calculate Bollinger Bands + +```python +# Only process if we have a new tick +if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= bb_period: + # Calculate Bollinger Bands + bb_result = calculate_bollinger_bands( + historical_rates.close, + period=bb_period, + num_std_dev=bb_std_dev + ) + + if bb_result: + middle_band, upper_band, lower_band = bb_result + current_price = current_tick.last +``` + +For each new tick, we: +- Check that it's different from the previous tick to avoid redundant calculations +- Ensure we have enough historical data +- Calculate the Bollinger Bands using our custom function +- Extract the individual band values and the current price for signal generation + +### Step 7: Generate Trading Signals + +```python +# Generate signals based on price position relative to bands +# Buy when price crosses below lower band (potential bounce) +is_buy_signal = current_price < lower_band + +# Sell when price crosses above upper band (potential reversal) +is_sell_signal = current_price > upper_band + +# Log band data and signals +logger.info(f"Current price: {current_price:.5f}") +logger.info(f"Bollinger Bands - Middle: {middle_band:.5f}, Upper: {upper_band:.5f}, Lower: {lower_band:.5f}") + +if is_buy_signal: + logger.info(f"Buy signal: Price ({current_price:.5f}) below lower band ({lower_band:.5f})") +elif is_sell_signal: + logger.info(f"Sell signal: Price ({current_price:.5f}) above upper band ({upper_band:.5f})") +``` + +The signal generation logic is based on price comparison with the bands: +1. We generate a buy signal when the current price falls below the lower band +2. We generate a sell signal when the current price rises above the upper band +3. We log the current values of price and bands, as well as any signals generated + +### Step 8: Execute Trades + +```python +# Execute trading positions based on signals +if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=is_buy_signal, + should_sell=is_sell_signal, + comment="Bollinger Bands Strategy" + ) +``` + +When a signal is detected: +- We check if we're within the allowed trading hours +- If yes, we execute the appropriate trade based on our signals +- The comment identifies the strategy in the trading terminal + +### Step 9: Update State and Check for End of Day + +```python +# Update trading statistics +trade.statistics() + +prev_tick_time = current_tick.time_msc + +# Check if it's the end of the trading day +if trade.days_end(): + trade.close_position("End of the trading day reached.") + break +``` + +After processing each tick, we: +- Update the trading statistics for monitoring +- Store the current tick time for the next iteration +- Check if it's the end of the trading day, and if so, close positions and exit + +### Step 10: Error Handling + +```python +except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") +except Exception as e: + logger.error(f"Error in strategy execution: {e}") +finally: + logger.info("Finishing the program.") +``` + +Our error handling ensures: +- Clean exit when the user interrupts the program +- Logging of any errors that occur +- Proper cleanup in the `finally` block + +## Full Source Code + +You can find the complete source code for this strategy in the [MQPy GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/bollinger_bands_strategy.py). + +## Optimization Opportunities + +This strategy can be improved by: + +| Improvement | Description | +|-------------|-------------| +| **Trend Filter** | Using a longer-term moving average to only take trades in the direction of the overall trend | +| **Band Width Analysis** | Trading based on the width of the bands (narrowing and widening) to identify volatility changes | +| **Band Touch Strategy** | Waiting for the price to return to the middle band after touching an outer band | +| **Volume Confirmation** | Using volume information to confirm potential reversals | +| **Dynamic Deviation** | Adjusting the number of standard deviations based on market volatility | + +## Next Steps + +Try experimenting with: + +| Experiment | Options | +|------------|---------| +| Period Length | Shorter periods for more signals, longer periods for fewer but stronger signals | +| Standard Deviation | Higher values (2.5 or 3.0) for fewer but more reliable signals | +| Indicator Combinations | Combine with momentum indicators like RSI to confirm signals | +| Entry Refinement | Wait for reversal candle patterns after price breaks a band before entering | diff --git a/docs/strategies/fibonacci_retracement.md b/docs/strategies/fibonacci_retracement.md new file mode 100644 index 0000000..e20ceba --- /dev/null +++ b/docs/strategies/fibonacci_retracement.md @@ -0,0 +1,485 @@ +# Fibonacci Retracement Trading Strategy + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + +## Overview + +The Fibonacci Retracement strategy is based on the mathematical sequence discovered by Leonardo Fibonacci. In financial markets, traders use Fibonacci ratios to identify potential reversal levels after a significant price movement. + +| Retracement Level | Description | +|-------------------|-------------| +| **0.0%** | The starting point of the move | +| **23.6%** | Minor retracement level | +| **38.2%** | Moderate retracement level, often significant | +| **50.0%** | Not a Fibonacci ratio but widely used in trading | +| **61.8%** | The Golden Ratio, typically the strongest level | +| **78.6%** | Deep retracement, often the last level before reversal | +| **100.0%** | Complete retracement to the starting point | + +This strategy identifies significant swing points in the market, calculates Fibonacci levels between them, and trades when price interacts with these levels. + +| Signal Type | Description | +|-------------|-------------| +| **Buy signal** | When price bounces off a Fibonacci retracement level during an uptrend | +| **Sell signal** | When price bounces off a Fibonacci retracement level during a downtrend | + +## Strategy Logic + +| Step | Description | +|------|-------------| +| 1 | Identify significant swing highs and lows in the price data | +| 2 | Calculate Fibonacci retracement levels between the most recent swing points | +| 3 | Determine if the market is in an uptrend or downtrend | +| 4 | Generate buy signals when price bounces off Fibonacci levels during uptrends | +| 5 | Generate sell signals when price bounces off Fibonacci levels during downtrends | +| 6 | Visualize price action with Fibonacci levels for analysis | + +### Strategy Flow + +```mermaid +flowchart TD + A[Start] --> B[Fetch Market Data] + B --> C[Identify Swing Highs and Lows] + C --> D[Determine Market Trend] + D --> E[Calculate Fibonacci Retracement Levels] + E --> F[Monitor Price Action] + F --> G{"Price at Fibonacci
Level?"} + G -->|No| F + G -->|Yes| H{"In Uptrend?"} + H -->|Yes| I[Generate Buy Signal] + H -->|No| J[Generate Sell Signal] + I --> K{"Within Trading
Hours?"} + J --> K + K -->|Yes| L[Execute Trade] + K -->|No| M[Skip Trade Execution] + L --> N[Update Statistics] + M --> N + N --> O[Wait for Next Tick] + O --> P{"Recalculation
Interval Passed?"} + P -->|Yes| B + P -->|No| F +``` + +## Code Implementation + +Let's break down the implementation step by step: + +### Step 1: Required Imports + +```python +from __future__ import annotations + +import logging +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from mpl_finance import candlestick_ohlc +import matplotlib.dates as mdates + +import MetaTrader5 as Mt5 + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Fibonacci retracement levels (standard) +FIBONACCI_LEVELS = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] +``` + +We import the necessary modules: +- Core MQPy modules for trading operations and data access +- Visualization libraries for creating charts with Fibonacci levels +- `numpy` and `pandas` for data manipulation +- Type hints for better code readability +- The standard Fibonacci levels are defined as a constant + +### Step 2: Swing Point Detection Function + +```python +def find_swing_points(prices: list[float], window: int = 5) -> tuple[list[int], list[int]]: + """Find swing high and swing low points in price data. + + Args: + prices: List of price values + window: Window size for determining swing points + + Returns: + Tuple containing lists of swing high and swing low indices + """ + highs: list[int] = [] + lows: list[int] = [] + + if len(prices) < 2 * window + 1: + return highs, lows + + for i in range(window, len(prices) - window): + # Check for swing high + if all(prices[i] > prices[i-j] for j in range(1, window+1)) and \ + all(prices[i] > prices[i+j] for j in range(1, window+1)): + highs.append(i) + + # Check for swing low + if all(prices[i] < prices[i-j] for j in range(1, window+1)) and \ + all(prices[i] < prices[i+j] for j in range(1, window+1)): + lows.append(i) + + return highs, lows +``` + +This function identifies swing highs and lows in the price data: +1. It takes a list of prices and a window parameter that determines how significant a swing point needs to be +2. It checks each price point to see if it's higher than all points within the window (for highs) or lower than all points within the window (for lows) +3. It returns two lists containing the indices of the identified swing highs and lows + +### Step 3: Fibonacci Level Calculation + +```python +def calculate_fibonacci_levels(start_price: float, end_price: float) -> dict[float, float]: + """Calculate Fibonacci retracement price levels. + + Args: + start_price: Starting price for retracement (swing high/low) + end_price: Ending price for retracement (swing low/high) + + Returns: + Dictionary mapping Fibonacci ratios to price levels + """ + diff = end_price - start_price + levels = {} + + for ratio in FIBONACCI_LEVELS: + levels[ratio] = start_price + diff * ratio + + return levels +``` + +This function calculates the actual Fibonacci retracement levels: +1. It takes the starting and ending price points (typically swing high and low) +2. It calculates the price difference between them +3. It applies each Fibonacci ratio to the price difference to determine the retracement levels +4. It returns a dictionary mapping the ratios to the corresponding price levels + +### Step 4: Visualization Function + +```python +def plot_fibonacci_levels( + rates: Rates, + swing_highs: list[int], + swing_lows: list[int], + retracement_levels: dict[float, float] | None = None +) -> None: + """Plot price chart with swing points and Fibonacci retracement levels. + + Args: + rates: Rate data containing OHLC prices and times + swing_highs: Indices of swing high points + swing_lows: Indices of swing low points + retracement_levels: Dictionary of Fibonacci levels to plot + """ + try: + # Convert rates data to pandas DataFrame + data = [] + for i in range(len(rates.time)): + # Convert to matplotlib date format + date = mdates.date2num(pd.to_datetime(rates.time[i], unit='s')) + data.append([date, rates.open[i], rates.high[i], rates.low[i], rates.close[i]]) + + df = pd.DataFrame(data, columns=['Date', 'Open', 'High', 'Low', 'Close']) + + # Create plot + fig, ax = plt.subplots(figsize=(12, 8)) + + # Plot candlestick chart + candlestick_ohlc(ax, df.values, width=0.001, colorup='green', colordown='red', alpha=0.8) + + # Format date axis + ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M')) + plt.xticks(rotation=45) + + # Mark swing highs and lows + for idx in swing_highs: + ax.plot(mdates.date2num(pd.to_datetime(rates.time[idx], unit='s')), + rates.high[idx], 'ro', markersize=8) + + for idx in swing_lows: + ax.plot(mdates.date2num(pd.to_datetime(rates.time[idx], unit='s')), + rates.low[idx], 'go', markersize=8) + + # Plot Fibonacci retracement levels if provided + if retracement_levels: + colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k'] + for i, (ratio, price) in enumerate(retracement_levels.items()): + ax.axhline(y=price, color=colors[i % len(colors)], linestyle='--', + label=f'Fib {ratio:.3f}: {price:.5f}') + + plt.title('EURUSD with Fibonacci Retracement Levels') + plt.ylabel('Price') + plt.legend() + plt.tight_layout() + + # Save the chart + plt.savefig('fibonacci_retracement.png') + logger.info("Chart saved as 'fibonacci_retracement.png'") + + plt.close() + + except Exception: + logger.exception("Error creating Fibonacci retracement chart") +``` + +This visualization function creates a candlestick chart with the Fibonacci levels: +1. It converts the price data to a pandas DataFrame +2. It creates a candlestick chart of the price action +3. It marks the identified swing highs and lows with colored dots +4. It draws horizontal lines for each Fibonacci retracement level +5. It saves the chart to a file for analysis + +### Step 5: Signal Generation Function + +```python +def analyze_retracement_signals( + current_price: float, + fib_levels: dict[float, float], + tolerance: float = 0.0001 +) -> tuple[bool, bool, Optional[float]]: + """Analyze price position relative to Fibonacci levels and generate signals. + + Args: + current_price: Current market price + fib_levels: Dictionary of Fibonacci retracement levels + tolerance: Price tolerance for level testing + + Returns: + Tuple containing (buy_signal, sell_signal, level_tested) + """ + buy_signal = False + sell_signal = False + level_tested = None + + # Determine trend direction from Fibonacci levels + uptrend = fib_levels[0.0] < fib_levels[1.0] + + for ratio, level in fib_levels.items(): + # Check if price is near a Fibonacci level + if abs(current_price - level) < tolerance: + level_tested = ratio + + # In uptrend, bounces off retracement levels can be buy signals + # Focus on the most reliable levels: 0.382, 0.5, 0.618 + if uptrend and ratio in [0.382, 0.5, 0.618]: + buy_signal = True + + # In downtrend, bounces off retracement levels can be sell signals + elif not uptrend and ratio in [0.382, 0.5, 0.618]: + sell_signal = True + + break + + return buy_signal, sell_signal, level_tested +``` + +This function analyzes the current price in relation to the Fibonacci levels: +1. It checks if the current price is close to any Fibonacci level (within a specified tolerance) +2. It determines if the market is in an uptrend or downtrend based on the direction of the Fibonacci levels +3. It generates buy signals during uptrends and sell signals during downtrends +4. It focuses on the most statistically significant levels (38.2%, 50%, 61.8%) +5. It returns the signals and the specific level being tested + +### Step 6: Initialize the Trading Strategy + +```python +trade = Trade( + expert_name="Fibonacci Retracement Strategy", + version="1.0", + symbol="EURUSD", + magic_number=571, + lot=0.1, + stop_loss=35, + emergency_stop_loss=100, + take_profit=70, + emergency_take_profit=200, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) +``` + +We initialize the trading parameters with: +- EURUSD as the trading instrument +- A magic number of 571 to identify this strategy's trades +- Stop loss and take profit levels (note that take profit is 2x the stop loss for a positive risk-reward ratio) +- Trading session times that limit when trades can be executed + +### Step 7: Main Trading Loop + +```python +# Strategy parameters +prev_tick_time = 0 +lookback_period = 100 # Number of candles to analyze +swing_window = 5 # Window for swing high/low detection +price_tolerance = 0.0001 # Tolerance for testing price at Fibonacci levels + +# Variables to track state +current_fib_levels = None +last_swing_check_time = 0 +swing_recalculation_interval = 10 * 60 # Recalculate swings every 10 minutes + +try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data (H1 timeframe for identifying swings) + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, lookback_period, 0, 60) # H1 timeframe + + current_time = current_tick.time +``` + +In the main loop: +- We set up strategy parameters, including how many candles to analyze and how sensitive the swing detection should be +- We initialize state tracking variables for the Fibonacci levels and when to recalculate them +- We prepare the symbol for trading and fetch the current price and historical data +- We use a 1-hour timeframe (60 minutes) for identifying swing points + +### Step 8: Swing Point and Fibonacci Level Calculation + +```python +# Only recalculate swing points and Fibonacci levels periodically +if (current_time - last_swing_check_time) > swing_recalculation_interval: + # Find swing highs and lows + swing_highs, swing_lows = find_swing_points(historical_rates.close, window=swing_window) + + # Calculate Fibonacci levels if we have swing points + if len(swing_highs) > 0 and len(swing_lows) > 0: + # Use the most recent swing high and swing low + recent_high_idx = swing_highs[-1] + recent_low_idx = swing_lows[-1] + + # Determine which one is more recent + if recent_high_idx > recent_low_idx: + # Downtrend: from high to low + start_price = historical_rates.high[recent_high_idx] + end_price = historical_rates.low[recent_low_idx] + logger.info(f"Detected downtrend from {start_price:.5f} to {end_price:.5f}") + else: + # Uptrend: from low to high + start_price = historical_rates.low[recent_low_idx] + end_price = historical_rates.high[recent_high_idx] + logger.info(f"Detected uptrend from {start_price:.5f} to {end_price:.5f}") + + # Calculate Fibonacci retracement levels + current_fib_levels = calculate_fibonacci_levels(start_price, end_price) + + # Log Fibonacci levels + logger.info("Fibonacci retracement levels:") + for ratio, price in current_fib_levels.items(): + logger.info(f" {ratio:.3f}: {price:.5f}") + + # Create and save Fibonacci retracement chart + plot_fibonacci_levels(historical_rates, swing_highs, swing_lows, current_fib_levels) + + last_swing_check_time = current_time +``` + +We recalculate swing points and Fibonacci levels periodically: +1. This saves processing power and avoids too-frequent changes in the levels +2. We find swing highs and lows using our defined function +3. We identify the most recent swing high and low to use for calculating Fibonacci levels +4. We determine if the market is in an uptrend or downtrend based on which swing point is more recent +5. We calculate the Fibonacci levels, log them, and create a visual chart + +### Step 9: Signal Generation and Trading + +```python +# Only process trading logic if we have new tick data and Fibonacci levels +if current_tick.time_msc != prev_tick_time and current_fib_levels: + # Current price + current_price = current_tick.ask # Use ask price for analysis + + # Analyze price relative to Fibonacci levels + buy_signal, sell_signal, level_tested = analyze_retracement_signals( + current_price, current_fib_levels, tolerance=price_tolerance + ) + + # Log signals + if level_tested is not None: + logger.info(f"Price {current_price:.5f} testing Fibonacci level {level_tested:.3f}") + + if buy_signal: + logger.info(f"Buy signal generated at Fibonacci level {level_tested:.3f}") + elif sell_signal: + logger.info(f"Sell signal generated at Fibonacci level {level_tested:.3f}") + + # Execute trading based on signals + if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=buy_signal, + should_sell=sell_signal, + comment=f"Fibonacci {level_tested:.3f} Strategy" if level_tested is not None else "Fibonacci Strategy" + ) + + # Update trading statistics + trade.statistics() + + prev_tick_time = current_tick.time_msc +``` + +For each new tick: +1. We analyze the current price in relation to the calculated Fibonacci levels +2. We generate buy or sell signals based on price interaction with the levels +3. We log detailed information about which level is being tested and which signal is generated +4. We execute trades only during the allowed trading hours +5. We include the specific Fibonacci level in the trade comment for reference + +### Step 10: End of Day Handling and Error Management + +```python +# Check if it's the end of the trading day +if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + +except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") +except Exception: + logger.exception("Error in strategy execution") +finally: + logger.info("Finishing the program.") +``` + +We include proper handling for: +- End of trading day detection and position closure +- User interruptions through keyboard input +- Exceptions with detailed logging +- Cleanup operations in the finally block + +## Potential Enhancements + +The Fibonacci Retracement strategy can be improved with: + +1. **Volume Confirmation**: Add volume analysis to confirm price reactions at Fibonacci levels +2. **Multiple Timeframe Analysis**: Incorporate Fibonacci levels from higher timeframes for stronger confluence +3. **Trend Filter**: Add a trend indicator to avoid counter-trend trades +4. **Pattern Recognition**: Integrate candlestick pattern recognition at Fibonacci levels +5. **Dynamic Stop-Loss**: Implement trailing stops based on the identified swing points + +## Conclusion + +The Fibonacci Retracement strategy combines ancient mathematical principles with modern algorithmic trading. By identifying swing points and calculating retracement levels, it provides a systematic way to find potential reversal points in the market. + +While not a perfect prediction tool, when combined with proper risk management and additional confirmation factors, it can be a valuable addition to a trader's arsenal of strategies. diff --git a/docs/strategies/market_depth_analysis.md b/docs/strategies/market_depth_analysis.md new file mode 100644 index 0000000..03d740b --- /dev/null +++ b/docs/strategies/market_depth_analysis.md @@ -0,0 +1,484 @@ +# Market Depth Analysis Strategy + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + +## Overview + +Market Depth Analysis (also known as DOM or Depth of Market analysis) examines the order book data to understand supply and demand dynamics that aren't visible on price charts alone. This approach allows traders to: + +1. See the actual buy and sell orders waiting to be executed at various price levels +2. Identify potential support and resistance levels based on order concentration +3. Gauge market sentiment through imbalances in buy and sell orders +4. Anticipate potential price movements based on order flow patterns + +Market depth data consists of: + +| Component | Description | +|-----------|-------------| +| **Buy Orders (Bids)** | Orders to buy at specific prices, appearing below current market price | +| **Sell Orders (Asks)** | Orders to sell at specific prices, appearing above current market price | +| **Volume** | The quantity associated with each order at a particular price level | + +This analysis tool doesn't generate direct trading signals but provides valuable context for making informed trading decisions. + +## Analysis Logic + +| Step | Description | +|------|-------------| +| 1 | Connect to MetaTrader 5 and retrieve order book (market depth) data | +| 2 | Analyze buy/sell order distribution and calculate key metrics | +| 3 | Identify price levels with unusually high order concentration | +| 4 | Visualize the order book structure and highlight significant levels | +| 5 | Track changes in order book structure over time | + +### Analysis Flow + +```mermaid +flowchart TD + A[Start] --> B[Connect to MT5] + B --> C[Fetch Market Depth Data] + C --> D[Calculate Buy/Sell Metrics] + D --> E[Calculate Volume Distribution] + E --> F[Identify Significant
Price Levels] + F --> G[Classify as Support
or Resistance] + G --> H[Create Visualization] + H --> I[Log Analysis Results] + I --> J[Wait for Next Snapshot
Interval] + J --> C +``` + +## Code Implementation + +Let's break down the implementation step by step: + +### Step 1: Required Imports + +```python +from __future__ import annotations + +import logging +import time + +import matplotlib.pyplot as plt + +from mqpy.tick import MqlBookInfo, Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) +``` + +We import the necessary modules: +- Core MQPy modules for accessing market data and trading functionality +- `matplotlib` for visualizing the market depth data +- `logging` for tracking the analysis process +- Type hints for better code readability + +### Step 2: Market Depth Analysis Function + +```python +def analyze_market_depth(book_info: list[MqlBookInfo]) -> tuple[dict[str, float], dict[float, int]]: + """Analyze market depth information to extract trading insights. + + Args: + book_info: List of MqlBookInfo objects containing the market depth data + + Returns: + Tuple containing market metrics and volume distribution by price + """ + if not book_info: + logger.warning("Empty market depth data received") + return {}, {} + + # Initialize counters + buy_volume = 0 + sell_volume = 0 + buy_orders = 0 + sell_orders = 0 + + # Dictionary to track volume at each price level + volume_by_price: dict[float, int] = {} + + # Process each entry in the order book + for entry in book_info: + # Track volume by price + volume_by_price[entry.price] = volume_by_price.get(entry.price, 0) + entry.volume + + if entry.type == 1: # Buy orders (bids) + buy_volume += entry.volume + buy_orders += 1 + elif entry.type == 2: # Sell orders (asks) + sell_volume += entry.volume + sell_orders += 1 + + # Calculate market metrics + total_volume = buy_volume + sell_volume + buy_percentage = (buy_volume / total_volume) * 100 if total_volume > 0 else 0 + sell_percentage = (sell_volume / total_volume) * 100 if total_volume > 0 else 0 + + # Calculate buy/sell ratio + buy_sell_ratio = buy_volume / sell_volume if sell_volume > 0 else float("inf") + + # Package results + metrics = { + "buy_volume": buy_volume, + "sell_volume": sell_volume, + "buy_orders": buy_orders, + "sell_orders": sell_orders, + "buy_percentage": buy_percentage, + "sell_percentage": sell_percentage, + "buy_sell_ratio": buy_sell_ratio, + "total_orders": buy_orders + sell_orders, + "total_volume": total_volume, + } + + return metrics, volume_by_price +``` + +This function analyzes the raw order book data: +1. It processes each entry in the market depth to track buy and sell volumes +2. It maintains a dictionary that maps price levels to their total volume +3. It calculates key metrics including buy/sell ratio and percentage distribution +4. It returns both the summary metrics and the volume distribution for further analysis + +### Step 3: Support/Resistance Detection Function + +```python +def identify_support_resistance( + volume_by_price: dict[float, int], threshold_factor: float = 1.5 +) -> list[dict[str, Any]]: + """Identify potential support and resistance levels based on volume concentration. + + Args: + volume_by_price: Dictionary mapping price levels to their volume + threshold_factor: Multiplier for the average volume to identify significant levels + + Returns: + List of dictionaries containing identified support/resistance levels + """ + if not volume_by_price: + return [] + + # Calculate average volume + volumes = list(volume_by_price.values()) + avg_volume = sum(volumes) / len(volumes) + threshold = avg_volume * threshold_factor + + # Find price levels with volume above threshold + significant_levels = [] + + # Sort price levels for easier analysis + sorted_prices = sorted(volume_by_price.keys()) + + for price in sorted_prices: + volume = volume_by_price[price] + if volume > threshold: + # Determine if this is likely support or resistance + # This is a simplistic approach - in reality, you'd need more context + # We'll use a simple heuristic based on position in the order book + median_price = sorted_prices[len(sorted_prices) // 2] + level_type = "support" if price < median_price else "resistance" + + significant_levels.append( + { + "price": price, + "volume": volume, + "type": level_type, + "strength": volume / avg_volume, # Relative strength factor + } + ) + + return significant_levels +``` + +This function identifies potential support and resistance levels: +1. It calculates the average volume across all price levels +2. It identifies price levels with volume significantly above average (using a threshold factor) +3. It classifies each level as support or resistance based on its position relative to the median price +4. It calculates a "strength" factor for each level based on how much its volume exceeds the average +5. It returns a list of identified levels with their characteristics + +### Step 4: Visualization Function + +```python +def plot_market_depth(book_info: list[MqlBookInfo], metrics: dict[str, float], levels: list[dict[str, Any]]) -> None: + """Visualize market depth data and identified support/resistance levels. + + Args: + book_info: List of MqlBookInfo objects containing the market depth data + metrics: Dictionary of market metrics from analyze_market_depth + levels: List of identified support/resistance levels + """ + try: + # Extract data for plotting + buy_prices = [] + buy_volumes = [] + sell_prices = [] + sell_volumes = [] + + for entry in book_info: + if entry.type == 1: # Buy orders + buy_prices.append(entry.price) + buy_volumes.append(entry.volume) + elif entry.type == 2: # Sell orders + sell_prices.append(entry.price) + sell_volumes.append(entry.volume) + + # Create the plot + plt.figure(figsize=(12, 8)) + + # Plot buy orders (bids) in green + plt.barh(buy_prices, buy_volumes, color="green", alpha=0.6, label="Bids (Buy Orders)") + + # Plot sell orders (asks) in red + plt.barh(sell_prices, sell_volumes, color="red", alpha=0.6, label="Asks (Sell Orders)") + + # Highlight support/resistance levels + for level in levels: + plt.axhline( + y=level["price"], + color="purple", + linestyle="--", + alpha=0.7, + label=f"{level['type'].capitalize()} ({level['price']})", + ) + + # Add labels and title + plt.title(f"Market Depth Analysis\nBuy/Sell Ratio: {metrics['buy_sell_ratio']:.2f}", fontsize=14) + plt.xlabel("Volume", fontsize=12) + plt.ylabel("Price", fontsize=12) + plt.grid(True, alpha=0.3) + + # Add buy/sell percentage annotation + annotation_text = ( + f"Buy Volume: {metrics['buy_volume']} ({metrics['buy_percentage']:.1f}%)\n" + f"Sell Volume: {metrics['sell_volume']} ({metrics['sell_percentage']:.1f}%)\n" + f"Total Orders: {metrics['total_orders']}" + ) + plt.annotate( + annotation_text, + xy=(0.02, 0.02), + xycoords="axes fraction", + bbox=dict(boxstyle="round,pad=0.5", fc="white", alpha=0.8), + ) + + # Clean up the legend (limit to unique entries) + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc="best") + + # Save the plot + plt.tight_layout() + plt.savefig("market_depth_analysis.png") + logger.info("Market depth visualization saved as 'market_depth_analysis.png'") + plt.close() + except Exception: + logger.exception("Error creating market depth visualization") +``` + +This visualization function creates a horizontal bar chart of the order book: +1. It separates buy and sell orders for plotting +2. It creates a horizontal bar chart with buy orders in green and sell orders in red +3. It highlights identified support/resistance levels with horizontal lines +4. It adds annotations with key metrics like buy/sell ratio and percentages +5. It saves the visualization to a file for later analysis + +### Step 5: Initialize the Trade Object + +```python +trade = Trade( + expert_name="Market Depth Analyzer", + version="1.0", + symbol="EURUSD", + magic_number=573, + lot=0.1, + stop_loss=30, + emergency_stop_loss=90, + take_profit=60, + emergency_take_profit=180, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) +``` + +We initialize the Trade object to: +- Identify our analysis tool with a name and version +- Specify the trading instrument (EURUSD) +- Set a unique magic number +- Configure risk management parameters (even though this is primarily an analysis tool, not a trading strategy) +- Define session trading hours + +### Step 6: Main Analysis Loop + +```python +# Previous tick time to track new ticks +prev_tick_time = 0 + +# Track depth changes +depth_snapshots: list[list[MqlBookInfo]] = [] +snapshot_timestamps: list[int] = [] +last_snapshot_time = 0 + +try: + # Main loop + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch current tick data + current_tick = Tick(trade.symbol) + + # Only process if we have a new tick + if current_tick.time_msc != prev_tick_time: + try: + # Get market depth data + book_info = current_tick.get_book() +``` + +In the main loop: +- We initialize variables to track tick times and store snapshots +- We prepare the symbol and get the current tick data +- We only process new ticks to avoid redundant calculations +- We retrieve the current market depth (order book) data + +### Step 7: Snapshot Collection and Analysis + +```python +# Check if we have valid market depth data +if book_info and len(book_info) > 0: + # Take snapshots at regular intervals (every 10 seconds) + current_time = int(time.time()) + if current_time - last_snapshot_time >= 10: + # Store the snapshot + depth_snapshots.append(book_info) + snapshot_timestamps.append(current_time) + last_snapshot_time = current_time + + # Keep only the last 10 snapshots + if len(depth_snapshots) > 10: + depth_snapshots.pop(0) + snapshot_timestamps.pop(0) + + # Analyze the current market depth + metrics, volume_by_price = analyze_market_depth(book_info) + + # Log key metrics + logger.info( + f"Market depth snapshot at {time.strftime('%H:%M:%S', time.localtime(current_time))}" + ) + logger.info(f"Buy/Sell Ratio: {metrics['buy_sell_ratio']:.2f}") + logger.info(f"Buy Volume: {metrics['buy_volume']} ({metrics['buy_percentage']:.1f}%)") + logger.info(f"Sell Volume: {metrics['sell_volume']} ({metrics['sell_percentage']:.1f}%)") +``` + +We collect and analyze market depth snapshots: +1. We take snapshots at regular intervals (every 10 seconds) +2. We maintain a rolling history of the last 10 snapshots +3. We analyze each snapshot to calculate key metrics +4. We log the results for monitoring + +### Step 8: Support/Resistance Identification and Visualization + +```python +# Identify potential support and resistance levels +levels = identify_support_resistance(volume_by_price) + +if levels: + logger.info("Potential support/resistance levels:") + for level in levels: + logger.info( + f"{level['type'].capitalize()} at {level['price']} " + f"(strength: {level['strength']:.1f}x average)" + ) + +# Create a visualization every 30 seconds +if len(depth_snapshots) % 3 == 0: + plot_market_depth(book_info, metrics, levels) +``` + +For each analyzed snapshot: +1. We identify potential support and resistance levels +2. We log the identified levels with their characteristics +3. We create a visualization every 30 seconds (every 3rd snapshot) + +### Step 9: Error Handling and Shutdown + +```python +except Exception: + logger.exception("Error processing market depth data") + +# Update previous tick time +prev_tick_time = current_tick.time_msc + +# Check if it's the end of the trading day +if trade.days_end(): + logger.info("End of trading day reached.") + break + +# Add a short delay to avoid excessive CPU usage +time.sleep(0.1) + +except KeyboardInterrupt: + logger.info("Analysis interrupted by user") +except Exception: + logger.exception("Error in market depth analysis") +finally: + logger.info("Finishing market depth analysis") + + # Create a final visualization if we have data + if depth_snapshots: + try: + # Analyze the most recent snapshot + metrics, volume_by_price = analyze_market_depth(depth_snapshots[-1]) + levels = identify_support_resistance(volume_by_price) + plot_market_depth(depth_snapshots[-1], metrics, levels) + logger.info("Final market depth visualization saved") + except Exception: + logger.exception("Error creating final visualization") +``` + +We implement comprehensive error handling: +1. We catch and log exceptions during market depth processing +2. We check for the end of the trading day +3. We handle user interruptions +4. We create a final visualization before shutting down + +## Interpreting the Results + +The Market Depth analysis provides several key insights: + +| Metric | Interpretation | +|--------|----------------| +| **Buy/Sell Ratio > 1.0** | Indicates stronger buying pressure; potentially bullish | +| **Buy/Sell Ratio < 1.0** | Indicates stronger selling pressure; potentially bearish | +| **Strong Support Level** | A price level with high buy order concentration; may prevent price from falling further | +| **Strong Resistance Level** | A price level with high sell order concentration; may prevent price from rising further | +| **Level Strength** | How significant a support/resistance level is compared to average (higher is stronger) | + +## Practical Applications + +Here are some ways to apply market depth analysis to your trading: + +1. **Confirmation Tool**: Use identified support/resistance levels to confirm technical analysis from price charts. + +2. **Entry/Exit Timing**: Watch how price interacts with levels of high order concentration for optimal trade timing. + +3. **Stop-Loss Placement**: Set stops beyond significant support/resistance levels that have high order volume. + +4. **Market Sentiment**: Track buy/sell ratio changes over time to gauge shifts in market sentiment. + +5. **Spotting Manipulation**: Unusually large orders or sudden changes in the order book may indicate institutional activity. + +## Conclusion + +Market Depth Analysis provides a window into the supply and demand dynamics that drive price movement. By examining the actual order book data rather than just price action, traders can gain valuable insights into market structure and sentiment. + +While not a standalone trading strategy, this analysis tool can significantly enhance trading decisions when combined with other technical and fundamental approaches. It's particularly valuable for short-term traders who need to understand immediate market dynamics and liquidity conditions. diff --git a/docs/strategies/moving_average.md b/docs/strategies/moving_average.md new file mode 100644 index 0000000..d4539bb --- /dev/null +++ b/docs/strategies/moving_average.md @@ -0,0 +1,285 @@ +# Moving Average Crossover Strategy + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + +## Overview + +The Moving Average Crossover is one of the most widely used trading strategies in technical analysis. It uses two moving averages of different periods to generate trading signals: + +| Signal Type | Description | +|-------------|-------------| +| **Buy signal** | When the faster (shorter-period) moving average crosses above the slower (longer-period) moving average | +| **Sell signal** | When the faster moving average crosses below the slower moving average | + +This strategy aims to identify potential trend changes in the market. + +## Strategy Logic + +| Step | Description | +|------|-------------| +| 1 | Calculate two Simple Moving Averages (SMA): a short-period SMA and a long-period SMA | +| 2 | Compare current and previous values of both SMAs to detect crossovers | +| 3 | Generate buy signals when short SMA crosses above long SMA | +| 4 | Generate sell signals when short SMA crosses below long SMA | +| 5 | Execute trades only during specified trading hours | + +### Strategy Flow + +```mermaid +flowchart TD + A[Start] --> B[Fetch Market Data] + B --> C[Calculate Short-Period SMA] + B --> D[Calculate Long-Period SMA] + C --> E[Compare Current & Previous SMAs] + D --> E + E --> F{"Short MA Crossed
Above Long MA?"} + F -->|Yes| G[Generate Buy Signal] + F -->|No| H{"Short MA Crossed
Below Long MA?"} + H -->|Yes| I[Generate Sell Signal] + H -->|No| J[No Trading Signal] + G --> K{"Within Trading
Hours?"} + I --> K + J --> K + K -->|Yes| L[Execute Trade] + K -->|No| M[Skip Trade Execution] + L --> N[Update Statistics] + M --> N + N --> O[Wait for Next Tick] + O --> B +``` + +## Code Implementation + +Let's break down the implementation step by step: + +### Step 1: Required Imports + +```python +from __future__ import annotations + +import logging + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) +``` + +We import the necessary modules from MQPy: +- `Rates`: For accessing historical price data +- `Tick`: For accessing current market prices +- `Trade`: For executing trading operations +- `logging`: For keeping track of what the strategy is doing + +### Step 2: Define SMA Calculation Function + +```python +def calculate_sma(prices: list[float], period: int) -> float | None: + """Calculate Simple Moving Average.""" + if len(prices) < period: + return None + return sum(prices[-period:]) / period +``` + +This function calculates a Simple Moving Average (SMA) from a list of prices: +- It first checks if we have enough price data for the requested period +- If not, it returns `None` +- Otherwise, it calculates the average of the last `period` prices + +### Step 3: Initialize the Trading Strategy + +```python +trade = Trade( + expert_name="Moving Average Crossover", + version="1.0", + symbol="EURUSD", + magic_number=567, + lot=0.1, + stop_loss=25, + emergency_stop_loss=300, + take_profit=25, + emergency_take_profit=300, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) +``` + +Here we initialize the `Trade` object with our strategy parameters: +- `expert_name`: The name of our trading strategy +- `symbol`: The trading instrument (EURUSD in this case) +- `magic_number`: A unique identifier for this strategy's trades +- `lot`: The trading volume +- `stop_loss`/`take_profit`: Risk management parameters in points +- `emergency_stop_loss`/`emergency_take_profit`: Larger safety values if regular ones fail +- `start_time`/`finishing_time`/`ending_time`: Define the trading session hours + +### Step 4: Set Strategy Parameters + +```python +# Strategy parameters +prev_tick_time = 0 +short_period = 5 +long_period = 20 + +# Variables to track previous state for crossover detection +prev_short_ma = None +prev_long_ma = None +``` + +We define the key parameters for our strategy: +- `short_period`: The period for the fast moving average (5 bars) +- `long_period`: The period for the slow moving average (20 bars) +- We also initialize variables to track the previous MA values for crossover detection + +### Step 5: Main Trading Loop + +```python +try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, long_period + 10, 0, 1) # Get extra data for reliability +``` + +The main loop: +- Prepares the symbol for trading +- Gets the current market price via `Tick` +- Retrieves historical price data via `Rates`. We request slightly more data (long_period + 10) for reliability + +### Step 6: Calculate Moving Averages + +```python +# Only process if we have a new tick +if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= long_period: + # Calculate moving averages + short_ma = calculate_sma(historical_rates.close, short_period) + long_ma = calculate_sma(historical_rates.close, long_period) +``` + +For each new tick: +- We check that it's different from the previous tick to avoid redundant calculations +- We ensure we have enough historical data +- We calculate both the short and long moving averages + +### Step 7: Detect Crossovers + +```python +# Check if we have enough data for comparison +if short_ma and long_ma and prev_short_ma and prev_long_ma: + # Detect crossover (short MA crosses above long MA) + cross_above = prev_short_ma <= prev_long_ma and short_ma > long_ma + + # Detect crossunder (short MA crosses below long MA) + cross_below = prev_short_ma >= prev_long_ma and short_ma < long_ma + + # Log crossover events + if cross_above: + logger.info(f"Bullish crossover detected: Short MA ({short_ma:.5f}) crossed above Long MA ({long_ma:.5f})") + elif cross_below: + logger.info(f"Bearish crossover detected: Short MA ({short_ma:.5f}) crossed below Long MA ({long_ma:.5f})") +``` + +To detect crossovers, we need both current and previous MA values: +- `cross_above`: Occurs when the short MA was below (or equal to) the long MA in the previous tick, but is now above it +- `cross_below`: Occurs when the short MA was above (or equal to) the long MA in the previous tick, but is now below it +- We log these events for monitoring the strategy + +### Step 8: Execute Trades + +```python +# Execute trading positions based on signals +if trade.trading_time(): # Only trade during allowed hours + trade.open_position( + should_buy=cross_above, + should_sell=cross_below, + comment="Moving Average Crossover Strategy" + ) +``` + +When a signal is detected: +- We first check if we're within the allowed trading hours using `trade.trading_time()` +- If yes, we call `open_position()` with our buy/sell signals +- The `comment` parameter helps identify the strategy in the trading terminal + +### Step 9: Update State and Check End of Day + +```python +# Update previous MA values for next comparison +prev_short_ma = short_ma +prev_long_ma = long_ma + +# Update trading statistics periodically +trade.statistics() + +prev_tick_time = current_tick.time_msc + +# Check if it's the end of the trading day +if trade.days_end(): + trade.close_position("End of the trading day reached.") + break +``` + +After processing each tick: +- We update the previous MA values for the next iteration +- We update trading statistics for monitoring +- We update the previous tick time +- We check if it's the end of the trading day, and if so, close positions and exit + +### Step 10: Error Handling + +```python +except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") +except Exception as e: + logger.error(f"Error in strategy execution: {e}") +finally: + logger.info("Finishing the program.") +``` + +Proper error handling ensures: +- Clean exit when the user interrupts the program +- Logging of any errors that occur +- Proper cleanup in the `finally` block + +## Full Source Code + +You can find the complete source code for this strategy in the [MQPy GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/basic_moving_average_strategy.py). + +## Backtesting and Optimization + +This strategy can be improved by: + +| Improvement | Description | +|-------------|-------------| +| Period Optimization | Finding the optimal MA periods for specific instruments | +| Filter Addition | Adding filters to avoid false signals in ranging markets | +| Position Sizing | Implementing dynamic position sizing based on market volatility | +| Stop Management | Adding trailing stop-loss to secure profits as the trend develops | + +## Next Steps + +Try experimenting with different: + +| Experiment | Options | +|------------|---------| +| MA Periods | Try pairs like 9 and 21, or 50 and 200 for different timeframes | +| MA Types | Test Exponential, Weighted, or other MA types for potential improvements | +| Instruments | Apply the strategy to various forex pairs, stocks, or commodities | +| Timeframes | Scale from M1 (1-minute) to D1 (daily) charts for different trading styles | diff --git a/docs/strategies/rsi_strategy.md b/docs/strategies/rsi_strategy.md new file mode 100644 index 0000000..f112caf --- /dev/null +++ b/docs/strategies/rsi_strategy.md @@ -0,0 +1,286 @@ +# RSI Trading Strategy + +!!! danger "Trading Risk Warning" + **IMPORTANT: All examples should be tested using demo accounts only!** + + - Trading involves substantial risk of loss + - These examples are for educational purposes only + - Always test with fake money before using real funds + +## Overview + +The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and change of price movements. It oscillates between 0 and 100 and is typically used to identify overbought or oversold conditions in a market. + +| Signal Type | Description | +|-------------|-------------| +| **Buy signal** | When RSI falls below the oversold threshold (typically 30) | +| **Sell signal** | When RSI rises above the overbought threshold (typically 70) | + +## Strategy Logic + +| Step | Description | +|------|-------------| +| 1 | Calculate the RSI indicator using price data | +| 2 | Generate buy signals when RSI falls below the oversold threshold | +| 3 | Generate sell signals when RSI rises above the overbought threshold | +| 4 | Execute trades only during specified trading hours | + +### Strategy Flow + +```mermaid +flowchart TD + A[Start] --> B[Fetch Market Data] + B --> C[Calculate Price Changes] + C --> D[Separate Gains and Losses] + D --> E[Calculate Average Gain and Loss] + E --> F[Calculate RSI Value] + F --> G{"RSI < Oversold
Threshold?"} + G -->|Yes| H[Generate Buy Signal] + G -->|No| I{"RSI > Overbought
Threshold?"} + I -->|Yes| J[Generate Sell Signal] + I -->|No| K[No Trading Signal] + H --> L{"Within Trading
Hours?"} + J --> L + K --> L + L -->|Yes| M[Execute Trade] + L -->|No| N[Skip Trade Execution] + M --> O[Update Statistics] + N --> O + O --> P[Wait for Next Tick] + P --> B +``` + +## Code Implementation + +Let's break down the implementation step by step: + +### Step 1: Required Imports + +```python +from __future__ import annotations + +import logging +import numpy as np + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) +``` + +We import the necessary modules: +- Core MQPy modules for trading and data access +- `numpy` for efficient calculations in our RSI function +- `logging` for tracking the strategy's operation + +### Step 2: RSI Calculation Function + +```python +def calculate_rsi(prices: list[float], period: int = 14) -> float | None: + """Calculate the Relative Strength Index.""" + if len(prices) < period + 1: + return None + + # Calculate price changes + deltas = np.diff(prices) + + # Separate gains and losses + gains = np.where(deltas > 0, deltas, 0) + losses = np.where(deltas < 0, -deltas, 0) + + # Calculate initial average gain and loss + avg_gain = np.mean(gains[:period]) + avg_loss = np.mean(losses[:period]) + + # Avoid division by zero + if avg_loss == 0: + return 100 + + # Calculate RS and RSI + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + return rsi +``` + +This function implements the RSI formula: +1. First, it checks if we have enough price data (at least period + 1 values) +2. It calculates price changes between consecutive closes using `np.diff()` +3. It separates the price changes into gains (positive changes) and losses (negative changes) +4. It calculates the average gain and average loss over the specified period +5. It computes the Relative Strength (RS) as the ratio of average gain to average loss +6. Finally, it converts RS to RSI using the formula: RSI = 100 - (100 / (1 + RS)) + +### Step 3: Initialize the Trading Strategy + +```python +trade = Trade( + expert_name="RSI Strategy", + version="1.0", + symbol="EURUSD", + magic_number=568, + lot=0.1, + stop_loss=30, + emergency_stop_loss=90, + take_profit=60, + emergency_take_profit=180, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) +``` + +We configure our trading strategy with: +- Identification parameters: name, version, magic number +- Trading parameters: symbol, lot size +- Risk management parameters: stop loss and take profit (notice the take profit is 2x the stop loss) +- Trading session times: when to start, when to stop opening new positions, and when to close all positions + +### Step 4: Set Strategy Parameters + +```python +# Strategy parameters +prev_tick_time = 0 +rsi_period = 14 +overbought_threshold = 70 +oversold_threshold = 30 +``` + +The key parameters for our RSI strategy are: +- `rsi_period`: The number of periods for RSI calculation (standard is 14) +- `overbought_threshold`: The RSI level above which we consider the market overbought (70) +- `oversold_threshold`: The RSI level below which we consider the market oversold (30) + +### Step 5: Main Trading Loop + +```python +try: + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, rsi_period + 20, 0, 1) # Get extra data for reliability +``` + +In the main loop, we: +- Prepare the symbol for trading +- Get the current market price +- Retrieve historical price data (we get rsi_period + 20 bars for reliable calculations) + +### Step 6: Calculate RSI and Generate Signals + +```python +# Only process if we have a new tick and enough data for RSI calculation +if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= rsi_period + 1: + # Calculate RSI + rsi_value = calculate_rsi(historical_rates.close, rsi_period) + + if rsi_value is not None: + # Generate signals based on RSI thresholds + is_buy_signal = rsi_value < oversold_threshold + is_sell_signal = rsi_value > overbought_threshold + + # Log RSI values and signals + if is_buy_signal: + logger.info(f"Oversold condition: RSI = {rsi_value:.2f} (< {oversold_threshold})") + elif is_sell_signal: + logger.info(f"Overbought condition: RSI = {rsi_value:.2f} (> {overbought_threshold})") + else: + logger.debug(f"Current RSI: {rsi_value:.2f}") +``` + +For each new tick, we: +1. Calculate the RSI value using our custom function +2. Generate trading signals based on simple threshold comparisons: + - Buy when RSI < oversold threshold (30) + - Sell when RSI > overbought threshold (70) +3. Log the RSI values and any signals generated for monitoring + +### Step 7: Execute Trades + +```python +# Execute trading positions based on signals during allowed trading hours +if trade.trading_time(): + trade.open_position( + should_buy=is_buy_signal, + should_sell=is_sell_signal, + comment=f"RSI Strategy: {rsi_value:.2f}" + ) +``` + +When a signal is detected: +- We check if we're within the allowed trading hours +- If yes, we execute the appropriate trade based on our signals +- The comment includes the RSI value for reference in the trading terminal + +### Step 8: Update State and Check for End of Day + +```python +# Update trading statistics periodically +trade.statistics() + +prev_tick_time = current_tick.time_msc + +# Check if it's the end of the trading day +if trade.days_end(): + trade.close_position("End of the trading day reached.") + break +``` + +After processing each tick, we: +- Update the trading statistics for monitoring +- Store the current tick time for the next iteration +- Check if it's the end of the trading day, and if so, close positions and exit + +### Step 9: Error Handling + +```python +except KeyboardInterrupt: + logger.info("Strategy execution interrupted by user.") + trade.close_position("User interrupted the strategy.") +except Exception as e: + logger.error(f"Error in strategy execution: {e}") +finally: + logger.info("Finishing the program.") +``` + +Our error handling ensures: +- Proper handling of user interruptions +- Logging of any errors that occur +- Clean program termination in the `finally` block + +## Full Source Code + +You can find the complete source code for this strategy in the [MQPy GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rsi_strategy.py). + +## Optimization Opportunities + +This strategy can be improved by: + +| Improvement | Description | +|-------------|-------------| +| **Smoothing** | Using a smoothed RSI or applying an additional moving average to filter out noise | +| **Trend Filters** | Only taking trades in the direction of the longer-term trend | +| **Divergence** | Looking for divergence between price and RSI for stronger signals | +| **Dynamic Thresholds** | Adjusting the overbought/oversold thresholds based on market volatility | +| **Position Management** | Taking partial profits when RSI reaches extreme levels | + +## Next Steps + +Try experimenting with: + +| Experiment | Options | +|------------|---------| +| RSI Periods | Shorter periods (9) for more signals, longer periods (21) for fewer but stronger signals | +| Threshold Levels | Test different levels like 20/80 for stronger signals but fewer trades | +| Complementary Indicators | Add moving averages or other oscillators to confirm RSI signals | +| Position Sizing | Implement different sizing based on the distance of RSI from thresholds | diff --git a/examples_of_expert_advisor/__init__.py b/examples_of_expert_advisor/__init__.py deleted file mode 100644 index 8f362cf..0000000 --- a/examples_of_expert_advisor/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Expert Advisor examples for MetaTrader 5. - -This package contains example implementations of various trading strategies. -""" diff --git a/mkdocs.yaml b/mkdocs.yaml index 7c72efa..6594621 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -130,4 +130,11 @@ plugins: nav: - Home: index.md - Contributing: contributing.md +- Examples: examples.md +- Strategy Explanations: + - Moving Average Crossover: strategies/moving_average.md + - RSI Strategy: strategies/rsi_strategy.md + - Bollinger Bands Strategy: strategies/bollinger_bands.md + - Fibonacci Retracement Strategy: strategies/fibonacci_retracement.md + - Market Depth Analysis: strategies/market_depth_analysis.md - Code documentation: reference/ diff --git a/mqpy/__init__.py b/mqpy/__init__.py index 925fab0..9e043eb 100644 --- a/mqpy/__init__.py +++ b/mqpy/__init__.py @@ -3,3 +3,7 @@ This package provides a bridge between Python and MetaTrader 5, allowing users to create Expert Advisors and implement trading strategies in Python. """ + +from mqpy.logger import get_logger + +__all__ = ["get_logger"] diff --git a/mqpy/book.py b/mqpy/book.py index 828d0a5..b069960 100644 --- a/mqpy/book.py +++ b/mqpy/book.py @@ -5,20 +5,14 @@ from __future__ import annotations -import logging from typing import Any import MetaTrader5 as Mt5 -# Configure logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +from mqpy.logger import get_logger -# Create console handler with formatting -console_handler = logging.StreamHandler() -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) +# Configure logging +logger = get_logger(__name__) class Book: @@ -39,11 +33,11 @@ def __init__(self, symbol: str) -> None: else: logger.error(f"Error adding {self.symbol} to the market book. Error: {Mt5.last_error()}") - def get(self) -> dict[str, Any] | None: + def get(self) -> list[Any] | None: """Get the market book for the financial instrument. Returns: - dict[str, Any] | None: The market book data if successful, None otherwise. + list[Any] | None: The market book data if successful, None otherwise. """ return Mt5.market_book_get(self.symbol) @@ -53,4 +47,5 @@ def release(self) -> bool: Returns: bool: True if successful, False otherwise. """ - return Mt5.market_book_release(self.symbol) + result = Mt5.market_book_release(self.symbol) + return False if result is None else result diff --git a/mqpy/indicator_connector.py b/mqpy/indicator_connector.py index 24fc282..669e483 100644 --- a/mqpy/indicator_connector.py +++ b/mqpy/indicator_connector.py @@ -7,19 +7,13 @@ import ast import json -import logging import socket from typing import Any +from mqpy.logger import get_logger + # Configure logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - -# Create console handler with formatting -console_handler = logging.StreamHandler() -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) +logger = get_logger(__name__) # To be able to use it you need the MQL5 Service to send the data, it is possible to found it here: # -------------------------------------------------------------------- # diff --git a/mqpy/logger.py b/mqpy/logger.py new file mode 100644 index 0000000..ffd9447 --- /dev/null +++ b/mqpy/logger.py @@ -0,0 +1,50 @@ +"""Logging configuration for the MqPy application. + +This module provides a centralized logging configuration for the entire application. +Import this module instead of directly importing the logging module to ensure consistent +logging behavior across the application. +""" + +from __future__ import annotations + +import logging +import sys + + +def get_logger(name: str, level: int | None = None) -> logging.Logger: + """Get a logger with the specified name and level. + + Args: + name (str): The name of the logger, typically __name__. + level (int | None): The logging level. Defaults to INFO if None. + + Returns: + logging.Logger: A configured logger instance. + """ + logger = logging.getLogger(name) + + # Only configure the logger if it doesn't already have handlers + if not logger.handlers: + # Set default level if not specified + if level is None: + level = logging.INFO + + logger.setLevel(level) + + # Create console handler with a specific format + console_handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + return logger + + +# Configure the root logger +root_logger = logging.getLogger() +if not root_logger.handlers: + root_logger.setLevel(logging.WARNING) + console_handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) diff --git a/mqpy/rates.py b/mqpy/rates.py index 56c7c9a..35c8bbb 100644 --- a/mqpy/rates.py +++ b/mqpy/rates.py @@ -5,6 +5,8 @@ from __future__ import annotations +from typing import Any + import MetaTrader5 as Mt5 @@ -40,11 +42,11 @@ def _raise_value_error(msg: str) -> None: self._tick_volume = [rate[5] for rate in rates] self._spread = [rate[6] for rate in rates] self._real_volume = [rate[7] for rate in rates] - except Mt5.Error as e: + except Exception as e: raise ValueError(f"Failed to create Rates object for symbol {symbol}") from e @property - def time(self) -> list[int | float]: + def time(self) -> list[Any]: """List of timestamps.""" return self._time @@ -69,16 +71,16 @@ def close(self) -> list[float]: return self._close @property - def tick_volume(self) -> list[int]: + def tick_volume(self) -> list[int | float]: """List of tick volumes.""" return self._tick_volume @property - def spread(self) -> list[int]: + def spread(self) -> list[int | float]: """List of spreads.""" return self._spread @property - def real_volume(self) -> list[int]: + def real_volume(self) -> list[int | float]: """List of real volumes.""" return self._real_volume diff --git a/mqpy/template.py b/mqpy/template.py index 66dfcd2..af852aa 100644 --- a/mqpy/template.py +++ b/mqpy/template.py @@ -7,7 +7,12 @@ import argparse from pathlib import Path -from typing import Any +from typing import Any, Callable + +from mqpy.logger import get_logger + +# Configure logging +logger = get_logger(__name__) def get_arguments() -> dict[str, Any]: @@ -16,20 +21,56 @@ def get_arguments() -> dict[str, Any]: Returns: dict[str, Any]: Dictionary containing the parsed arguments. """ - parser = argparse.ArgumentParser() - parser.add_argument("--file_name", type=str, action="store", default="demo") - parser.add_argument("--symbol", type=str, action="store", default="EURUSD") + parser = argparse.ArgumentParser( + description="Generate MetaTrader 5 expert advisor templates using the mqpy framework.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument( + "--file_name", type=str, default="demo", help="Name of the output Python file (without .py extension)" + ) + + parser.add_argument("--symbol", type=str, default="EURUSD", help="Trading symbol to use in the template") + + parser.add_argument( + "--strategy", + type=str, + choices=["moving_average", "rsi", "macd", "bollinger"], + default="moving_average", + help="Trading strategy template to generate", + ) + + parser.add_argument("--magic_number", type=int, default=567, help="Magic number for the trading strategy") + + parser.add_argument("--lot", type=float, default=1.0, help="Lot size for trading") + + parser.add_argument("--stop_loss", type=float, default=25.0, help="Stop loss in points") + + parser.add_argument("--take_profit", type=float, default=25.0, help="Take profit in points") + + parser.add_argument("--directory", type=str, default=".", help="Directory to save the generated file") + return vars(parser.parse_args()) -def main() -> None: - """Generate a template file for a trading strategy.""" - file_name = get_arguments()["file_name"] - symbol = get_arguments()["symbol"] +def generate_moving_average_template(args: dict[str, Any]) -> str: + """Generate a moving average crossover strategy template. + + Args: + args: Dictionary with template parameters + + Returns: + str: The template code as a string + """ + return f"""#!/usr/bin/env python3 +''' +Moving Average Crossover Strategy for MetaTrader 5 +Generated with mqpy template generator + +This strategy trades based on crossovers between short and long moving averages. +''' - with Path(f"{file_name}.py").open("w") as file: - file.write( - f"""from mqpy.rates import Rates +from mqpy.rates import Rates from mqpy.tick import Tick from mqpy.trade import Trade @@ -37,53 +78,471 @@ def main() -> None: trade = Trade( expert_name="Moving Average Crossover", version="1.0", - symbol="{symbol}", - magic_number=567, - lot=1.0, - stop_loss=25, - emergency_stop_loss=300, - take_profit=25, - emergency_take_profit=300, + symbol="{args['symbol']}", + magic_number={args['magic_number']}, + lot={args['lot']}, + stop_loss={args['stop_loss']}, + emergency_stop_loss={args['stop_loss'] * 12}, + take_profit={args['take_profit']}, + emergency_take_profit={args['take_profit'] * 12}, start_time="9:15", finishing_time="17:30", ending_time="17:50", fee=0.5, ) -# Main trading loop +# Strategy parameters prev_tick_time = 0 short_window_size = 5 long_window_size = 20 # Adjust the window size as needed -while True: - # Fetch tick and rates data - current_tick = Tick(trade.symbol) - historical_rates = Rates(trade.symbol, long_window_size, 0, 1) +def main(): + '''Main execution function''' + global prev_tick_time + + print(f"Starting Moving Average Crossover strategy on {{trade.symbol}}") + + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, long_window_size, 0, 1) + + # Trading logic - only execute on new tick + if current_tick.time_msc != prev_tick_time: + # Calculate moving averages + if len(historical_rates.close) >= long_window_size: + short_ma = sum(historical_rates.close[-short_window_size:]) / short_window_size + long_ma = sum(historical_rates.close[-long_window_size:]) / long_window_size + + # Generate signals based on moving average crossover + is_cross_above = short_ma > long_ma and current_tick.last > short_ma + is_cross_below = short_ma < long_ma and current_tick.last < short_ma + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during trading hours + trade.open_position(is_cross_above, is_cross_below, "Moving Average Crossover") + + # Update statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\\nStrategy execution interrupted by user.") + except Exception as e: + print(f"Error in strategy execution: {{e}}") + finally: + print("Finishing the program.") +""" + + +def generate_rsi_template(args: dict[str, Any]) -> str: + """Generate an RSI strategy template. + + Args: + args: Dictionary with template parameters + + Returns: + str: The template code as a string + """ + return f"""#!/usr/bin/env python3 +''' +RSI (Relative Strength Index) Strategy for MetaTrader 5 +Generated with mqpy template generator + +This strategy trades based on overbought and oversold conditions using RSI. +''' + +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Initialize the trading strategy +trade = Trade( + expert_name="RSI Strategy", + version="1.0", + symbol="{args['symbol']}", + magic_number={args['magic_number']}, + lot={args['lot']}, + stop_loss={args['stop_loss']}, + emergency_stop_loss={args['stop_loss'] * 12}, + take_profit={args['take_profit']}, + emergency_take_profit={args['take_profit'] * 12}, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) + +# Strategy parameters +prev_tick_time = 0 +rsi_period = 14 +oversold_threshold = 30 +overbought_threshold = 70 + +def calculate_rsi(prices, period=14): + '''Calculate the Relative Strength Index''' + if len(prices) < period + 1: + return 50 # Default to neutral when not enough data + + # Calculate price changes + deltas = [prices[i] - prices[i-1] for i in range(1, len(prices))] + + # Separate gains and losses + gains = [delta if delta > 0 else 0 for delta in deltas] + losses = [-delta if delta < 0 else 0 for delta in deltas] + + # Calculate initial average gain and loss + avg_gain = sum(gains[:period]) / period + avg_loss = sum(losses[:period]) / period + + # Use smoothed averages for the rest of the data + for i in range(period, len(deltas)): + avg_gain = (avg_gain * (period - 1) + gains[i]) / period + avg_loss = (avg_loss * (period - 1) + losses[i]) / period + + # Calculate RS and RSI + if avg_loss == 0: + return 100 + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + return rsi + +def main(): + '''Main execution function''' + global prev_tick_time + + print(f"Starting RSI strategy on {{trade.symbol}}") + + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, rsi_period + 10, 0, 1) + + # Trading logic - only execute on new tick + if current_tick.time_msc != prev_tick_time: + # Calculate RSI + if len(historical_rates.close) > rsi_period + 1: + rsi_value = calculate_rsi(historical_rates.close, rsi_period) + + # Generate signals based on RSI + is_buy_signal = rsi_value < oversold_threshold # Buy when oversold + is_sell_signal = rsi_value > overbought_threshold # Sell when overbought + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during trading hours + trade.open_position(is_buy_signal, is_sell_signal, f"RSI Strategy: {{rsi_value:.2f}}") + + # Update statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\\nStrategy execution interrupted by user.") + except Exception as e: + print(f"Error in strategy execution: {{e}}") + finally: + print("Finishing the program.") +""" + + +def generate_macd_template(args: dict[str, Any]) -> str: + """Generate a MACD strategy template. + + Args: + args: Dictionary with template parameters + + Returns: + str: The template code as a string + """ + return f"""#!/usr/bin/env python3 +''' +MACD (Moving Average Convergence Divergence) Strategy for MetaTrader 5 +Generated with mqpy template generator + +This strategy trades based on MACD crossovers and signal line. +''' - # Check for new tick - if current_tick.time_msc != prev_tick_time: - # Calculate moving averages - short_ma = sum(historical_rates.close[-short_window_size:]) / short_window_size - long_ma = sum(historical_rates.close[-long_window_size:]) / long_window_size +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Initialize the trading strategy +trade = Trade( + expert_name="MACD Strategy", + version="1.0", + symbol="{args['symbol']}", + magic_number={args['magic_number']}, + lot={args['lot']}, + stop_loss={args['stop_loss']}, + emergency_stop_loss={args['stop_loss'] * 12}, + take_profit={args['take_profit']}, + emergency_take_profit={args['take_profit'] * 12}, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) + +# Strategy parameters +prev_tick_time = 0 +fast_period = 12 +slow_period = 26 +signal_period = 9 + +def calculate_ema(prices, period): + '''Calculate the Exponential Moving Average''' + if len(prices) < period: + return sum(prices) / len(prices) + + multiplier = 2 / (period + 1) + ema = sum(prices[:period]) / period + + for price in prices[period:]: + ema = (price - ema) * multiplier + ema + + return ema + +def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9): + '''Calculate MACD, Signal line, and Histogram''' + if len(prices) < slow_period: + return 0, 0, 0 + + # Calculate EMA values + fast_ema = calculate_ema(prices, fast_period) + slow_ema = calculate_ema(prices, slow_period) + + # MACD line + macd_line = fast_ema - slow_ema - # Generate signals based on moving average crossover - is_cross_above = short_ma > long_ma and current_tick.last > short_ma - is_cross_below = short_ma < long_ma and current_tick.last < short_ma + # Signal line (EMA of MACD) + # For simplicity, we're approximating the signal line here + signal_line = (macd_line + sum(prices[-signal_period:]) / signal_period) / 2 - # Execute trading positions based on signals - trade.open_position(is_cross_above, is_cross_below, "Moving Average Crossover Strategy") + # MACD histogram + histogram = macd_line - signal_line - prev_tick_time = current_tick.time_msc + return macd_line, signal_line, histogram - # Check if it's the end of the trading day - if trade.days_end(): - trade.close_position("End of the trading day reached.") - break +def main(): + '''Main execution function''' + global prev_tick_time -print("Finishing the program.") -print("Program finished.") + print(f"Starting MACD strategy on {{trade.symbol}}") + + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, slow_period + signal_period + 10, 0, 1) + + # Trading logic - only execute on new tick + if current_tick.time_msc != prev_tick_time: + # Calculate MACD + if len(historical_rates.close) > slow_period + signal_period: + macd_line, signal_line, histogram = calculate_macd( + historical_rates.close, fast_period, slow_period, signal_period + ) + + # Generate signals based on MACD + is_buy_signal = macd_line > signal_line and histogram > 0 + is_sell_signal = macd_line < signal_line and histogram < 0 + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during trading hours + trade.open_position(is_buy_signal, is_sell_signal, "MACD Crossover") + + # Update statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\\nStrategy execution interrupted by user.") + except Exception as e: + print(f"Error in strategy execution: {{e}}") + finally: + print("Finishing the program.") """ - ) + + +def generate_bollinger_template(args: dict[str, Any]) -> str: + """Generate a Bollinger Bands strategy template. + + Args: + args: Dictionary with template parameters + + Returns: + str: The template code as a string + """ + return f"""#!/usr/bin/env python3 +''' +Bollinger Bands Strategy for MetaTrader 5 +Generated with mqpy template generator + +This strategy trades based on price movements outside Bollinger Bands. +''' + +from math import sqrt +from mqpy.rates import Rates +from mqpy.tick import Tick +from mqpy.trade import Trade + +# Initialize the trading strategy +trade = Trade( + expert_name="Bollinger Bands Strategy", + version="1.0", + symbol="{args['symbol']}", + magic_number={args['magic_number']}, + lot={args['lot']}, + stop_loss={args['stop_loss']}, + emergency_stop_loss={args['stop_loss'] * 12}, + take_profit={args['take_profit']}, + emergency_take_profit={args['take_profit'] * 12}, + start_time="9:15", + finishing_time="17:30", + ending_time="17:50", + fee=0.5, +) + +# Strategy parameters +prev_tick_time = 0 +bb_period = 20 +std_dev_multiplier = 2.0 + +def calculate_bollinger_bands(prices, period=20, multiplier=2): + '''Calculate Bollinger Bands (middle, upper, lower)''' + if len(prices) < period: + return prices[-1], prices[-1], prices[-1] + + # Calculate SMA (middle band) + sma = sum(prices[-period:]) / period + + # Calculate standard deviation + squared_diff = [(price - sma) ** 2 for price in prices[-period:]] + std_dev = sqrt(sum(squared_diff) / period) + + # Calculate upper and lower bands + upper_band = sma + (multiplier * std_dev) + lower_band = sma - (multiplier * std_dev) + + return sma, upper_band, lower_band + +def main(): + '''Main execution function''' + global prev_tick_time + + print(f"Starting Bollinger Bands strategy on {{trade.symbol}}") + + while True: + # Prepare the symbol for trading + trade.prepare_symbol() + + # Fetch tick and rates data + current_tick = Tick(trade.symbol) + historical_rates = Rates(trade.symbol, bb_period + 10, 0, 1) + + # Trading logic - only execute on new tick + if current_tick.time_msc != prev_tick_time: + # Calculate Bollinger Bands + if len(historical_rates.close) >= bb_period: + middle_band, upper_band, lower_band = calculate_bollinger_bands( + historical_rates.close, bb_period, std_dev_multiplier + ) + + # Generate signals based on Bollinger Bands + current_price = current_tick.last + is_buy_signal = current_price < lower_band # Buy when price crosses below lower band + is_sell_signal = current_price > upper_band # Sell when price crosses above upper band + + # Execute trading positions based on signals + if trade.trading_time(): # Only trade during trading hours + trade.open_position(is_buy_signal, is_sell_signal, "Bollinger Bands Signal") + + # Update statistics periodically + trade.statistics() + + prev_tick_time = current_tick.time_msc + + # Check if it's the end of the trading day + if trade.days_end(): + trade.close_position("End of the trading day reached.") + break + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\\nStrategy execution interrupted by user.") + except Exception as e: + print(f"Error in strategy execution: {{e}}") + finally: + print("Finishing the program.") +""" + + +def main() -> None: + """Generate a template file for a trading strategy based on user arguments.""" + args = get_arguments() + + # Create directory if it doesn't exist + output_dir = Path(args["directory"]) + if not output_dir.exists(): + output_dir.mkdir(parents=True, exist_ok=True) + + # Select the appropriate template generator based on strategy + template_generators: dict[str, Callable[[dict[str, Any]], str]] = { + "moving_average": generate_moving_average_template, + "rsi": generate_rsi_template, + "macd": generate_macd_template, + "bollinger": generate_bollinger_template, + } + + generator = template_generators.get(args["strategy"], generate_moving_average_template) + template_content = generator(args) + + # Generate the file + output_file = output_dir / f"{args['file_name']}.py" + with output_file.open("w", encoding="utf-8") as file: + file.write(template_content) + + logger.info(f"Strategy template generated: {output_file.absolute()}") + logger.info(f"Strategy type: {args['strategy']}") + logger.info(f"Trading symbol: {args['symbol']}") if __name__ == "__main__": diff --git a/mqpy/trade.py b/mqpy/trade.py index 907be72..5484fa6 100644 --- a/mqpy/trade.py +++ b/mqpy/trade.py @@ -1,23 +1,26 @@ """Module for trading operations with MetaTrader 5. Provides a Trade class for managing trading operations. + +Trade Modes: + - 0: Disabled - Trading is completely disabled for the symbol + - 1: Long only - Only buy positions allowed + - 2: Short only - Only sell positions allowed + - 3: Long and Short - Both buy and sell positions allowed (regular trading) + - 4: Close only - Only position closing is allowed, no new positions can be opened + +The Trade class automatically respects these limitations when attempting to open or close positions. """ -import logging import sys from datetime import datetime, timedelta, timezone import MetaTrader5 as Mt5 -# Configure logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +from mqpy.logger import get_logger -# Create console handler with formatting -console_handler = logging.StreamHandler() -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) +# Configure logging +logger = get_logger(__name__) class Trade: @@ -113,7 +116,8 @@ def select_symbol(self) -> None: Returns: None """ - Mt5.symbol_select(self.symbol, enable=True) + # Using positional arguments as the MetaTrader5 library doesn't support keywords + Mt5.symbol_select(self.symbol, True) # noqa: FBT003 def prepare_symbol(self) -> None: """Prepare the trading symbol for opening positions. @@ -130,7 +134,8 @@ def prepare_symbol(self) -> None: if not symbol_info.visible: logger.warning(f"The {self.symbol} is not visible, needed to be switched on.") - if not Mt5.symbol_select(self.symbol, enable=True): + # Using positional arguments as the MetaTrader5 library doesn't support keywords + if not Mt5.symbol_select(self.symbol, True): # noqa: FBT003 logger.error( f"The expert advisor {self.expert_name} failed in select the symbol {self.symbol}, turning off." ) @@ -138,17 +143,51 @@ def prepare_symbol(self) -> None: logger.error("Turned off") sys.exit(1) + # Check the trade mode + if symbol_info.trade_mode == 0: + logger.warning( + f"Trading is disabled for {self.symbol} (trade_mode = 0). No positions can be opened or closed." + ) + elif symbol_info.trade_mode == 4: + logger.warning( + f"{self.symbol} is in 'Close only' mode (trade_mode = 4). Only existing positions can be closed." + ) + + def get_trade_mode_description(self) -> str: + """Get a description of the symbol's trade mode. + + Returns: + str: A description of the trade mode. + """ + trade_mode = Mt5.symbol_info(self.symbol).trade_mode + + if trade_mode == 0: + return "Disabled (trading disabled for the symbol)" + if trade_mode == 1: + return "Long only (only buy positions allowed)" + if trade_mode == 2: + return "Short only (only sell positions allowed)" + if trade_mode == 3: + return "Long and Short (both buy and sell positions allowed)" + if trade_mode == 4: + return "Close only (only position closing is allowed)" + return f"Unknown trade mode: {trade_mode}" + def summary(self) -> None: """Print a summary of the expert advisor parameters. Returns: None """ + trade_mode = Mt5.symbol_info(self.symbol).trade_mode + trade_mode_desc = self.get_trade_mode_description() + logger.info( f"Summary:\n" f"ExpertAdvisor name: {self.expert_name}\n" f"ExpertAdvisor version: {self.version}\n" f"Running on symbol: {self.symbol}\n" + f"Symbol trade mode: {trade_mode} - {trade_mode_desc}\n" f"MagicNumber: {self.magic_number}\n" f"Number of lot(s): {self.lot}\n" f"StopLoss: {self.stop_loss}\n" @@ -185,6 +224,18 @@ def open_buy_position(self, comment: str = "") -> None: Returns: None """ + # Check trade mode to see if Buy operations are allowed + symbol_info = Mt5.symbol_info(self.symbol) + if symbol_info.trade_mode == 0: + logger.warning(f"Cannot open Buy position for {self.symbol} - trading is disabled.") + return + if symbol_info.trade_mode == 2: # Short only + logger.warning(f"Cannot open Buy position for {self.symbol} - only Sell positions are allowed.") + return + if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0: + logger.warning(f"Cannot open Buy position for {self.symbol} - symbol is in 'Close only' mode.") + return + point = Mt5.symbol_info(self.symbol).point price = Mt5.symbol_info_tick(self.symbol).ask @@ -217,6 +268,18 @@ def open_sell_position(self, comment: str = "") -> None: Returns: None """ + # Check trade mode to see if Sell operations are allowed + symbol_info = Mt5.symbol_info(self.symbol) + if symbol_info.trade_mode == 0: + logger.warning(f"Cannot open Sell position for {self.symbol} - trading is disabled.") + return + if symbol_info.trade_mode == 1: # Long only + logger.warning(f"Cannot open Sell position for {self.symbol} - only Buy positions are allowed.") + return + if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0: + logger.warning(f"Cannot open Sell position for {self.symbol} - symbol is in 'Close only' mode.") + return + point = Mt5.symbol_info(self.symbol).point price = Mt5.symbol_info_tick(self.symbol).bid @@ -240,7 +303,7 @@ def open_sell_position(self, comment: str = "") -> None: result = Mt5.order_send(request) self.request_result(price, result) - def request_result(self, price: float, result: Mt5.TradeResult) -> None: + def request_result(self, price: float, result: int) -> None: """Process the result of a trading request. Args: @@ -261,6 +324,64 @@ def request_result(self, price: float, result: Mt5.TradeResult) -> None: else: logger.info(f"Position Closed: {result.price}") + def _handle_trade_mode_restrictions(self, symbol_info: Mt5.SymbolInfo) -> bool: + """Handle trade mode restrictions for different symbol types. + + Args: + symbol_info (Mt5.SymbolInfo): The symbol information. + + Returns: + bool: True if a position was opened or a restriction was handled, False otherwise. + """ + # Check if the symbol is in "Disabled" mode (trade_mode = 0) + if symbol_info.trade_mode == 0: + logger.warning(f"Cannot open new positions for {self.symbol} - trading is disabled.") + return True + + # Check if the symbol is in "Close only" mode (trade_mode = 4) + if symbol_info.trade_mode == 4 and len(Mt5.positions_get(symbol=self.symbol)) == 0: + logger.warning(f"Cannot open new positions for {self.symbol} - symbol is in 'Close only' mode.") + return True + + # No restrictions that prevent all trading + return False + + def _handle_position_by_trade_mode( + self, symbol_info: Mt5.SymbolInfo, *, should_buy: bool, should_sell: bool, comment: str + ) -> None: + """Open a position based on trade mode and buy/sell conditions. + + Args: + symbol_info (Mt5.SymbolInfo): The symbol information. + should_buy (bool): Whether a buy position should be opened. + should_sell (bool): Whether a sell position should be opened. + comment (str): A comment for the trade. + """ + # For "Long only" mode (trade_mode = 1), only allow Buy positions + if symbol_info.trade_mode == 1: + if should_buy: + self.open_buy_position(comment) + self.total_deals += 1 + elif should_sell: + logger.warning(f"Cannot open Sell position for {self.symbol} - only Buy positions are allowed.") + + # For "Short only" mode (trade_mode = 2), only allow Sell positions + elif symbol_info.trade_mode == 2: + if should_sell: + self.open_sell_position(comment) + self.total_deals += 1 + elif should_buy: + logger.warning(f"Cannot open Buy position for {self.symbol} - only Sell positions are allowed.") + + # For regular trading (trade_mode = 3) or other modes, allow both Buy and Sell + else: + if should_buy and not should_sell: + self.open_buy_position(comment) + self.total_deals += 1 + if should_sell and not should_buy: + self.open_sell_position(comment) + self.total_deals += 1 + def open_position(self, *, should_buy: bool, should_sell: bool, comment: str = "") -> None: """Open a position based on buy and sell conditions. @@ -272,16 +393,22 @@ def open_position(self, *, should_buy: bool, should_sell: bool, comment: str = " Returns: None """ + symbol_info = Mt5.symbol_info(self.symbol) + + # Check trade mode restrictions + if self._handle_trade_mode_restrictions(symbol_info): + return + + # Open a position if no existing positions and within trading time if (len(Mt5.positions_get(symbol=self.symbol)) == 0) and self.trading_time(): - if should_buy and not should_sell: - self.open_buy_position(comment) - self.total_deals += 1 - if should_sell and not should_buy: - self.open_sell_position(comment) - self.total_deals += 1 + self._handle_position_by_trade_mode( + symbol_info, should_buy=should_buy, should_sell=should_sell, comment=comment + ) + # Check for stop loss and take profit conditions self.stop_and_gain(comment) + # Check if it's the end of the trading day if self.days_end(): logger.info("It is the end of trading the day.") logger.info("Closing all positions.") @@ -297,6 +424,13 @@ def close_position(self, comment: str = "") -> None: Returns: None """ + symbol_info = Mt5.symbol_info(self.symbol) + + # If trading is completely disabled for the symbol, log a warning and return + if symbol_info.trade_mode == 0: + logger.warning(f"Cannot close position for {self.symbol} - trading is disabled for this symbol.") + return + if len(Mt5.positions_get(symbol=self.symbol)) == 1: if Mt5.positions_get(symbol=self.symbol)[0].type == 0: # Buy position self.open_sell_position(comment) diff --git a/mqpy/utilities.py b/mqpy/utilities.py index daf0d3b..d444e66 100644 --- a/mqpy/utilities.py +++ b/mqpy/utilities.py @@ -3,20 +3,14 @@ Provides helper functions and classes for trading operations. """ -import logging from datetime import datetime, timezone import MetaTrader5 as Mt5 -# Configure logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +from mqpy.logger import get_logger -# Create console handler with formatting -console_handler = logging.StreamHandler() -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) +# Configure logging +logger = get_logger(__name__) class Utilities: @@ -57,7 +51,7 @@ def check_trade_availability(self, symbol: str, count_until: int) -> bool: self.__counter_flag = True if self.__minutes_counter == count_until: - logger.info("Trading is allowed.\n") + logger.info("Trading is allowed.") self.__reset_counters() return self.__allowed_to_trade @@ -68,3 +62,59 @@ def __reset_counters(self) -> None: self.__counter_flag = True self.__allow_to_count = False self.__allowed_to_trade = True + + # Test-only methods + def _test_get_minutes_counter(self) -> int: + """Get the minutes counter (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + return self.__minutes_counter + + def _test_get_counter_flag(self) -> bool: + """Get the counter flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + return self.__counter_flag + + def _test_get_allowed_to_trade(self) -> bool: + """Get the allowed to trade flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + return self.__allowed_to_trade + + def _test_get_allow_to_count(self) -> bool: + """Get the allow to count flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + return self.__allow_to_count + + def _test_get_recent_trade(self) -> bool: + """Get the recent trade flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + return self.__recent_trade + + def _test_set_minutes_counter(self, value: int) -> None: + """Set the minutes counter (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__minutes_counter = value + + def _test_set_counter_flag(self, value: bool) -> None: + """Set the counter flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__counter_flag = value + + def _test_set_allowed_to_trade(self, value: bool) -> None: + """Set the allowed to trade flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__allowed_to_trade = value + + def _test_set_allow_to_count(self, value: bool) -> None: + """Set the allow to count flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__allow_to_count = value + + def _test_set_recent_trade(self, value: bool) -> None: + """Set the recent trade flag (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__recent_trade = value + + def _test_reset_counters(self) -> None: + """Reset counters (for testing only).""" + logger.warning("This method is for testing purposes only and should not be used in production code.") + self.__reset_counters() diff --git a/pyproject.toml b/pyproject.toml index 2b8d5b5..582638b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools", + "setuptools>=61.0", "wheel", ] build-backend = "setuptools.build_meta" @@ -8,31 +8,20 @@ build-backend = "setuptools.build_meta" [project] name = "mqpy" authors = [ - {name = "Joao Euko"} + {email = "joao@example.com", name = "Joao Euko"}, ] version = "v0.6.9" description = "I developed this library to simplify the process of creating an Expert Advisor in MQL5. While developing in MQL5 can be complex, the same task is more streamlined in Python." -requires-python = ">=3.9" +requires-python = ">=3.8" dependencies = [] readme = "README.md" -license = "MIT" +license = {text = "MIT"} -[tool.setuptools.packages.find] -where = [] +[project.scripts] +mqpy = "mqpy.__main__:main" -[tool.semantic_release] -version_variable = [ - "mqpy/version.py:__version__", -] -version_toml = [ - "pyproject.toml:project.version", -] - -[tool.semantic_release.changelog] -retain_old_entries = true - -[dependency-groups] +[project.optional-dependencies] dev = [ "pre-commit>=4.0.1", "pylint>=3.3.3", @@ -49,6 +38,21 @@ docs = [ "mkdocstrings[python]>=0.29.1", ] +[tool.setuptools] +packages = ["mqpy"] + +[tool.semantic_release] +version_variable = [ + "mqpy/version.py:__version__", +] +version_toml = [ + "pyproject.toml:project.version", +] +commit_message = "chore(release): v{version}" + +[tool.semantic_release.changelog] +retain_old_entries = true + # pre-commit [tool.pytest.ini_options] markers = [] @@ -57,13 +61,43 @@ markers = [] warn_unused_configs = true ignore_missing_imports = true disable_error_code = [ - "misc", - "attr-defined", - "call-arg", - "name-defined", + "attr-defined", + "name-defined", + "assignment", + "return-value", + "arg-type", + "index", + "misc", + "operator" ] -show_error_codes = true files = "**/*.py" +exclude = [ + "venv", + "mt5", + "site-packages", + "^build/", + "^dist/" +] + +[[tool.mypy.overrides]] +module = "MetaTrader5.*" +ignore_errors = true +disallow_untyped_defs = false +disallow_incomplete_defs = false +disallow_untyped_decorators = false +disallow_any_generics = false +disallow_untyped_calls = false +check_untyped_defs = false + +[[tool.mypy.overrides]] +module = "_virtualenv" +ignore_errors = true +disallow_untyped_defs = false +disallow_incomplete_defs = false +disallow_untyped_decorators = false +disallow_any_generics = false +disallow_untyped_calls = false +check_untyped_defs = false [tool.ruff] line-length = 120 @@ -122,7 +156,9 @@ max-complexity = 10 [tool.ruff.lint.per-file-ignores] "conftest.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001"] -"test_*.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001"] +"test_*.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001", "SLF001", "FBT003"] +"mqpy/utilities.py" = ["FBT001"] +"docs/examples/*.py" = ["C901"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/setup.py b/setup.py index 70f70d4..422a2fb 100644 --- a/setup.py +++ b/setup.py @@ -4,31 +4,29 @@ for creating Expert Advisors in MetaTrader 5. """ -import pathlib +from pathlib import Path -import setuptools +from setuptools import find_packages, setup -long_description = (pathlib.Path(__file__).parent / "README.md").read_text() +# Read the README for the long description +with Path("README.md").open(encoding="utf-8") as f: + long_description = f.read() -setuptools.setup( +setup( name="mqpy", - version="v0.6.9", - description=( - "A library to simplify the process of creating an Expert Advisor in MQL5. " - "It makes Python development more streamlined than MQL5." - ), - author="Joao Paulo Euko", - license="MIT", - keywords=["metatrader5", "algotrading", "stock market"], + version="0.6.9", + packages=find_packages(), + install_requires=[], + author="Joao Euko", + author_email="", + description="A library to simplify creating Expert Advisors in MQL5", + long_description=long_description, long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - install_requires=[ - "metatrader5 == 5.0.4874", - "setuptools == 78.1.0", - ], + license="MIT", entry_points={ "console_scripts": [ - "mqpy = __main__:main", + "mqpy=mqpy.__main__:main", ], }, + python_requires=">=3.8", ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f1d8f21 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +"""Test fixtures and configuration shared across test modules.""" + +from __future__ import annotations + +import logging +from typing import Generator + +import pytest + + +@pytest.fixture +def test_symbols() -> dict[str, str]: + """Provides common test symbols that can be used across tests.""" + return {"forex": "EURUSD", "indices": "US500", "commodities": "XAUUSD", "crypto": "BTCUSD", "invalid": "INVALID"} + + +@pytest.fixture +def configure_logging() -> Generator[None, None, None]: + """Sets up logging configuration for tests.""" + root = logging.getLogger() + for handler in root.handlers[:]: + root.removeHandler(handler) + + handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + root.addHandler(handler) + + root.setLevel(logging.INFO) + + yield + + for handler in root.handlers[:]: + root.removeHandler(handler) diff --git a/tests/integration/test_mt5_connection.py b/tests/integration/test_mt5_connection.py index e017f5c..62160cb 100644 --- a/tests/integration/test_mt5_connection.py +++ b/tests/integration/test_mt5_connection.py @@ -24,19 +24,24 @@ success = False for attempt in range(10): - if mt5.initialize( - login=int(os.getenv("MT5_LOGIN")), # type: ignore[arg-type] - password=os.getenv("MT5_PASSWORD"), - server=os.getenv("MT5_SERVER"), - path=os.getenv("MT5_PATH"), - ): - logger.info("MT5 initialized successfully") - mt5.shutdown() - success = True - break - else: - logger.info(f"Attempt {attempt+1}: Not ready yet, sleeping...") - time.sleep(5) + try: + if mt5.initialize( + login=int(os.getenv("MT5_LOGIN")), # type: ignore[arg-type] + password=os.getenv("MT5_PASSWORD"), + server=os.getenv("MT5_SERVER"), + path=os.getenv("MT5_PATH"), + ): + logger.info("MT5 initialized successfully") + mt5.shutdown() + success = True + break + except (ConnectionError, ValueError, TypeError) as e: + logger.info(f"Connection error: {e}") + try: + mt5.initialize() + except (ConnectionError, ValueError, TypeError) as e: + logger.info(f"Attempt {attempt+1}: Not ready yet, sleeping... Error: {e}") + time.sleep(5) if not success: logger.info("Failed to initialize MT5 after waiting.") diff --git a/tests/test_book.py b/tests/test_book.py new file mode 100644 index 0000000..248ba61 --- /dev/null +++ b/tests/test_book.py @@ -0,0 +1,149 @@ +"""Tests for the Book class that manages market depth information from MetaTrader 5.""" + +from __future__ import annotations + +import logging +import time +from typing import TYPE_CHECKING, Generator + +import MetaTrader5 as Mt5 +import pytest + +if TYPE_CHECKING: + from _pytest.logging import LogCaptureFixture + +from mqpy.book import Book + + +@pytest.fixture(scope="module", autouse=True) +def setup_teardown() -> Generator[None, None, None]: + """Set up and tear down MetaTrader5 connection for the test module.""" + if not Mt5.initialize(): + pytest.skip("MetaTrader5 could not be initialized") + + time.sleep(5) + + yield + + Mt5.shutdown() + + +@pytest.fixture +def symbol() -> str: + """Provides a valid trading symbol for testing.""" + time.sleep(1) + + symbols = Mt5.symbols_get() + if not symbols: + pytest.skip("No symbols available for testing") + + for symbol in symbols: + if symbol.name == "EURUSD": + return "EURUSD" + + return symbols[0].name + + +def test_book_initialization(symbol: str, caplog: LogCaptureFixture) -> None: + """Test initialization of Book with a real symbol.""" + caplog.set_level(logging.INFO) + # Create book instance (used to trigger log message) + Book(symbol) + + assert f"The symbol {symbol} was successfully added to the market book" in caplog.text + + +def test_book_get(symbol: str) -> None: + """Test getting real market book data.""" + book = Book(symbol) + + time.sleep(1) + + market_data = book.get() + + assert market_data is not None + + if market_data: + assert isinstance(market_data, list) + + # Loop separately to check for bids and asks + has_bids = False + has_asks = False + + for item in market_data: + if item.type == Mt5.BOOK_TYPE_SELL: + has_bids = True + if item.type == Mt5.BOOK_TYPE_BUY: + has_asks = True + + if not (has_bids or has_asks): + logging.warning(f"No bids or asks found in market book for {symbol}") + + book.release() + + +def test_book_release(symbol: str) -> None: + """Test releasing the market book.""" + book = Book(symbol) + + result = book.release() + + assert result is True + + +def test_full_workflow(symbol: str) -> None: + """Test a complete workflow with the real market book.""" + book = Book(symbol) + + time.sleep(1) + + market_data = book.get() + + assert market_data is not None + + release_result = book.release() + assert release_result is True + + time.sleep(1) + data_after_release = book.get() + + if data_after_release is not None and len(data_after_release) > 0: + logging.info("Market book data still available after release") + + +def test_multiple_symbols() -> None: + """Test using Book with multiple symbols simultaneously.""" + symbols = Mt5.symbols_get() + if len(symbols) < 2: + pytest.skip("Need at least 2 symbols for this test") + + symbol1 = symbols[0].name + symbol2 = symbols[1].name + + book1 = Book(symbol1) + book2 = Book(symbol2) + + time.sleep(1) + + data1 = book1.get() + data2 = book2.get() + + assert data1 is not None + assert data2 is not None + + book1.release() + book2.release() + + +def test_unavailable_symbol(caplog: LogCaptureFixture) -> None: + """Test behavior with an unavailable symbol.""" + caplog.set_level(logging.ERROR) + + invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" + + book = Book(invalid_symbol) + + assert "Error adding INVALID_SYMBOL_THAT_DOESNT_EXIST to the market book" in caplog.text + + release_result = book.release() + assert release_result is False diff --git a/tests/test_rates.py b/tests/test_rates.py new file mode 100644 index 0000000..948b324 --- /dev/null +++ b/tests/test_rates.py @@ -0,0 +1,157 @@ +"""Tests for the Rates class that retrieves historical price data from MetaTrader 5.""" + +from __future__ import annotations + +import logging +import time +from typing import Generator + +import MetaTrader5 as Mt5 +import pytest + +from mqpy.rates import Rates + + +@pytest.fixture(scope="module", autouse=True) +def setup_teardown() -> Generator[None, None, None]: + """Set up and tear down MetaTrader5 connection for the test module.""" + if not Mt5.initialize(): + pytest.skip("MetaTrader5 could not be initialized") + + time.sleep(5) + + yield + + Mt5.shutdown() + + +@pytest.fixture +def symbol() -> str: + """Provides a valid trading symbol for testing.""" + time.sleep(1) + + symbols = Mt5.symbols_get() + if not symbols: + pytest.skip("No symbols available for testing") + + for symbol in symbols: + if symbol.name == "EURUSD": + return "EURUSD" + + return symbols[0].name + + +@pytest.fixture +def timeframe() -> int: + """Provides a valid timeframe for testing.""" + return Mt5.TIMEFRAME_H1 + + +def test_rates_initialization(symbol: str, timeframe: int) -> None: + """Test initialization of Rates with a real symbol.""" + rates = Rates(symbol, timeframe, 0, 10) + + assert len(rates.time) == 10 + assert len(rates.open) == 10 + assert len(rates.high) == 10 + assert len(rates.low) == 10 + assert len(rates.close) == 10 + assert len(rates.tick_volume) == 10 + assert len(rates.spread) == 10 + assert len(rates.real_volume) == 10 + + +def test_rates_data_types(symbol: str, timeframe: int) -> None: + """Test data types of all Rates properties.""" + rates = Rates(symbol, timeframe, 0, 5) + + assert rates.time[0] is not None + assert isinstance(rates.open[0], float) + assert isinstance(rates.high[0], float) + assert isinstance(rates.low[0], float) + assert isinstance(rates.close[0], float) + + assert rates.tick_volume[0] >= 0 + assert rates.spread[0] >= 0 + assert rates.real_volume[0] >= 0 + + logging.info(f"Type of tick_volume: {type(rates.tick_volume[0])}") + logging.info(f"Type of spread: {type(rates.spread[0])}") + logging.info(f"Type of real_volume: {type(rates.real_volume[0])}") + + +def test_rates_data_relationships(symbol: str, timeframe: int) -> None: + """Test relationships between rate data points.""" + rates = Rates(symbol, timeframe, 0, 10) + + for i in range(len(rates.high)): + assert rates.high[i] >= rates.open[i] + assert rates.high[i] >= rates.close[i] + assert rates.high[i] >= rates.low[i] + + assert rates.low[i] <= rates.open[i] + assert rates.low[i] <= rates.close[i] + assert rates.low[i] <= rates.high[i] + + assert rates.spread[i] >= 0 + assert rates.tick_volume[i] >= 0 + assert rates.real_volume[i] >= 0 + + +def test_different_timeframes(symbol: str) -> None: + """Test retrieving rates with different timeframes.""" + timeframes = [Mt5.TIMEFRAME_M1, Mt5.TIMEFRAME_M5, Mt5.TIMEFRAME_H1, Mt5.TIMEFRAME_D1] + + for tf in timeframes: + rates = Rates(symbol, tf, 0, 5) + + assert len(rates.time) == 5 + assert len(rates.open) == 5 + assert len(rates.close) == 5 + + logging.info(f"Successfully retrieved rates for {symbol} with timeframe {tf}") + + +def test_different_counts(symbol: str, timeframe: int) -> None: + """Test retrieving different number of rates.""" + counts = [1, 10, 50] + + for count in counts: + rates = Rates(symbol, timeframe, 0, count) + + assert len(rates.time) == count + assert len(rates.open) == count + assert len(rates.close) == count + + logging.info(f"Successfully retrieved {count} rates for {symbol}") + + +def test_different_start_positions(symbol: str, timeframe: int) -> None: + """Test retrieving rates from different start positions.""" + start_positions = [0, 10, 50] + + for pos in start_positions: + rates = Rates(symbol, timeframe, pos, 5) + + assert len(rates.time) == 5 + assert len(rates.open) == 5 + assert len(rates.close) == 5 + + logging.info(f"Successfully retrieved rates for {symbol} from position {pos}") + + +def test_invalid_symbol() -> None: + """Test behavior with an invalid symbol.""" + invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" + + with pytest.raises(ValueError, match="Failed to create Rates object"): + Rates(invalid_symbol, Mt5.TIMEFRAME_H1, 0, 10) + + +def test_invalid_parameters(symbol: str) -> None: + """Test behavior with invalid parameter values.""" + with pytest.raises(ValueError, match="Failed to create Rates object"): + Rates(symbol, Mt5.TIMEFRAME_H1, 0, -1) + + with pytest.raises(ValueError, match="Failed to create Rates object"): + Rates(symbol, 9999, 0, 10) diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..880c7a2 --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,320 @@ +"""Tests for the template module that generates MT5 expert advisor template files.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +# Import the template functions directly for function-level testing +from mqpy.template import main + + +def test_template_generates_file_with_default_values() -> None: + """Test that the template generates a file with default values.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Run the main function directly + sys.argv = ["template.py"] # Reset sys.argv + main() + + # Verify file was created with default name + assert Path("demo.py").exists() + + # Check the content contains expected defaults + with Path("demo.py").open(encoding="utf-8") as file: + content = file.read() + assert 'symbol="EURUSD"' in content + assert "Moving Average Crossover" in content + assert "short_window_size = 5" in content + assert "long_window_size = 20" in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_template_generates_file_with_custom_values() -> None: + """Test that the template generates a file with custom values.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Set custom command line arguments + sys.argv = ["template.py", "--file_name", "custom_strategy", "--symbol", "BTCUSD"] + main() + + # Verify file was created with custom name + assert Path("custom_strategy.py").exists() + + # Check the content contains the custom symbol + with Path("custom_strategy.py").open(encoding="utf-8") as file: + content = file.read() + assert 'symbol="BTCUSD"' in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_template_overwrites_existing_file() -> None: + """Test that the template overwrites an existing file.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Create an existing file with known content + Path("overwrite_test.py").write_text("ORIGINAL CONTENT THAT SHOULD BE REPLACED", encoding="utf-8") + + # Run the template generator with the same filename + sys.argv = ["template.py", "--file_name", "overwrite_test", "--symbol", "EURUSD"] + main() + + # Verify the file was overwritten + with Path("overwrite_test.py").open(encoding="utf-8") as file: + content = file.read() + assert "ORIGINAL CONTENT THAT SHOULD BE REPLACED" not in content + assert 'symbol="EURUSD"' in content + assert "Moving Average Crossover" in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_template_runs_as_script() -> None: + """Test that the template script can be run through Python.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Run the template script using subprocess with custom parameters + result = subprocess.run( # noqa: S603 + [sys.executable, "-m", "mqpy.template", "--file_name", "script_test", "--symbol", "XAUUSD"], + capture_output=True, + text=True, + check=True, + ) + + # Check that the process ran successfully + assert result.returncode == 0 + + # Verify file was created with the right name + assert Path("script_test.py").exists() + + # Check the content contains the custom symbol + with Path("script_test.py").open(encoding="utf-8") as file: + content = file.read() + assert 'symbol="XAUUSD"' in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_template_creates_valid_python_file() -> None: + """Test that the generated template is valid Python code.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Generate a template file + sys.argv = ["template.py", "--file_name", "syntax_test"] + main() + + # Try to compile the generated file to check for syntax errors + with Path("syntax_test.py").open(encoding="utf-8") as file: + content = file.read() + + # This will raise a SyntaxError if the code is not valid + compile(content, "syntax_test.py", "exec") + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +# CLI-specific tests +def test_cli_command_help() -> None: + """Test that the CLI command --help option works correctly.""" + result = subprocess.run( # noqa: S603 + [sys.executable, "-m", "mqpy.template", "--help"], capture_output=True, text=True, check=False + ) + + # Check that the command ran successfully + assert result.returncode == 0 + + # Check that the help output contains expected content + assert "Generate MetaTrader 5 expert advisor templates" in result.stdout + assert "--file_name" in result.stdout + assert "--symbol" in result.stdout + assert "--strategy" in result.stdout + assert "moving_average" in result.stdout + + +def test_cli_different_strategies() -> None: + """Test generating all different strategy types via CLI.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Test each strategy type + strategies = ["moving_average", "rsi", "macd", "bollinger"] + + for strategy in strategies: + # Run the CLI command + result = subprocess.run( # noqa: S603 + [sys.executable, "-m", "mqpy.template", "--strategy", strategy, "--file_name", f"test_{strategy}"], + capture_output=True, + text=True, + check=False, + ) + + # Check the command ran successfully + assert result.returncode == 0 + + # Verify file was created + assert Path(f"test_{strategy}.py").exists() + + # Check strategy-specific content + with Path(f"test_{strategy}.py").open(encoding="utf-8") as file: + content = file.read() + + # Check for strategy-specific indicators + if strategy == "moving_average": + assert "short_window_size = 5" in content + assert "long_window_size = 20" in content + elif strategy == "rsi": + assert "rsi_period = 14" in content + assert "oversold_threshold = 30" in content + assert "calculate_rsi(" in content + elif strategy == "macd": + assert "fast_period = 12" in content + assert "slow_period = 26" in content + assert "calculate_macd(" in content + elif strategy == "bollinger": + assert "bb_period = 20" in content + assert "std_dev_multiplier = 2.0" in content + assert "calculate_bollinger_bands(" in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_cli_custom_directory() -> None: + """Test that the --directory option works correctly.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Create a subdirectory path that doesn't exist yet + custom_dir = Path(temp_dir) / "custom_output_dir" + + # Run the CLI command with custom directory + result = subprocess.run( # noqa: S603 + [sys.executable, "-m", "mqpy.template", "--file_name", "dir_test", "--directory", str(custom_dir)], + capture_output=True, + text=True, + check=False, + ) + + # Check the command ran successfully + assert result.returncode == 0 + + # Verify directory was created + assert custom_dir.exists() + + # Verify file was created in the custom directory + assert (custom_dir / "dir_test.py").exists() + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) + + +def test_cli_custom_parameters() -> None: + """Test that the CLI command accepts custom trading parameters.""" + # Create a temporary directory for testing + temp_dir = tempfile.mkdtemp() + try: + # Save current directory and move to temp directory + original_dir = Path.cwd() + os.chdir(temp_dir) + + # Run the CLI command with custom parameters + result = subprocess.run( # noqa: S603 + [ + sys.executable, + "-m", + "mqpy.template", + "--file_name", + "params_test", + "--symbol", + "GBPJPY", + "--magic_number", + "12345", + "--lot", + "0.25", + "--stop_loss", + "30", + "--take_profit", + "60", + ], + capture_output=True, + text=True, + check=False, + ) + + # Check the command ran successfully + assert result.returncode == 0 + + # Verify file was created + assert Path("params_test.py").exists() + + # Check custom parameters in the content + with Path("params_test.py").open(encoding="utf-8") as file: + content = file.read() + assert 'symbol="GBPJPY"' in content + assert "magic_number=12345" in content + assert "lot=0.25" in content + assert "stop_loss=30" in content + assert "take_profit=60" in content + + finally: + # Clean up: restore original directory and remove temp directory + os.chdir(original_dir) + shutil.rmtree(temp_dir) diff --git a/tests/test_tick.py b/tests/test_tick.py new file mode 100644 index 0000000..110ab07 --- /dev/null +++ b/tests/test_tick.py @@ -0,0 +1,133 @@ +"""Tests for the Tick class that retrieves real-time tick data from MetaTrader 5.""" + +from __future__ import annotations + +import logging +import time +from typing import Generator + +import MetaTrader5 as Mt5 +import pytest + +from mqpy.tick import Tick + + +@pytest.fixture(scope="module", autouse=True) +def setup_teardown() -> Generator[None, None, None]: + """Set up and tear down MetaTrader5 connection for the test module.""" + if not Mt5.initialize(): + pytest.skip("MetaTrader5 could not be initialized") + + time.sleep(5) + + yield + + Mt5.shutdown() + + +@pytest.fixture +def symbol() -> str: + """Provides a valid trading symbol for testing.""" + time.sleep(1) + + symbols = Mt5.symbols_get() + if not symbols: + pytest.skip("No symbols available for testing") + + for symbol in symbols: + if symbol.name == "EURUSD": + return "EURUSD" + + return symbols[0].name + + +def test_tick_initialization(symbol: str) -> None: + """Test initialization of Tick with a real symbol.""" + tick = Tick(symbol) + + assert tick.symbol == symbol + + assert isinstance(tick.time, int) + assert isinstance(tick.bid, float) + assert isinstance(tick.ask, float) + assert tick.ask >= 0 + assert tick.bid >= 0 + assert tick.ask >= tick.bid + + +def test_tick_properties(symbol: str) -> None: + """Test all Tick properties with a real symbol.""" + tick = Tick(symbol) + + assert isinstance(tick.symbol, str) + assert isinstance(tick.time, int) + assert isinstance(tick.bid, float) + assert isinstance(tick.ask, float) + assert isinstance(tick.time_msc, int) + assert isinstance(tick.flags, int) + + # Check last property + if tick.last is not None: + assert isinstance(tick.last, float) + + # Check volume property + if tick.volume is not None: + assert isinstance(tick.volume, int) + + if tick.volume_real is not None: + assert isinstance(tick.volume_real, float) + + +def test_updated_tick_data(symbol: str) -> None: + """Test getting updated tick data after waiting.""" + first_tick = Tick(symbol) + first_time = first_tick.time + + time.sleep(2) + + second_tick = Tick(symbol) + second_time = second_tick.time + + if first_time == second_time: + # Log instead of print + logging.info(f"No tick update for {symbol} after 2 seconds") + + +def test_multiple_symbols() -> None: + """Test Tick with multiple symbols simultaneously.""" + symbols = Mt5.symbols_get() + if len(symbols) < 2: + pytest.skip("Need at least 2 symbols for this test") + + symbol1 = symbols[0].name + symbol2 = symbols[1].name + + tick1 = Tick(symbol1) + tick2 = Tick(symbol2) + + assert tick1.symbol == symbol1 + assert tick2.symbol == symbol2 + + assert isinstance(tick1.bid, float) + assert isinstance(tick1.ask, float) + assert isinstance(tick2.bid, float) + assert isinstance(tick2.ask, float) + + +def test_invalid_symbol() -> None: + """Test behavior with an invalid symbol.""" + invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" + + with pytest.raises(AttributeError, match="'NoneType' object has no attribute 'time'"): + Tick(invalid_symbol) + + +def test_spread_calculation(symbol: str) -> None: + """Test spread calculation from bid/ask values.""" + tick = Tick(symbol) + + spread = tick.ask - tick.bid + + assert spread >= 0 + + logging.info(f"Spread for {symbol}: {spread}") diff --git a/tests/test_trade.py b/tests/test_trade.py new file mode 100644 index 0000000..9379107 --- /dev/null +++ b/tests/test_trade.py @@ -0,0 +1,441 @@ +"""Tests for the Trade class that manages trading operations with MetaTrader 5.""" + +from __future__ import annotations + +import contextlib +import logging +import time +from typing import Generator + +import MetaTrader5 as Mt5 +import pytest + +from mqpy.trade import Trade + +logger = logging.getLogger(__name__) + +# Constants for test values +TEST_LOT_SIZE = 0.01 +TEST_STOP_LOSS = 50.0 +TEST_EMERGENCY_STOP_LOSS = 100.0 +TEST_TAKE_PROFIT = 100.0 +TEST_EMERGENCY_TAKE_PROFIT = 200.0 +TEST_MAGIC_NUMBER = 12345 +TEST_FEE = 1.5 + + +@pytest.fixture(scope="module", autouse=True) +def setup_teardown() -> Generator[None, None, None]: + """Set up and tear down MetaTrader5 connection for the test module.""" + if not Mt5.initialize(): + pytest.skip("MetaTrader5 could not be initialized") + + time.sleep(5) + + yield + + try: + positions = Mt5.positions_get() + if positions: + for position in positions: + if position.symbol == "EURUSD": + if position.type == 0: # Buy position + Mt5.order_send( + { + "action": Mt5.TRADE_ACTION_DEAL, + "symbol": "EURUSD", + "volume": position.volume, + "type": Mt5.ORDER_TYPE_SELL, + "position": position.ticket, + "price": Mt5.symbol_info_tick("EURUSD").bid, + "deviation": 20, + "magic": 12345, + "comment": "Close test position", + "type_time": Mt5.ORDER_TIME_GTC, + "type_filling": Mt5.ORDER_FILLING_FOK, + } + ) + else: # Sell position + Mt5.order_send( + { + "action": Mt5.TRADE_ACTION_DEAL, + "symbol": "EURUSD", + "volume": position.volume, + "type": Mt5.ORDER_TYPE_BUY, + "position": position.ticket, + "price": Mt5.symbol_info_tick("EURUSD").ask, + "deviation": 20, + "magic": 12345, + "comment": "Close test position", + "type_time": Mt5.ORDER_TIME_GTC, + "type_filling": Mt5.ORDER_FILLING_FOK, + } + ) + except Exception: + logger.exception("Error cleaning up positions") + + Mt5.shutdown() + + +@pytest.fixture +def symbol() -> str: + """Provides a valid trading symbol for testing.""" + time.sleep(1) + + symbols = Mt5.symbols_get() + if not symbols: + pytest.skip("No symbols available for testing") + + for symbol in symbols: + if symbol.name == "EURUSD": + return "EURUSD" + + return symbols[0].name + + +@pytest.fixture(autouse=True) +def cleanup_positions(symbol: str) -> None: + """Close any open positions after each test.""" + yield + # Clean up after the test + Mt5.initialize() + positions = Mt5.positions_get(symbol=symbol) + if positions and len(positions) > 0: + try: + for position in positions: + if position.symbol == symbol: + # Close the position + request = { + "action": Mt5.TRADE_ACTION_DEAL, + "symbol": symbol, + "volume": position.volume, + "type": Mt5.ORDER_TYPE_BUY if position.type == 1 else Mt5.ORDER_TYPE_SELL, + "position": position.ticket, + "price": Mt5.symbol_info_tick(symbol).ask + if position.type == 1 + else Mt5.symbol_info_tick(symbol).bid, + "deviation": 20, + "magic": 12345, + "comment": "Close position", + "type_time": Mt5.ORDER_TIME_GTC, + "type_filling": Mt5.ORDER_FILLING_RETURN, + } + Mt5.order_send(request) + except Exception: + logger.exception("Error cleaning up positions") + + Mt5.shutdown() + + +@pytest.fixture +def trade(symbol: str) -> Trade: + """Create a Trade instance for testing.""" + trade = Trade( + expert_name="TestExpert", + version="1.0", + symbol=symbol, + magic_number=TEST_MAGIC_NUMBER, + lot=TEST_LOT_SIZE, + stop_loss=TEST_STOP_LOSS, + emergency_stop_loss=TEST_EMERGENCY_STOP_LOSS, + take_profit=TEST_TAKE_PROFIT, + emergency_take_profit=TEST_EMERGENCY_TAKE_PROFIT, + ) + + positions = Mt5.positions_get(symbol=symbol) + if positions: + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + trade.close_position("Cleaning up for tests") + + return trade + + +def test_trade_initialization(symbol: str) -> None: + """Test the initialization of the Trade class.""" + trade = Trade( + expert_name="TestExpert", + version="1.0", + symbol=symbol, + magic_number=TEST_MAGIC_NUMBER, + lot=TEST_LOT_SIZE, + stop_loss=TEST_STOP_LOSS, + emergency_stop_loss=TEST_EMERGENCY_STOP_LOSS, + take_profit=TEST_TAKE_PROFIT, + emergency_take_profit=TEST_EMERGENCY_TAKE_PROFIT, + ) + + assert trade.expert_name == "TestExpert" + assert trade.version == "1.0" + assert trade.symbol == symbol + assert trade.magic_number == TEST_MAGIC_NUMBER + assert trade.lot == TEST_LOT_SIZE + assert trade.stop_loss == TEST_STOP_LOSS + assert trade.emergency_stop_loss == TEST_EMERGENCY_STOP_LOSS + assert trade.take_profit == TEST_TAKE_PROFIT + assert trade.emergency_take_profit == TEST_EMERGENCY_TAKE_PROFIT + + assert trade.start_time_hour == "9" + assert trade.start_time_minutes == "15" + assert trade.finishing_time_hour == "17" + assert trade.finishing_time_minutes == "30" + assert trade.ending_time_hour == "17" + assert trade.ending_time_minutes == "50" + assert trade.fee == 0.0 + + assert trade.loss_deals == 0 + assert trade.profit_deals == 0 + assert trade.total_deals == 0 + assert trade.balance == 0.0 + assert trade.ticket == 0 + + assert Mt5.symbol_info(symbol) is not None + + +def test_trade_with_custom_times(symbol: str) -> None: + """Test the initialization of the Trade class with custom times.""" + trade = Trade( + expert_name="TestExpert", + version="1.0", + symbol=symbol, + magic_number=TEST_MAGIC_NUMBER, + lot=TEST_LOT_SIZE, + stop_loss=TEST_STOP_LOSS, + emergency_stop_loss=TEST_EMERGENCY_STOP_LOSS, + take_profit=TEST_TAKE_PROFIT, + emergency_take_profit=TEST_EMERGENCY_TAKE_PROFIT, + start_time="10:30", + finishing_time="16:45", + ending_time="17:00", + fee=TEST_FEE, + ) + + assert trade.start_time_hour == "10" + assert trade.start_time_minutes == "30" + assert trade.finishing_time_hour == "16" + assert trade.finishing_time_minutes == "45" + assert trade.ending_time_hour == "17" + assert trade.ending_time_minutes == "00" + assert trade.fee == TEST_FEE + + +def test_symbol_selection(trade: Trade) -> None: + """Test that a symbol can be selected.""" + with contextlib.suppress(Exception): + Mt5.symbol_select(trade.symbol, False) + + trade.select_symbol() + + symbol_info = Mt5.symbol_info(trade.symbol) + assert symbol_info is not None + assert symbol_info.visible is True + + +def test_prepare_symbol(trade: Trade) -> None: + """Test preparing a symbol for trading.""" + trade.prepare_symbol() + + symbol_info = Mt5.symbol_info(trade.symbol) + assert symbol_info is not None + assert symbol_info.visible is True + + +def test_trading_time_calculation() -> None: + """Test the trading time calculation logic.""" + trade = Trade( + expert_name="TestExpert", + version="1.0", + symbol="EURUSD", + magic_number=TEST_MAGIC_NUMBER, + lot=TEST_LOT_SIZE, + stop_loss=TEST_STOP_LOSS, + emergency_stop_loss=TEST_EMERGENCY_STOP_LOSS, + take_profit=TEST_TAKE_PROFIT, + emergency_take_profit=TEST_EMERGENCY_TAKE_PROFIT, + start_time="0:00", + finishing_time="23:59", + ) + + result = trade.trading_time() + assert isinstance(result, bool) + + +def test_days_end_calculation() -> None: + """Test the days_end calculation logic.""" + trade = Trade( + expert_name="TestExpert", + version="1.0", + symbol="EURUSD", + magic_number=TEST_MAGIC_NUMBER, + lot=TEST_LOT_SIZE, + stop_loss=TEST_STOP_LOSS, + emergency_stop_loss=TEST_EMERGENCY_STOP_LOSS, + take_profit=TEST_TAKE_PROFIT, + emergency_take_profit=TEST_EMERGENCY_TAKE_PROFIT, + ending_time="23:59", + ) + + result = trade.days_end() + assert isinstance(result, bool) + + +def test_statistics_with_zero_deals(trade: Trade) -> None: + """Test the statistics method with zero deals.""" + trade.statistics() + + +def test_statistics_with_deals(trade: Trade) -> None: + """Test the statistics method with some deals.""" + trade.profit_deals = 3 + trade.loss_deals = 2 + trade.total_deals = 5 + trade.balance = 150.0 + + trade.statistics() + + +def _check_position(magic_number: int, expected_type: int) -> bool: + """Helper to check if a position of the expected type exists. + + Args: + magic_number: The magic number to filter positions by + expected_type: The expected position type (Buy or Sell) + + Returns: + True if a position of the expected type exists, False otherwise + """ + positions = Mt5.positions_get() + if not positions: + return False + + return any(position.magic == magic_number and position.type == expected_type for position in positions) + + +@pytest.mark.real_trading +def test_open_position_with_conditions(trade: Trade) -> None: + """Test the open_position method with buy/sell conditions.""" + try: + # Cleanup any existing positions + positions = Mt5.positions_get(symbol=trade.symbol) + if positions: + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + trade.close_position("Cleaning up for test") + + # Configure trade time settings for 24/7 trading + trade.start_time_hour = "0" + trade.start_time_minutes = "00" + trade.finishing_time_hour = "23" + trade.finishing_time_minutes = "59" + + # Test buy condition + trade.open_position(should_buy=True, should_sell=False, comment="Test Buy Condition") + time.sleep(2) + + assert _check_position(TEST_MAGIC_NUMBER, Mt5.ORDER_TYPE_BUY) + trade.close_position("Cleaning up after Buy test") + time.sleep(2) + + # Test sell condition + trade.open_position(should_buy=False, should_sell=True, comment="Test Sell Condition") + time.sleep(2) + + assert _check_position(TEST_MAGIC_NUMBER, Mt5.ORDER_TYPE_SELL) + trade.close_position("Cleaning up after Sell test") + time.sleep(2) + + except (ConnectionError, TimeoutError, RuntimeError) as e: + pytest.skip(f"Trading error: {e} - Skipping real trading test") + + +@pytest.mark.real_trading +def test_open_buy_position(trade: Trade) -> None: + """Test opening a Buy position with real trades.""" + try: + positions = Mt5.positions_get(symbol=trade.symbol) + initial_positions_count = len(positions) if positions else 0 + + trade.open_buy_position("Test Buy Position") + + time.sleep(2) + + positions = Mt5.positions_get(symbol=trade.symbol) + if positions is not None: + assert len(positions) >= initial_positions_count + + latest_position = None + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + latest_position = position + break + + if latest_position is not None: + assert latest_position.type == Mt5.ORDER_TYPE_BUY + except (ConnectionError, TimeoutError, RuntimeError) as e: + pytest.skip(f"Trading error: {e} - Skipping real trading test") + + +@pytest.mark.real_trading +def test_open_sell_position(trade: Trade) -> None: + """Test opening a Sell position with real trades.""" + try: + positions = Mt5.positions_get(symbol=trade.symbol) + if positions: + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + trade.close_position("Cleaning up for test_open_sell_position") + + trade.open_sell_position("Test Sell Position") + + time.sleep(2) + + positions = Mt5.positions_get(symbol=trade.symbol) + assert positions is not None + + has_sell_position = False + for position in positions: + if position.magic == TEST_MAGIC_NUMBER and position.type == Mt5.ORDER_TYPE_SELL: + has_sell_position = True + break + + assert has_sell_position + except (ConnectionError, TimeoutError, RuntimeError) as e: + pytest.skip(f"Trading error: {e} - Skipping real trading test") + + +@pytest.mark.real_trading +def test_close_position(trade: Trade) -> None: + """Test closing a position with real trades.""" + try: + trade.open_buy_position("Test Position to Close") + + time.sleep(2) + + positions = Mt5.positions_get(symbol=trade.symbol) + assert positions is not None + + has_position = False + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + has_position = True + break + + if not has_position: + pytest.skip("Could not open position to test closing - skipping test") + + trade.close_position("Test Closing Position") + + time.sleep(2) + + positions = Mt5.positions_get(symbol=trade.symbol) + + has_position = False + if positions: + for position in positions: + if position.magic == TEST_MAGIC_NUMBER: + has_position = True + break + + assert not has_position + except (ConnectionError, TimeoutError, RuntimeError) as e: + pytest.skip(f"Trading error: {e} - Skipping real trading test") diff --git a/tests/test_utilities.py b/tests/test_utilities.py new file mode 100644 index 0000000..66deb79 --- /dev/null +++ b/tests/test_utilities.py @@ -0,0 +1,96 @@ +"""Tests for the Utilities class that provides helper functions for trading operations.""" + +from __future__ import annotations + +import time +from typing import Generator + +import MetaTrader5 as Mt5 +import pytest + +from mqpy.utilities import Utilities + + +@pytest.fixture(scope="module", autouse=True) +def setup_teardown() -> Generator[None, None, None]: + """Set up and tear down MetaTrader5 connection for the test module.""" + if not Mt5.initialize(): + pytest.skip("MetaTrader5 could not be initialized") + + time.sleep(5) + + yield + + Mt5.shutdown() + + +@pytest.fixture +def symbol() -> str: + """Provides a valid trading symbol for testing.""" + time.sleep(1) + + symbols = Mt5.symbols_get() + if not symbols: + pytest.skip("No symbols available for testing") + + for symbol in symbols: + if symbol.name == "EURUSD": + return "EURUSD" + + return symbols[0].name + + +@pytest.fixture +def utilities() -> Utilities: + """Provides a Utilities instance for testing.""" + return Utilities() + + +def test_utilities_initialization() -> None: + """Test initialization of Utilities class.""" + utilities = Utilities() + assert isinstance(utilities, Utilities) + symbol = "EURUSD" + assert utilities.check_trade_availability(symbol, 5) is True + + +def test_multiple_utilities_instances() -> None: + """Test that multiple Utilities instances work independently.""" + utilities1 = Utilities() + utilities2 = Utilities() + assert utilities1.check_trade_availability("EURUSD", 5) is True + assert utilities2.check_trade_availability("EURUSD", 5) is True + assert utilities1 is not utilities2 + + +def test_utilities_attributes(utilities: Utilities) -> None: + """Test that the Utilities class has the expected attributes.""" + assert hasattr(utilities, "_test_get_minutes_counter") + assert hasattr(utilities, "_test_get_counter_flag") + assert hasattr(utilities, "_test_get_allowed_to_trade") + assert hasattr(utilities, "_test_get_allow_to_count") + assert hasattr(utilities, "_test_get_recent_trade") + + assert utilities._test_get_minutes_counter() == 0 + assert utilities._test_get_counter_flag() is True + assert utilities._test_get_allowed_to_trade() is True + assert utilities._test_get_allow_to_count() is False + assert utilities._test_get_recent_trade() is False + + assert hasattr(utilities, "check_trade_availability") + assert callable(utilities.check_trade_availability) + + +def test_reset_counters_functionality(utilities: Utilities) -> None: + """Test the reset_counters functionality directly.""" + utilities._test_set_minutes_counter(5) + utilities._test_set_counter_flag(False) + utilities._test_set_allowed_to_trade(False) + utilities._test_set_allow_to_count(True) + + utilities._test_reset_counters() + + assert utilities._test_get_minutes_counter() == 0 + assert utilities._test_get_counter_flag() is True + assert utilities._test_get_allowed_to_trade() is True + assert utilities._test_get_allow_to_count() is False diff --git a/uv.lock b/uv.lock index 57d6fb5..4712785 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.9" +requires-python = ">=3.8" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", From 9d34eb71de0f02c2448b7fecff7631d45e771b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Euko?= <50120357+Joaopeuko@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:00:02 +0100 Subject: [PATCH 3/4] fix: Fix documentation (#38) ### Description Remove `UV` from documentation deployment. Fixes #37 --- .github/workflows/deploy-mkdocs.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-mkdocs.yml b/.github/workflows/deploy-mkdocs.yml index 5d3d060..41564b9 100644 --- a/.github/workflows/deploy-mkdocs.yml +++ b/.github/workflows/deploy-mkdocs.yml @@ -20,13 +20,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install uv and set the python version - uses: astral-sh/setup-uv@v5 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: "3.11" - - - name: Install the project - run: uv sync --locked --group docs + python-version: '3.11' + + - name: Install the dependencies + run: | + pip install ` + "mkdocs>=1.6.1" ` + "mkdocs-gen-files>=0.5.0" ` + "mkdocs-jupyter>=0.25.1" ` + "mkdocs-literate-nav>=0.6.2" ` + "mkdocs-material>=9.6.12" ` + "mkdocs-section-index>=0.3.10" ` + "mkdocstrings[python]>=0.29.1" - name: Build MkDocs run: mkdocs build --site-dir ./deploy From 775aaf11597fa8b6261470b8fdb8629374e8afc2 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sun, 27 Apr 2025 21:03:52 +0000 Subject: [PATCH 4/4] chore(release): v0.6.10 --- CHANGELOG.md | 429 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7cd0e1b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,429 @@ +# CHANGELOG + + +## v0.6.10 (2025-04-27) + +### Bug Fixes + +- Fix all the broken changes from the last pr + ([#36](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/36), + [`fa5d1fa`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/fa5d1fa1a949fc63b05b2fade35dd78756d6f928)) + +### Description + +This PR improve documentation, Adds `unittest`, that, don't use mock data, to ensure it works. Make + the code work again. + +Fixes #35 + +- Fix documentation ([#38](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/38), + [`9d34eb7`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/9d34eb71de0f02c2448b7fecff7631d45e771b8a)) + +### Description + +Remove `UV` from documentation deployment. + +Fixes #37 + +### Refactoring + +- Re-arrange the project to make improvements + ([#34](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/34), + [`087ecff`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/087ecff5d2b07e840b9cb113878208846f188400)) + +### Description + +This PR adds a few things: + +- [x] Documentation page - [x] Pre-commit CI - [ ] Pytest CI - [x] Integration test CI + +Fixes #29 + +This is a breaking changes. + + +## v0.6.9 (2025-03-28) + +### Bug Fixes + +- File generation for mqpy command + ([`e2e395a`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/e2e395a4349dcab87858f3dd804f8e2562721e2d)) + +- File generation for mqpy command + ([`e106a62`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/e106a62b67362d106f12bdaf42836e4aea9b099a)) + +- File generation for mqpy command + ([`e6b6a21`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/e6b6a219c7a92b77e76415353fcec0a7cbe6c14a)) + +### Build System + +- **deps**: Bump actions/checkout from 2 to 4 + ([#9](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/9), + [`83e4fa6`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/83e4fa648cba3edd65d67730e7028b8f9cd7b535)) + +Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4. - [Release + notes](https://github.com/actions/checkout/releases) - + [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - + [Commits](https://github.com/actions/checkout/compare/v2...v4) + +--- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production + +update-type: version-update:semver-major ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump actions/setup-python from 2 to 5 + ([#8](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/8), + [`e386757`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/e386757679984be8170e4ba1cd95cc63abc029af)) + +Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 5. - [Release + notes](https://github.com/actions/setup-python/releases) - + [Commits](https://github.com/actions/setup-python/compare/v2...v5) + +--- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production + +update-type: version-update:semver-major ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 from 5.0.45 to 5.0.4682 + ([#10](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/10), + [`d011a4a`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/d011a4a78db7d1a2231ec2cc37771c9f60ca51f6)) + +Bumps [metatrader5](https://www.metatrader5.com) from 5.0.45 to 5.0.4682. + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#14](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/14), + [`672ace8`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/672ace84a9cb7a038596b0b5036cddafe67cde15)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.45 to 5.0.4682 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#15](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/15), + [`5fe3dc1`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/5fe3dc1256cd10cb2d2d19b6533c740dae043dde)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4682 to 5.0.4687 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#16](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/16), + [`4da9c50`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/4da9c50593bd22fea886a8e4e44caad0e82e4873)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4682 to 5.0.4687 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#17](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/17), + [`3ac0f09`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/3ac0f091b0c55be25004500c4e07c40d2859ebb2)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4687 to 5.0.4732 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#18](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/18), + [`73c668e`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/73c668eaf35b5c39a242893c9c47a39a210c2a58)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4687 to 5.0.4732 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#19](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/19), + [`f0b2918`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/f0b29187a6dcd181e4c517c207e7506d7a2c7260)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4732 to 5.0.4738 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#20](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/20), + [`381a459`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/381a4593d3c499376d3376501cd3a224174ffe2c)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4732 to 5.0.4738 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#23](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/23), + [`7bcc150`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/7bcc150754ce3cf63fd5f55a98df0664b8548416)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4738 to 5.0.4803 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#24](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/24), + [`a06a0b8`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/a06a0b8d76de3f2d9057e6c0d3edce717807416d)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4738 to 5.0.4803 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump metatrader5 in the python-requirements group + ([#25](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/25), + [`a4ebcd8`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/a4ebcd88f161269a316ecaf326444ee9a169b9b9)) + +Bumps the python-requirements group with 1 update: [metatrader5](https://www.metatrader5.com). + +Updates `metatrader5` from 5.0.4803 to 5.0.4874 + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump setuptools from 69.0.2 to 75.6.0 + ([#12](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/12), + [`07c6c63`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/07c6c6311b35b41bf3d4869222bd3e8112d2fa81)) + +Bumps [setuptools](https://github.com/pypa/setuptools) from 69.0.2 to 75.6.0. - [Release + notes](https://github.com/pypa/setuptools/releases) - + [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - + [Commits](https://github.com/pypa/setuptools/compare/v69.0.2...v75.6.0) + +--- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production + +update-type: version-update:semver-major ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump setuptools in the python-requirements group + ([#21](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/21), + [`12b55ee`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/12b55ee69f0c49bc00e972c4faf81611d19b94a2)) + +Bumps the python-requirements group with 1 update: [setuptools](https://github.com/pypa/setuptools). + +Updates `setuptools` from 75.6.0 to 75.7.0 - [Release + notes](https://github.com/pypa/setuptools/releases) - + [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - + [Commits](https://github.com/pypa/setuptools/compare/v75.6.0...v75.7.0) + +--- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production + +update-type: version-update:semver-minor + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump setuptools in the python-requirements group + ([#22](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/22), + [`655da8f`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/655da8f12473292c265b68cf9b53c050f68dd938)) + +Bumps the python-requirements group with 1 update: [setuptools](https://github.com/pypa/setuptools). + +Updates `setuptools` from 75.7.0 to 75.8.0 - [Release + notes](https://github.com/pypa/setuptools/releases) - + [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - + [Commits](https://github.com/pypa/setuptools/compare/v75.7.0...v75.8.0) + +--- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production + +update-type: version-update:semver-minor + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps**: Bump the python-requirements group with 2 updates + ([#28](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/28), + [`63de899`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/63de899ac9e8b103cecb34c10169aedbd5a8738b)) + +Bumps the python-requirements group with 2 updates: [metatrader5](https://www.metatrader5.com) and + [setuptools](https://github.com/pypa/setuptools). + +Updates `metatrader5` from 5.0.4803 to 5.0.4874 + +Updates `setuptools` from 75.8.0 to 78.1.0 - [Release + notes](https://github.com/pypa/setuptools/releases) - + [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - + [Commits](https://github.com/pypa/setuptools/compare/v75.8.0...v78.1.0) + +--- updated-dependencies: - dependency-name: metatrader5 dependency-type: direct:production + +update-type: version-update:semver-patch + +dependency-group: python-requirements + +- dependency-name: setuptools dependency-type: direct:production + +update-type: version-update:semver-major + +dependency-group: python-requirements ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps-dev**: Bump black from 23.11.0 to 24.8.0 + ([#13](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/13), + [`0c2de63`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/0c2de6351c92eb5ce282d2731eb6467d003ff2ac)) + +Bumps [black](https://github.com/psf/black) from 23.11.0 to 24.8.0. - [Release + notes](https://github.com/psf/black/releases) - + [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - + [Commits](https://github.com/psf/black/compare/23.11.0...24.8.0) + +--- updated-dependencies: - dependency-name: black dependency-type: direct:development + +update-type: version-update:semver-major ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +- **deps-dev**: Bump isort from 5.12.0 to 5.13.2 + ([#11](https://github.com/Joaopeuko/Mql5-Python-Integration/pull/11), + [`085b198`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/085b198b8d4ffbfb482992b280f69c7917dca6d2)) + +Bumps [isort](https://github.com/pycqa/isort) from 5.12.0 to 5.13.2. - [Release + notes](https://github.com/pycqa/isort/releases) - + [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - + [Commits](https://github.com/pycqa/isort/compare/5.12.0...5.13.2) + +--- updated-dependencies: - dependency-name: isort dependency-type: direct:development + +update-type: version-update:semver-minor ... + +Signed-off-by: dependabot[bot] + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +### Documentation + +- Add github template + ([`db14270`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/db1427033b8ab7f397a1a5fb1f39584a66b9922a)) + +- Improve README information + ([`b1f70dd`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/b1f70ddc408a29236df661009c8c55db2d236c02)) + +- Improve README information + ([`e0fd27d`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/e0fd27db11bbb07800fa29e2e149ca61607cbe1b)) + +### Features + +- Add command for autogenerating boilerplate code + ([`1e0033e`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/1e0033e9cb32684e1188973c7be983f6ed88d579)) + +### Refactoring + +- Rearrange file structure for better organization + ([`a6c79a7`](https://github.com/Joaopeuko/Mql5-Python-Integration/commit/a6c79a7c13255ef0c8d4ec4ed0846cc43b5258eb)) diff --git a/pyproject.toml b/pyproject.toml index 582638b..63362bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ name = "mqpy" authors = [ {email = "joao@example.com", name = "Joao Euko"}, ] -version = "v0.6.9" +version = "0.6.10" description = "I developed this library to simplify the process of creating an Expert Advisor in MQL5. While developing in MQL5 can be complex, the same task is more streamlined in Python." requires-python = ">=3.8" dependencies = []