├── .codespell-ignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── build.yaml │ ├── chore.yaml │ ├── ci.yaml │ ├── docs.yaml │ ├── feat.yaml │ ├── fix.yaml │ ├── perf.yaml │ ├── refactor.yaml │ ├── style.yaml │ └── test.yaml ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── deploy-mkdocs.yml │ ├── deploy-pypi-packages.yaml │ ├── deploy-semantic-release.yaml │ ├── test-pre-commit.yaml │ └── test-pytest-and-integration.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── assets │ ├── favicon.svg │ ├── logo-icon.svg │ └── logo.svg ├── contributing.md ├── examples.md ├── examples │ ├── README.md │ ├── __init__.py │ ├── basic_moving_average_strategy.py │ ├── bollinger_bands_strategy.py │ ├── example.py │ ├── example_sockets_connection.py │ ├── fibonacci_retracement_eurusd.py │ ├── fimathe │ │ ├── README.md │ │ ├── __init__.py │ │ ├── eurusd_fimathe.py │ │ └── win_fimathe.py │ ├── getting_started.py │ ├── indicator_connector_strategy.py │ ├── market_depth_analysis.py │ ├── rate_converter_example.py │ └── rsi_strategy.py ├── extra.css ├── extra.js ├── index.md └── strategies │ ├── bollinger_bands.md │ ├── fibonacci_retracement.md │ ├── market_depth_analysis.md │ ├── moving_average.md │ └── rsi_strategy.md ├── mkdocs.yaml ├── mqpy ├── __init__.py ├── __main__.py ├── book.py ├── indicator_connector.py ├── logger.py ├── rates.py ├── template.py ├── tick.py ├── trade.py └── utilities.py ├── pyproject.toml ├── scripts ├── __init__.py ├── build_docs.sh └── gen_ref_pages.py ├── setup.py ├── tests ├── conftest.py ├── integration │ └── test_mt5_connection.py ├── test_book.py ├── test_rates.py ├── test_template.py ├── test_tick.py ├── test_trade.py └── test_utilities.py └── uv.lock /.codespell-ignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Joaopeuko/Mql5-Python-Integration/4d488d1bfc2ea567bd99e29b928b1a22561e78dc/.codespell-ignore -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Joaopeuko 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.yaml: -------------------------------------------------------------------------------- 1 | name: "[build] Build Request" 2 | description: Changes related to the build system, dependencies, or configuration updates (e.g., build scripts, Dockerfiles, etc.). 3 | title: "build: " 4 | labels: [build] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: requested_change 16 | attributes: 17 | label: Describe the Requested Change 18 | description: Provide a detailed description of the proposed change. 19 | placeholder: e.g., Bump package X from 1.0.0 to 1.0.1 or update build script Y. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.yaml: -------------------------------------------------------------------------------- 1 | name: "[chore] Chore Request" 2 | description: Routine tasks or administrative updates not affecting code functionality (e.g., updates to documentation, configurations, or non-functional dependencies). 3 | title: "chore: " 4 | labels: [chore] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: task_description 16 | attributes: 17 | label: Describe the Task 18 | description: Specify the task or update that should be performed. 19 | placeholder: e.g., Add secrets to GitHub Secret, update CI config, or clean up old logs. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.yaml: -------------------------------------------------------------------------------- 1 | name: "[ci] CI Request" 2 | description: Changes or updates related to continuous integration (CI) and deployment pipelines. 3 | title: "ci: " 4 | labels: [ci] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | - type: textarea 14 | id: ci_modification 15 | attributes: 16 | label: What CI modification is needed? 17 | description: Provide a clear description of the issue or the part of the CI/CD pipeline that requires modification. 18 | placeholder: e.g., Fix failing build step, update deployment script, or add new test stage. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yaml: -------------------------------------------------------------------------------- 1 | name: "[docs] Documentation Request" 2 | description: Request documentation for functions, scripts, modules, or any other code-related areas that lack clarity. 3 | title: "docs: " 4 | labels: [docs] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: unclear_section 16 | attributes: 17 | label: What is unclear? 18 | description: Specify the part of the code or documentation that is unclear. Include details about what information is missing or confusing. 19 | placeholder: e.g., "The purpose of function X is unclear" or "More details needed about how module Y interacts with Z." 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.yaml: -------------------------------------------------------------------------------- 1 | name: "[feat] Task Request" 2 | description: Request a task or feature to be added to the project. 3 | title: "feat: " 4 | labels: [feat] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: task 16 | attributes: 17 | label: Task Description 18 | description: Provide a clear and concise description of what needs to be done. 19 | placeholder: Describe the problem or proposed solution. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fix.yaml: -------------------------------------------------------------------------------- 1 | name: "[fix] Bug Fix Request" 2 | description: Request a bug fix or patch to resolve an issue in the codebase. 3 | title: "fix: " 4 | labels: [bug, fix] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: steps_to_reproduce 16 | attributes: 17 | label: Steps to Reproduce 18 | description: Provide the steps or code needed to reproduce the bug. Include specific instructions or a code snippet if possible. 19 | placeholder: e.g., "1. Go to page A, 2. Click button B, 3. Observe behavior C." 20 | 21 | - type: textarea 22 | id: proposed_solution 23 | attributes: 24 | label: Proposed Solution 25 | description: Suggest a solution to fix the bug. Include any alternative approaches you've considered. 26 | placeholder: e.g., "Fix the validation in X module" or "Add a check for Y before proceeding." 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/perf.yaml: -------------------------------------------------------------------------------- 1 | name: "[perf] Performance Refactor Request" 2 | description: Request for performance optimizations or refactors to make the code more efficient or faster. 3 | title: "perf: " 4 | labels: [perf] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: area_for_improvement 16 | attributes: 17 | label: Area for Performance Improvement 18 | 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. 19 | placeholder: e.g., "The sorting algorithm in module X is slow when handling large datasets." 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.yaml: -------------------------------------------------------------------------------- 1 | name: "[refactor] Code Refactor Request" 2 | description: Request for code refactoring to enhance code quality, structure, and maintainability. 3 | title: "refactor: " 4 | labels: [refactor] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: area_to_refactor 16 | attributes: 17 | label: What needs to be refactored? 18 | description: Provide a clear and concise description of the parts of the codebase that should be refactored. Explain any issues with the current implementation, such as complexity, readability, or maintainability problems. 19 | placeholder: e.g., "The function X is too complex and difficult to maintain." 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.yaml: -------------------------------------------------------------------------------- 1 | name: "[style] Style Refactor Request" 2 | description: Request for code style and formatting changes without altering functionality. 3 | title: "style: " 4 | labels: [style] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: style_issues 16 | attributes: 17 | label: What style improvements are needed? 18 | description: Provide a clear description of the style or formatting issues in the code that need to be addressed. Focus on readability, consistency, or adherence to coding standards. 19 | placeholder: e.g., "Inconsistent indentation in module X" or "Variable names in function Y don’t follow the naming convention." 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.yaml: -------------------------------------------------------------------------------- 1 | name: "[test] Test Request" 2 | description: Suggest updates or additions to test cases, including creating new tests or modifying existing ones. 3 | title: "test: " 4 | labels: [test] 5 | projects: [] 6 | body: 7 | - type: input 8 | id: root_path 9 | attributes: 10 | label: "Where?" 11 | description: "Provide path from the root of the repository." 12 | placeholder: "mqpy/" 13 | 14 | - type: textarea 15 | id: test_scope 16 | attributes: 17 | label: What needs to be tested? 18 | description: Provide a clear and concise description of the functionality or components that require testing. Specify if new test cases should be added or if existing ones need modification. 19 | placeholder: e.g., "Test the new feature X in module Y" or "Modify the test for function Z to cover edge cases." 20 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | python-requirements: 9 | patterns: 10 | - "*" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/.github/workflows/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | Fixes (issue) 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy-mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy | Deploy MkDocs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ['main'] 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | jobs: 14 | deploy: 15 | runs-on: windows-latest 16 | environment: 17 | name: github-pages 18 | url: ${{ steps.deployment.outputs.page_url }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.11' 27 | 28 | - name: Install the dependencies 29 | run: | 30 | pip install ` 31 | "mkdocs>=1.6.1" ` 32 | "mkdocs-gen-files>=0.5.0" ` 33 | "mkdocs-jupyter>=0.25.1" ` 34 | "mkdocs-literate-nav>=0.6.2" ` 35 | "mkdocs-material>=9.6.12" ` 36 | "mkdocs-section-index>=0.3.10" ` 37 | "mkdocstrings[python]>=0.29.1" 38 | 39 | - name: Build MkDocs 40 | run: mkdocs build --site-dir ./deploy 41 | 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: './deploy' 49 | 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pypi-packages.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy | Publish Pypi Packages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - '**' # All branches for Test PyPI 8 | tags: 9 | - "*" 10 | jobs: 11 | build-and-publish: 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install --upgrade setuptools wheel build twine 27 | 28 | - name: Clean dist directory 29 | run: | 30 | if (Test-Path -Path dist) { Remove-Item dist -Recurse -Force } 31 | 32 | - name: Extract issue number and suffix 33 | id: issue 34 | if: startsWith(github.ref, 'refs/heads/') 35 | run: | 36 | # Look for # in commit message 37 | $match = git log -1 --pretty=%B | Select-String -Pattern '#(\d+)' 38 | if ($match) { 39 | $num = $match.Matches.Groups[1].Value 40 | $suffix = "rc$num" 41 | } else { 42 | # No issue number => development build 43 | $suffix = 'dev0' 44 | } 45 | echo "SUFFIX=$suffix" >> $env:GITHUB_ENV 46 | echo "suffix=$suffix" >> $env:GITHUB_OUTPUT 47 | 48 | - name: Extract version from pyproject.toml 49 | id: version 50 | run: | 51 | $verLine = Get-Content pyproject.toml | Select-String -Pattern 'version = "(.*)"' 52 | $VERSION = $verLine.Matches.Groups[1].Value -replace '^v', '' 53 | echo "VERSION=$VERSION" >> $env:GITHUB_ENV 54 | echo "version=$VERSION" >> $env:GITHUB_OUTPUT 55 | if ("${{ github.ref }}".StartsWith('refs/tags/')) { 56 | $TAG_VERSION = "${{ github.ref }}".Substring(10) -replace '^v', '' 57 | echo "TAG_VERSION=$TAG_VERSION" >> $env:GITHUB_ENV 58 | } 59 | 60 | - name: Create temporary pyproject.toml for test build 61 | if: startsWith(github.ref, 'refs/heads/') 62 | run: | 63 | # Read the current pyproject.toml 64 | $content = Get-Content pyproject.toml -Raw 65 | 66 | # Get the current version 67 | $version = "${{ env.VERSION }}" 68 | $suffix = "${{ env.SUFFIX }}" 69 | 70 | # Update the version with the suffix 71 | $newVersion = "$version.$suffix" 72 | 73 | # Replace the version in the content 74 | $updatedContent = $content -replace 'version = "(.*?)"', "version = `"$newVersion`"" 75 | 76 | # Save to a temporary file 77 | $updatedContent | Out-File -FilePath pyproject.toml.temp -Encoding utf8 78 | 79 | # Show the changes 80 | Write-Host "Original version: $version" 81 | Write-Host "Updated version: $newVersion" 82 | 83 | # Backup original and replace with temp version 84 | Move-Item -Path pyproject.toml -Destination pyproject.toml.bak -Force 85 | Move-Item -Path pyproject.toml.temp -Destination pyproject.toml -Force 86 | 87 | - name: Build package for Test PyPI 88 | if: startsWith(github.ref, 'refs/heads/') 89 | run: | 90 | python -m build 91 | 92 | # After building, restore the original pyproject.toml 93 | Move-Item -Path pyproject.toml.bak -Destination pyproject.toml -Force 94 | 95 | - name: Build package for PyPI 96 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 97 | run: | 98 | python -m build 99 | 100 | - name: Check distributions 101 | run: | 102 | twine check dist/* 103 | 104 | - name: Publish to Test PyPI (branch push) 105 | if: startsWith(github.ref, 'refs/heads/') 106 | env: 107 | TWINE_USERNAME: __token__ 108 | TWINE_PASSWORD: ${{ secrets.TEST_PYPI }} 109 | run: | 110 | Write-Host "Files ready for upload:" 111 | Get-ChildItem dist/* | ForEach-Object { Write-Host " $_" } 112 | 113 | # Upload with verbose output for debugging 114 | twine upload --skip-existing --verbose --repository-url https://test.pypi.org/legacy/ dist/* 115 | 116 | - name: Publish to PyPI (new tag or workflow dispatch) 117 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 118 | env: 119 | TWINE_USERNAME: __token__ 120 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 121 | run: | 122 | Write-Host "Files to upload to PyPI:" 123 | Get-ChildItem dist/* | ForEach-Object { Write-Host " $_" } 124 | twine upload --verbose dist/* 125 | 126 | - name: Create Step Summary 127 | run: | 128 | # Set the display version based on the ref 129 | if ("${{ github.ref }}".StartsWith("refs/tags/")) { 130 | $displayVersion = "${{ env.TAG_VERSION }}" 131 | } else { 132 | $displayVersion = "${{ env.VERSION }}.${{ env.SUFFIX }}" 133 | } 134 | 135 | @" 136 | # MQPy Package 137 | 138 | ## Installation Instructions 139 | 140 | ### Important Warning ⚠️ 141 | **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** 142 | 143 | - Always use a **demo account** with fake money when testing strategies 144 | - MQPy is provided for **educational purposes only** 145 | - Past performance is not indicative of future results 146 | - Never trade with money you cannot afford to lose 147 | - The developers are not responsible for any financial losses 148 | 149 | ### Windows-Only Compatibility 150 | This package is designed to work exclusively on Windows operating systems. 151 | 152 | ### Installation Steps 153 | 154 | $( if ("${{ github.ref }}".StartsWith("refs/tags/")) { 155 | @" 156 | #### Production Release 157 | This is an official release version (${{ env.TAG_VERSION }}) published to PyPI. 158 | 159 | ``` 160 | pip install mqpy==${{ env.TAG_VERSION }} 161 | ``` 162 | "@ 163 | } else { 164 | @" 165 | #### Test/RC Version 166 | This is a release candidate version published to Test PyPI. 167 | 168 | ``` 169 | pip install mqpy==$displayVersion --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ 170 | ``` 171 | "@ 172 | }) 173 | 174 | ### Documentation 175 | For complete documentation, visit our [GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration). 176 | "@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY 177 | -------------------------------------------------------------------------------- /.github/workflows/deploy-semantic-release.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy | Semantic Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dry_run: 7 | description: 'Dry run (no changes will be committed)' 8 | type: boolean 9 | default: false 10 | debug: 11 | description: 'Enable verbose debugging output' 12 | type: boolean 13 | default: false 14 | push: 15 | branches: 16 | - '**' 17 | paths-ignore: 18 | - 'docs/**' 19 | - '*.md' 20 | - '.github/workflows/deploy-pypi-packages.yaml' 21 | 22 | jobs: 23 | release: 24 | runs-on: ubuntu-latest 25 | concurrency: release 26 | permissions: 27 | contents: write 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Set run mode 36 | id: set_mode 37 | shell: bash 38 | run: | 39 | IS_DRY_RUN=$([ "${{ github.event_name }}" = "push" ] || [ "${{ inputs.dry_run }}" = "true" ] && echo "true" || echo "false") 40 | echo "is_dry_run=$IS_DRY_RUN" >> $GITHUB_OUTPUT 41 | echo "Mode: $([ "$IS_DRY_RUN" = "true" ] && echo "Dry run" || echo "Full release")" 42 | 43 | - name: Python Release - Dry Run 44 | id: release_dryrun 45 | if: steps.set_mode.outputs.is_dry_run == 'true' 46 | uses: python-semantic-release/python-semantic-release@v9.20.0 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | push: "false" 50 | commit: "false" 51 | tag: "false" 52 | changelog: "false" 53 | root_options: ${{ inputs.debug && '-vv --noop' || '-v --noop' }} 54 | 55 | - name: Extract Next Version Info 56 | id: extract_next_version 57 | if: steps.set_mode.outputs.is_dry_run == 'true' && steps.release_dryrun.outputs.version == '' 58 | shell: bash 59 | run: | 60 | # When no release is needed, semantic-release doesn't output the next version 61 | # We need to determine it manually from the commit history 62 | 63 | # Check if we have commits that would trigger a version bump 64 | FEAT_COMMITS=$(git log --grep="^feat:" -i --pretty=format:"%h" | wc -l) 65 | FIX_COMMITS=$(git log --grep="^fix:" -i --pretty=format:"%h" | wc -l) 66 | BREAKING_COMMITS=$(git log --grep="BREAKING CHANGE:" -i --pretty=format:"%h" | wc -l) 67 | 68 | # Get current version from pyproject.toml 69 | CURRENT_VERSION=$(grep -m 1 'version = "' pyproject.toml | awk -F'"' '{print $2}' | sed 's/^v//') 70 | echo "Current version: $CURRENT_VERSION" 71 | 72 | # Split current version into components 73 | IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" 74 | 75 | # Determine the next version based on conventional commits 76 | if [ "$BREAKING_COMMITS" -gt 0 ]; then 77 | # Major version bump 78 | NEXT_VERSION="$((MAJOR + 1)).0.0" 79 | elif [ "$FEAT_COMMITS" -gt 0 ]; then 80 | # Minor version bump 81 | NEXT_VERSION="$MAJOR.$((MINOR + 1)).0" 82 | elif [ "$FIX_COMMITS" -gt 0 ]; then 83 | # Patch version bump 84 | NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" 85 | else 86 | # No significant changes, use development version 87 | NEXT_VERSION="${CURRENT_VERSION}.dev0" 88 | fi 89 | 90 | echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT 91 | echo "next_tag=v$NEXT_VERSION" >> $GITHUB_OUTPUT 92 | echo "Determined next version: $NEXT_VERSION" 93 | 94 | - name: Python Release 95 | id: release 96 | if: ${{ github.event_name == 'workflow_dispatch' && !inputs.dry_run }} 97 | uses: python-semantic-release/python-semantic-release@v9.20.0 98 | with: 99 | github_token: ${{ secrets.GITHUB_TOKEN }} 100 | push: "true" 101 | changelog: "true" 102 | root_options: ${{ inputs.debug && '-vv' || '-v' }} 103 | 104 | - name: Create Step Summary 105 | shell: bash 106 | run: | 107 | IS_DRY_RUN="${{ steps.set_mode.outputs.is_dry_run }}" 108 | RELEASE_ID=$([ "$IS_DRY_RUN" = "true" ] && echo "release_dryrun" || echo "release") 109 | WAS_RELEASED=$([ "${{ steps.release_dryrun.outputs.released || steps.release.outputs.released }}" = "true" ] && echo "Yes" || echo "No") 110 | 111 | # First try to get version from release outputs 112 | VERSION="${{ steps.release_dryrun.outputs.version || steps.release.outputs.version }}" 113 | TAG="${{ steps.release_dryrun.outputs.tag || steps.release.outputs.tag }}" 114 | 115 | # If no version from release outputs, try to get from extract_next_version step 116 | if [ "$IS_DRY_RUN" = "true" ] && [ -z "$VERSION" ]; then 117 | VERSION="${{ steps.extract_next_version.outputs.next_version }}" 118 | TAG="${{ steps.extract_next_version.outputs.next_tag }}" 119 | fi 120 | 121 | # Display trigger information 122 | if [ "${{ github.event_name }}" = "push" ]; then 123 | TRIGGER_INFO="Triggered by push to branch: ${{ github.ref_name }}" 124 | else 125 | TRIGGER_INFO="Triggered manually via workflow dispatch" 126 | fi 127 | 128 | # Create warning text for dry run 129 | if [ "$IS_DRY_RUN" = "true" ]; then 130 | DRY_RUN_TEXT="⚠️ This is a dry run - no changes were committed" 131 | TITLE_SUFFIX=" (Dry Run)" 132 | else 133 | DRY_RUN_TEXT="" 134 | TITLE_SUFFIX="" 135 | fi 136 | 137 | cat > $GITHUB_STEP_SUMMARY << EOF 138 | # MQPy Release$TITLE_SUFFIX 139 | 140 | ## Release Summary 141 | 142 | $TRIGGER_INFO 143 | $DRY_RUN_TEXT 144 | 145 | Current/Next Version: $VERSION 146 | Current/Next Tag: $TAG 147 | Release required: $WAS_RELEASED 148 | 149 | ## Installation Instructions 150 | 151 | ### Important Warning ⚠️ 152 | **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** 153 | 154 | - Always use a **demo account** with fake money when testing strategies 155 | - MQPy is provided for **educational purposes only** 156 | - Past performance is not indicative of future results 157 | - Never trade with money you cannot afford to lose 158 | - The developers are not responsible for any financial losses 159 | 160 | ### Windows-Only Compatibility 161 | This package is designed to work exclusively on Windows operating systems. 162 | 163 | ### Installation Steps 164 | 165 | #### $([ "$IS_DRY_RUN" = "true" ] && echo "Test/RC Version" || echo "Production Version") 166 | $([ "$IS_DRY_RUN" = "true" ] && echo "This is a release candidate version published to Test PyPI. 167 | 168 | \`\`\` 169 | pip install mqpy==$VERSION --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ 170 | \`\`\`" || echo "\`\`\` 171 | pip install mqpy==$VERSION 172 | \`\`\`") 173 | 174 | ### Documentation 175 | For complete documentation, visit our [GitHub repository](https://github.com/Joaopeuko/Mql5-Python-Integration). 176 | EOF 177 | -------------------------------------------------------------------------------- /.github/workflows/test-pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Test | Pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ['*'] 7 | 8 | jobs: 9 | pre-commit: 10 | permissions: write-all 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Run Pre-commit 18 | uses: pre-commit/action@v3.0.1 19 | with: 20 | extra_args: --all-files 21 | -------------------------------------------------------------------------------- /.github/workflows/test-pytest-and-integration.yml: -------------------------------------------------------------------------------- 1 | name: Test | Pytest and Integration Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: ['*'] 8 | schedule: 9 | - cron: '0 12 * * 0' # Run every week monday at 12:00 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | timeout-minutes: 15 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.11' 23 | 24 | - name: Download MetaTrader5 Installer 25 | shell: pwsh 26 | run: | 27 | $url = "https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5setup.exe" 28 | $output = "$env:GITHUB_WORKSPACE\mt5setup.exe" 29 | Invoke-WebRequest -Uri $url -OutFile $output 30 | Write-Host "Download completed. File size: $((Get-Item $output).Length) bytes" 31 | 32 | - name: Install MetaTrader5 33 | run: | 34 | $process = Start-Process -FilePath ".\mt5setup.exe" -ArgumentList "/auto", "/portable" -PassThru 35 | $process.WaitForExit(300000) 36 | if (-not $process.HasExited) { 37 | Write-Host "MT5 installer stuck, killing..." 38 | Stop-Process -Id $process.Id -Force 39 | exit 1 40 | } 41 | shell: pwsh 42 | 43 | - name: Launch MT5 44 | shell: pwsh 45 | run: | 46 | $mt5Path = Resolve-Path "C:\Program Files\MetaTrader 5\terminal64.exe" 47 | 48 | # Launch with diagnostics 49 | Start-Process $mt5Path -ArgumentList @( 50 | "/portable", 51 | "/headless", 52 | "/config:config", 53 | "/noreport" 54 | ) -NoNewWindow 55 | 56 | # Verify process start 57 | $attempts = 0 58 | while ($attempts -lt 10) { 59 | if (Get-Process terminal64 -ErrorAction SilentlyContinue) { 60 | Write-Host "MT5 process detected" 61 | break 62 | } 63 | $attempts++ 64 | Start-Sleep 5 65 | } 66 | 67 | if (-not (Get-Process terminal64 -ErrorAction SilentlyContinue)) { 68 | Get-Content ".\MetaTrader 5\logs\*.log" | Write-Host 69 | throw "MT5 failed to start" 70 | } 71 | 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install --upgrade pip 75 | pip install pytest pytest-cov MetaTrader5 76 | pip install -e . 77 | 78 | - name: Run tests with coverage 79 | env: 80 | HEADLESS_MODE: true 81 | MT5_LOGIN: ${{ secrets.MT5_LOGIN }} 82 | MT5_PASSWORD: ${{ secrets.MT5_PASSWORD }} 83 | MT5_SERVER: "MetaQuotes-Demo" 84 | MT5_PATH: "C:\\Program Files\\MetaTrader 5\\terminal64.exe" 85 | run: | 86 | pytest -k . --cov=mqpy --cov-append --junitxml=pytest.xml -x | tee pytest-coverage.txt 87 | 88 | - name: Generate coverage summary 89 | shell: pwsh 90 | run: | 91 | "## Test Coverage Summary" | Out-File -FilePath summary.md -Encoding utf8 92 | '```' | Out-File -FilePath summary.md -Append -Encoding utf8 93 | Get-Content pytest-coverage.txt | Out-File -FilePath summary.md -Append -Encoding utf8 94 | '```' | Out-File -FilePath summary.md -Append -Encoding utf8 95 | Get-Content summary.md | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.pyc 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-ast 9 | - id: check-builtin-literals 10 | - id: check-byte-order-marker 11 | - id: check-case-conflict 12 | - id: check-docstring-first 13 | - id: check-executables-have-shebangs 14 | - id: check-json 15 | - id: check-merge-conflict 16 | - id: check-shebang-scripts-are-executable 17 | - id: check-symlinks 18 | - id: check-yaml 19 | - id: debug-statements 20 | - id: destroyed-symlinks 21 | - id: end-of-file-fixer 22 | - id: file-contents-sorter 23 | - id: trailing-whitespace 24 | 25 | - repo: https://github.com/astral-sh/ruff-pre-commit 26 | rev: v0.8.6 27 | hooks: 28 | - id: ruff 29 | args: [--fix, --exit-non-zero-on-fix] 30 | - id: ruff-format 31 | 32 | - repo: https://github.com/codespell-project/codespell 33 | rev: v2.3.0 34 | hooks: 35 | - id: codespell 36 | language: python 37 | types: [text] 38 | entry: codespell --ignore-words=.codespell-ignore --check-filenames 39 | exclude: uv.lock 40 | 41 | - repo: https://github.com/pre-commit/mirrors-mypy 42 | rev: v1.14.1 43 | hooks: 44 | - id: mypy 45 | name: mypy 46 | pass_filenames: false 47 | args: 48 | [ 49 | --strict-equality, 50 | --disallow-untyped-calls, 51 | --disallow-untyped-defs, 52 | --disallow-incomplete-defs, 53 | --disallow-any-generics, 54 | --check-untyped-defs, 55 | --disallow-untyped-decorators, 56 | --warn-redundant-casts, 57 | --warn-unused-ignores, 58 | --no-warn-no-return, 59 | --warn-unreachable, 60 | ] 61 | additional_dependencies: ["types-requests", "types-PyYAML"] 62 | 63 | - repo: local 64 | hooks: 65 | - id: pylint 66 | name: pylint 67 | entry: pylint 68 | language: python 69 | additional_dependencies: ["pylint"] 70 | types: [python] 71 | args: ["--disable=all", "--enable=missing-docstring,unused-argument"] 72 | exclude: 'test_\.py$' 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 Joao Euko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mql5-Python-Integration (MQPy) 2 | 3 |

4 | MQPy Logo 5 |

6 | 7 |

8 | PyPI - Downloads 9 | PyPI 10 | PyPI - Wheel 11 | PyPI - License 12 |

13 | 14 | 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. 15 | 16 | ## ⚠️ Important Notice 17 | 18 | Trading involves risk. Always: 19 | - Use demo accounts for testing 20 | - Never trade with money you cannot afford to lose 21 | - Understand that past performance doesn't guarantee future results 22 | 23 | ## Installation 24 | 25 | ```bash 26 | pip install mqpy 27 | ``` 28 | 29 | ## Requirements 30 | 31 | - Windows OS 32 | - Python 3.8 or later 33 | - MetaTrader 5 installed 34 | 35 | ## Quick Start 36 | 37 | Generate a trading strategy template: 38 | 39 | ```bash 40 | mqpy --symbol EURUSD --file_name my_strategy 41 | ``` 42 | 43 | ## Features 44 | 45 | - Simple integration with MetaTrader 5 46 | - Easy-to-use command line interface 47 | - Basic template generation for trading strategies 48 | - Support for various trading strategies: 49 | - Moving Average Crossover 50 | - RSI-based trading 51 | - Bollinger Bands strategies 52 | - Fibonacci Retracement patterns 53 | - Multi-timeframe analysis 54 | - Custom indicator integration 55 | 56 | ## Documentation 57 | 58 | For detailed documentation, examples, and strategy explanations, visit: 59 | [https://joaopeuko.com/Mql5-Python-Integration/](https://joaopeuko.com/Mql5-Python-Integration/) 60 | 61 | ## Support 62 | 63 | MQPy is a free and open-source project. If you'd like to support its development, consider becoming a sponsor. 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License - see the LICENSE file for details. 68 | -------------------------------------------------------------------------------- /docs/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/assets/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to MQPy 2 | 3 | Thank you for your interest in contributing to MQPy! There are many ways to contribute, and we appreciate all of them. 4 | 5 | ## Development Process 6 | 7 | 1. Fork the repository 8 | 2. Create a feature branch 9 | 3. Make your changes 10 | 4. Submit a pull request 11 | 12 | ## Reporting Issues 13 | 14 | If you've found a bug or have a feature request, please file an issue using one of our issue templates: 15 | 16 | ### Issue Templates 17 | 18 | Choose the appropriate template for your issue: 19 | 20 | - **Bug Fix**: [Report a bug 🐛](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=fix.yaml&title=fix%3A+) 21 | - **Documentation**: [Request documentation improvements 📚](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=docs.yaml&title=docs%3A+) 22 | - **Feature Request**: [Suggest a new feature ✨](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=feat.yaml&title=feat%3A+) 23 | - **Performance Improvement**: [Report performance issues ⚡](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=perf.yaml&title=perf%3A+) 24 | - **Test**: [Suggest test improvements 🧪](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=test.yaml&title=test%3A+) 25 | - **Build**: [Report build issues 🔨](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=build.yaml&title=build%3A+) 26 | - **Chore**: [Suggest maintenance tasks 🧹](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=chore.yaml&title=chore%3A+) 27 | - **Style**: [Report style issues 💅](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=style.yaml&title=style%3A+) 28 | - **Refactor**: [Suggest code refactoring 🔄](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=refactor.yaml&title=refactor%3A+) 29 | - **CI**: [Suggest CI improvements ⚙️](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=ci.yaml&title=ci%3A+) 30 | 31 | ## Coding Standards 32 | 33 | - Follow PEP 8 style guide 34 | - Write docstrings in Google format 35 | - Include appropriate tests 36 | 37 | ## Pull Request Process 38 | 39 | 1. Ensure your code meets all tests 40 | 2. Update documentation where necessary 41 | 3. The PR should work for Python 3.6 and above 42 | 4. PRs will be merged once reviewed by maintainers 43 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # MQPy Examples 2 | 3 | !!! danger "Trading Risk Warning" 4 | **IMPORTANT: All examples should be tested using demo accounts only!** 5 | 6 | - Trading involves substantial risk of loss 7 | - These examples are for educational purposes only 8 | - Always test with fake money before using real funds 9 | - Past performance is not indicative of future results 10 | - The developers are not responsible for any financial losses 11 | 12 | MQPy provides a variety of example trading strategies to help you understand how to implement your own algorithmic trading solutions using MetaTrader 5. 13 | 14 | ## Getting Started 15 | 16 | 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: 17 | 18 | - Initializing the trading environment 19 | - Fetching market data 20 | - Making trading decisions 21 | - Executing trades 22 | 23 | ## Basic Strategies 24 | 25 | ### Moving Average Crossover 26 | 27 | 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: 28 | 29 | | Feature | Description | 30 | |---------|-------------| 31 | | Signal Generation | Uses crossovers between short and long moving averages | 32 | | Implementation | Includes proper crossover detection logic | 33 | | Error Handling | Comprehensive logging and exception handling | 34 | 35 | [Read detailed explanation of the Moving Average strategy →](strategies/moving_average.md) 36 | 37 | ```python 38 | def calculate_sma(prices, period): 39 | """Calculate Simple Moving Average.""" 40 | if len(prices) < period: 41 | return None 42 | return sum(prices[-period:]) / period 43 | ``` 44 | 45 | ## Technical Indicator Strategies 46 | 47 | ### RSI Strategy 48 | 49 | The [RSI Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rsi_strategy.py) example demonstrates: 50 | 51 | | Feature | Description | 52 | |---------|-------------| 53 | | Indicator | Implementation of the Relative Strength Index (RSI) | 54 | | Trading Approach | Entry/exit based on overbought and oversold conditions | 55 | | Technical Analysis | Practical example of calculating and using indicators | 56 | 57 | [Read detailed explanation of the RSI strategy →](strategies/rsi_strategy.md) 58 | 59 | ### Bollinger Bands Strategy 60 | 61 | The [Bollinger Bands Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/bollinger_bands_strategy.py) shows: 62 | 63 | | Feature | Description | 64 | |---------|-------------| 65 | | Trading Approach | Using Bollinger Bands for trading range breakouts | 66 | | Strategy Type | Mean reversion trading principles | 67 | | Signal Generation | Volatility-based entry and exit logic | 68 | 69 | [Read detailed explanation of the Bollinger Bands strategy →](strategies/bollinger_bands.md) 70 | 71 | ## Advanced Strategies 72 | 73 | ### Fibonacci Retracement Strategy 74 | 75 | The [Fibonacci Retracement Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/fibonacci_retracement_eurusd.py) for EURUSD: 76 | 77 | | Feature | Description | 78 | |---------|-------------| 79 | | Strategy Type | Implements the FiMathe strategy | 80 | | Pattern Recognition | Uses Fibonacci retracement levels for entries and exits | 81 | | Risk Management | Includes dynamic stop-loss adjustment based on price action | 82 | 83 | [Read detailed explanation of the Fibonacci Retracement strategy →](strategies/fibonacci_retracement.md) 84 | 85 | ### Market Depth Analysis 86 | 87 | 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: 88 | 89 | | Feature | Description | 90 | |---------|-------------| 91 | | Order Book Analysis | Examines buy/sell order distribution and concentration | 92 | | Support/Resistance | Identifies potential support and resistance levels from actual orders | 93 | | Visualization | Creates horizontal bar charts showing bid/ask distribution with key levels | 94 | 95 | [Read detailed explanation of the Market Depth Analysis →](strategies/market_depth_analysis.md) 96 | 97 | ### Multi-Timeframe Analysis 98 | 99 | The [Rate Converter Example](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rate_converter_example.py) demonstrates: 100 | 101 | | Feature | Description | 102 | |---------|-------------| 103 | | Timeframe Conversion | How to convert between different timeframes using the RateConverter | 104 | | Multi-timeframe Analysis | Calculating moving averages across different timeframes | 105 | | Visualization | Creating charts for price data across 1-minute, 5-minute, and 1-hour timeframes | 106 | 107 | ### Indicator Connector Strategy 108 | 109 | The [Indicator Connector Strategy](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/indicator_connector_strategy.py) shows: 110 | 111 | | Feature | Description | 112 | |---------|-------------| 113 | | Connectivity | How to connect to MetaTrader 5's custom indicators | 114 | | Signal Combination | Combining multiple indicator signals (Stochastic and Moving Average) | 115 | | Advanced Techniques | Advanced signal generation and filtering approaches | 116 | 117 | ## Running the Examples 118 | 119 | To run any of these examples: 120 | 121 | 1. Ensure you have MQPy installed: 122 | ```bash 123 | pip install mqpy 124 | ``` 125 | 126 | 2. Make sure MetaTrader 5 is installed and running on your system 127 | 128 | 3. Run any example with Python: 129 | ```bash 130 | python getting_started.py 131 | ``` 132 | 133 | ## Contributing Your Own Examples 134 | 135 | If you've developed an interesting strategy using MQPy, consider contributing it to this examples collection by submitting a pull request! 136 | 137 | ## Disclaimer 138 | 139 | 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. 140 | 141 | ## All Example Files 142 | 143 | You can access these examples in several ways: 144 | 145 | 1. **Clone the entire repository**: 146 | ```bash 147 | git clone https://github.com/Joaopeuko/Mql5-Python-Integration.git 148 | cd Mql5-Python-Integration/docs/examples 149 | ``` 150 | 151 | 2. **Download individual files** by clicking on the links in the table below. 152 | 153 | 3. **Copy the code** from the strategy explanations page for the strategies with detailed documentation. 154 | 155 | Here are direct links to all the example files in the MQPy repository: 156 | 157 | | Strategy | Description | Source Code | 158 | |----------|-------------|-------------| 159 | | Getting Started | Basic introduction to MQPy | [getting_started.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/getting_started.py) | 160 | | 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) | 161 | | RSI Strategy | Momentum-based overbought/oversold strategy | [rsi_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/rsi_strategy.py) | 162 | | 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) | 163 | | 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) | 164 | | 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) | 165 | | 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) | 166 | | Indicator Connector | Custom indicator integration | [indicator_connector_strategy.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/indicator_connector_strategy.py) | 167 | | Sockets Connection | Advanced MetaTrader connectivity | [example_sockets_connection.py](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/main/docs/examples/example_sockets_connection.py) | 168 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # MQPy Trading Strategy Examples 2 | 3 | This directory contains various example trading strategies implemented using the MQPy framework for MetaTrader 5 integration. 4 | 5 | ## Getting Started 6 | 7 | If you're new to MQPy, start with the `getting_started.py` example which demonstrates basic concepts: 8 | 9 | - Initializing the trading environment 10 | - Fetching market data 11 | - Making trading decisions 12 | - Executing trades 13 | 14 | ## Available Examples 15 | 16 | ### Basic Strategies 17 | 18 | 1. **Getting Started** (`getting_started.py`) 19 | - A simple introduction to the MQPy framework 20 | - Demonstrates basic data retrieval and trading operations 21 | - Perfect for beginners 22 | 23 | 2. **Moving Average Crossover** (`basic_moving_average_strategy.py`) 24 | - Uses crossovers between short and long moving averages 25 | - Implements proper crossover detection logic 26 | - Includes logging and exception handling 27 | 28 | ### Technical Indicator Strategies 29 | 30 | 3. **RSI Strategy** (`rsi_strategy.py`) 31 | - Implements the Relative Strength Index (RSI) indicator 32 | - Trades based on overbought and oversold conditions 33 | - Shows how to calculate and use technical indicators 34 | 35 | 4. **Bollinger Bands Strategy** (`bollinger_bands_strategy.py`) 36 | - Uses Bollinger Bands for trading range breakouts 37 | - Demonstrates mean reversion trading principles 38 | - Includes volatility-based entry and exit logic 39 | 40 | ### Advanced Strategies 41 | 42 | 5. **Fibonacci Retracement Strategy** (`fibonacci_retracement_eurusd.py`) 43 | - Implements the FiMathe strategy for EURUSD 44 | - Uses Fibonacci retracement levels for entries and exits 45 | - Includes dynamic stop-loss adjustment based on price action 46 | 47 | 6. **Multi-Timeframe Analysis** (`rate_converter_example.py`) 48 | - Demonstrates how to convert between different timeframes using the RateConverter 49 | - Implements multi-timeframe analysis by calculating moving averages across timeframes 50 | - Visualizes price data and indicators across 1-minute, 5-minute, and 1-hour charts 51 | 52 | ## Fibonacci Retracement Strategy 53 | 54 | The Fibonacci Retracement strategy (`fibonacci_retracement_eurusd.py`) demonstrates how to implement a trading system based on Fibonacci retracement levels. This strategy: 55 | 56 | 1. **Identifies swing points**: The algorithm detects significant market swing highs and lows within a specified window. 57 | 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. 58 | 3. **Generates trading signals**: The strategy produces buy signals when price bounces off key retracement levels during uptrends and sell signals during downtrends. 59 | 4. **Visualizes analysis**: Creates charts showing price action with identified swing points and Fibonacci levels to aid in trading decisions. 60 | 61 | 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. 62 | 63 | ## Market Depth Analysis 64 | 65 | 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: 66 | 67 | 1. **Real-time market depth monitoring**: Captures and analyzes order book snapshots at regular intervals. 68 | 2. **Buy/sell pressure analysis**: Calculates metrics such as buy/sell volume ratio, percentage distribution, and order concentration. 69 | 3. **Support/resistance identification**: Detects potential support and resistance levels based on unusual volume concentration at specific price points. 70 | 4. **Visual representation**: Creates horizontal bar charts showing the distribution of buy (bid) and sell (ask) orders, with highlighted support/resistance zones. 71 | 72 | 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. 73 | 74 | ## Detailed Strategy Documentation 75 | 76 | For an in-depth explanation of advanced strategies including theoretical background, implementation details, and potential customizations, see the [detailed strategy documentation](STRATEGY_DOCUMENTATION.md). 77 | 78 | ## Running the Examples 79 | 80 | 1. Make sure you have MQPy installed: 81 | ```bash 82 | pip install mqpy 83 | ``` 84 | 85 | 2. Ensure MetaTrader 5 is installed and running on your system 86 | 87 | 3. Run any example with Python: 88 | ```bash 89 | python getting_started.py 90 | ``` 91 | 92 | ## Strategy Development Best Practices 93 | 94 | When developing your own strategies with MQPy, consider the following best practices: 95 | 96 | 1. **Error Handling**: Implement proper exception handling to catch network issues, data problems, or unexpected errors 97 | 98 | 2. **Logging**: Use Python's logging module to record important events and debug information 99 | 100 | 3. **Testing**: Test your strategy on historical data before deploying with real money 101 | 102 | 4. **Risk Management**: Always implement proper stop-loss and take-profit levels 103 | 104 | 5. **Architecture**: Separate your trading logic, indicators, and execution code for better maintainability 105 | 106 | ## Contributing 107 | 108 | If you've developed an interesting strategy using MQPy, consider contributing it to this examples collection by submitting a pull request. 109 | 110 | ## Disclaimer 111 | 112 | 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. 113 | -------------------------------------------------------------------------------- /docs/examples/__init__.py: -------------------------------------------------------------------------------- 1 | """Example package for MQPy demonstration code.""" 2 | -------------------------------------------------------------------------------- /docs/examples/basic_moving_average_strategy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Basic Moving Average Crossover Strategy Example. 3 | 4 | This example demonstrates a simple moving average crossover strategy using the mqpy framework. 5 | When a shorter-period moving average crosses above a longer-period moving average, 6 | the strategy generates a buy signal. Conversely, when the shorter-period moving average 7 | crosses below the longer-period moving average, the strategy generates a sell signal. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import logging 13 | 14 | from mqpy.rates import Rates 15 | from mqpy.tick import Tick 16 | from mqpy.trade import Trade 17 | 18 | # Configure logging 19 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def calculate_sma(prices: list[float], period: int) -> float | None: 24 | """Calculate Simple Moving Average. 25 | 26 | Args: 27 | prices: A list of price values 28 | period: The period for the moving average calculation 29 | 30 | Returns: 31 | The simple moving average value or None if insufficient data 32 | """ 33 | if len(prices) < period: 34 | return None 35 | return sum(prices[-period:]) / period 36 | 37 | 38 | def main() -> None: 39 | """Main execution function for the Moving Average Crossover strategy.""" 40 | # Initialize the trading strategy 41 | trade = Trade( 42 | expert_name="Moving Average Crossover", 43 | version="1.0", 44 | symbol="EURUSD", 45 | magic_number=567, 46 | lot=0.1, 47 | stop_loss=25, 48 | emergency_stop_loss=300, 49 | take_profit=25, 50 | emergency_take_profit=300, 51 | start_time="9:15", 52 | finishing_time="17:30", 53 | ending_time="17:50", 54 | fee=0.5, 55 | ) 56 | 57 | logger.info(f"Starting Moving Average Crossover strategy on {trade.symbol}") 58 | 59 | # Strategy parameters 60 | prev_tick_time = 0 61 | short_period = 5 62 | long_period = 20 63 | 64 | # Variables to track previous state for crossover detection 65 | prev_short_ma = None 66 | prev_long_ma = None 67 | 68 | try: 69 | while True: 70 | # Prepare the symbol for trading 71 | trade.prepare_symbol() 72 | 73 | # Fetch tick and rates data 74 | current_tick = Tick(trade.symbol) 75 | historical_rates = Rates(trade.symbol, long_period + 10, 0, 1) # Get extra data for reliability 76 | 77 | # Only process if we have a new tick and enough historical data 78 | has_new_tick = current_tick.time_msc != prev_tick_time 79 | has_enough_data = len(historical_rates.close) >= long_period 80 | 81 | if has_new_tick and has_enough_data: 82 | # Calculate moving averages 83 | short_ma = calculate_sma(historical_rates.close, short_period) 84 | long_ma = calculate_sma(historical_rates.close, long_period) 85 | 86 | # Check if we have enough data for comparison 87 | has_short_ma = short_ma is not None 88 | has_long_ma = long_ma is not None 89 | has_prev_short_ma = prev_short_ma is not None 90 | has_prev_long_ma = prev_long_ma is not None 91 | has_valid_ma_values = has_short_ma and has_long_ma and has_prev_short_ma and has_prev_long_ma 92 | 93 | if has_valid_ma_values: 94 | # Check short MA and long MA relationship for current and previous values 95 | is_above_now = short_ma > long_ma 96 | is_above_prev = prev_short_ma > prev_long_ma 97 | 98 | # Detect crossover (short MA crosses above long MA) 99 | cross_above = is_above_now and not is_above_prev 100 | 101 | # Detect crossunder (short MA crosses below long MA) 102 | cross_below = not is_above_now and is_above_prev 103 | 104 | # Log crossover events 105 | if cross_above: 106 | logger.info( 107 | f"Bullish crossover detected: Short MA ({short_ma:.5f}) " 108 | f"crossed above Long MA ({long_ma:.5f})" 109 | ) 110 | elif cross_below: 111 | logger.info( 112 | f"Bearish crossover detected: Short MA ({short_ma:.5f}) " 113 | f"crossed below Long MA ({long_ma:.5f})" 114 | ) 115 | 116 | # Execute trading positions based on signals 117 | if trade.trading_time(): # Only trade during allowed hours 118 | trade.open_position( 119 | should_buy=cross_above, should_sell=cross_below, comment="Moving Average Crossover Strategy" 120 | ) 121 | 122 | # Update previous MA values for next comparison 123 | prev_short_ma = short_ma 124 | prev_long_ma = long_ma 125 | 126 | # Update trading statistics periodically 127 | trade.statistics() 128 | 129 | prev_tick_time = current_tick.time_msc 130 | 131 | # Check if it's the end of the trading day 132 | if trade.days_end(): 133 | trade.close_position("End of the trading day reached.") 134 | break 135 | 136 | except KeyboardInterrupt: 137 | logger.info("Strategy execution interrupted by user.") 138 | trade.close_position("User interrupted the strategy.") 139 | except Exception: 140 | logger.exception("Error in strategy execution") 141 | finally: 142 | logger.info("Finishing the program.") 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /docs/examples/bollinger_bands_strategy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Bollinger Bands Strategy Example. 3 | 4 | This example demonstrates a Bollinger Bands trading strategy using the mqpy framework. 5 | The strategy enters long positions when price breaks below the lower band and enters 6 | short positions when price breaks above the upper band. The strategy is designed to 7 | trade price reversals from extreme movements. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import logging 13 | 14 | import numpy as np 15 | 16 | from mqpy.rates import Rates 17 | from mqpy.tick import Tick 18 | from mqpy.trade import Trade 19 | 20 | # Configure logging 21 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def calculate_bollinger_bands( 26 | prices: list[float], period: int = 20, num_std_dev: float = 2.0 27 | ) -> tuple[float, float, float] | None: 28 | """Calculate Bollinger Bands (middle, upper, lower). 29 | 30 | Args: 31 | prices: A list of closing prices 32 | period: The period for SMA calculation, default is 20 33 | num_std_dev: Number of standard deviations for bands, default is 2.0 34 | 35 | Returns: 36 | A tuple of (middle_band, upper_band, lower_band) or None if not enough data 37 | """ 38 | if len(prices) < period: 39 | return None 40 | 41 | # Convert to numpy array for vectorized calculations 42 | price_array = np.array(prices[-period:]) 43 | 44 | # Calculate SMA (middle band) 45 | sma = np.mean(price_array) 46 | 47 | # Calculate standard deviation 48 | std_dev = np.std(price_array) 49 | 50 | # Calculate upper and lower bands 51 | upper_band = sma + (num_std_dev * std_dev) 52 | lower_band = sma - (num_std_dev * std_dev) 53 | 54 | return (sma, upper_band, lower_band) 55 | 56 | 57 | def main() -> None: 58 | """Main execution function for the Bollinger Bands strategy.""" 59 | # Initialize the trading strategy 60 | trade = Trade( 61 | expert_name="Bollinger Bands Strategy", 62 | version="1.0", 63 | symbol="EURUSD", 64 | magic_number=569, 65 | lot=0.1, 66 | stop_loss=50, 67 | emergency_stop_loss=150, 68 | take_profit=100, 69 | emergency_take_profit=300, 70 | start_time="9:15", 71 | finishing_time="17:30", 72 | ending_time="17:50", 73 | fee=0.5, 74 | ) 75 | 76 | logger.info(f"Starting Bollinger Bands strategy on {trade.symbol}") 77 | 78 | # Strategy parameters 79 | prev_tick_time = 0 80 | bb_period = 20 81 | bb_std_dev = 2.0 82 | 83 | try: 84 | while True: 85 | # Prepare the symbol for trading 86 | trade.prepare_symbol() 87 | 88 | # Fetch tick and rates data 89 | current_tick = Tick(trade.symbol) 90 | historical_rates = Rates(trade.symbol, bb_period + 10, 0, 1) # Get extra data for reliability 91 | 92 | # Only process if we have a new tick 93 | if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= bb_period: 94 | # Calculate Bollinger Bands 95 | bb_result = calculate_bollinger_bands(historical_rates.close, period=bb_period, num_std_dev=bb_std_dev) 96 | 97 | if bb_result: 98 | middle_band, upper_band, lower_band = bb_result 99 | current_price = current_tick.last 100 | 101 | # Generate signals based on price position relative to bands 102 | # Buy when price crosses below lower band (potential bounce) 103 | is_buy_signal = current_price < lower_band 104 | 105 | # Sell when price crosses above upper band (potential reversal) 106 | is_sell_signal = current_price > upper_band 107 | 108 | # Log band data and signals 109 | logger.info(f"Current price: {current_price:.5f}") 110 | logger.info( 111 | f"Bollinger Bands - Middle: {middle_band:.5f}, Upper: {upper_band:.5f}, Lower: {lower_band:.5f}" 112 | ) 113 | 114 | if is_buy_signal: 115 | logger.info(f"Buy signal: Price ({current_price:.5f}) below lower band ({lower_band:.5f})") 116 | elif is_sell_signal: 117 | logger.info(f"Sell signal: Price ({current_price:.5f}) above upper band ({upper_band:.5f})") 118 | 119 | # Execute trading positions based on signals 120 | if trade.trading_time(): # Only trade during allowed hours 121 | trade.open_position( 122 | should_buy=is_buy_signal, should_sell=is_sell_signal, comment="Bollinger Bands Strategy" 123 | ) 124 | 125 | # Update trading statistics 126 | trade.statistics() 127 | 128 | prev_tick_time = current_tick.time_msc 129 | 130 | # Check if it's the end of the trading day 131 | if trade.days_end(): 132 | trade.close_position("End of the trading day reached.") 133 | break 134 | 135 | except KeyboardInterrupt: 136 | logger.info("Strategy execution interrupted by user.") 137 | trade.close_position("User interrupted the strategy.") 138 | except Exception: 139 | logger.exception("Error in strategy execution") 140 | finally: 141 | logger.info("Finishing the program.") 142 | 143 | 144 | if __name__ == "__main__": 145 | main() 146 | -------------------------------------------------------------------------------- /docs/examples/example.py: -------------------------------------------------------------------------------- 1 | """Example trading strategy using the MQL5-Python integration. 2 | 3 | This example demonstrates a Moving Average Crossover strategy. 4 | """ 5 | 6 | from mqpy.rates import Rates 7 | from mqpy.tick import Tick 8 | from mqpy.trade import Trade 9 | 10 | # Initialize the trading strategy 11 | trade = Trade( 12 | expert_name="Moving Average Crossover", 13 | version="1.0", 14 | symbol="EURUSD", 15 | magic_number=567, 16 | lot=0.1, 17 | stop_loss=25, 18 | emergency_stop_loss=300, 19 | take_profit=25, 20 | emergency_take_profit=300, 21 | start_time="9:15", 22 | finishing_time="17:30", 23 | ending_time="17:50", 24 | fee=0.5, 25 | ) 26 | 27 | # Main trading loop 28 | prev_tick_time = 0 29 | short_window_size = 5 30 | long_window_size = 20 # Adjust the window size as needed 31 | 32 | while True: 33 | # Fetch tick and rates data 34 | current_tick = Tick(trade.symbol) 35 | historical_rates = Rates(trade.symbol, long_window_size, 0, 1) 36 | 37 | # Check for new tick 38 | if current_tick.time_msc != prev_tick_time: 39 | # Calculate moving averages 40 | short_ma = sum(historical_rates.close[-short_window_size:]) / short_window_size 41 | long_ma = sum(historical_rates.close[-long_window_size:]) / long_window_size 42 | 43 | # Generate signals based on moving average crossover 44 | is_cross_above = short_ma > long_ma and current_tick.last > short_ma 45 | is_cross_below = short_ma < long_ma and current_tick.last < short_ma 46 | 47 | # Execute trading positions based on signals 48 | trade.open_position( 49 | should_buy=is_cross_above, should_sell=is_cross_below, comment="Moving Average Crossover Strategy" 50 | ) 51 | 52 | prev_tick_time = current_tick.time_msc 53 | 54 | # Check if it's the end of the trading day 55 | if trade.days_end(): 56 | trade.close_position("End of the trading day reached.") 57 | break 58 | -------------------------------------------------------------------------------- /docs/examples/example_sockets_connection.py: -------------------------------------------------------------------------------- 1 | """Example Expert Advisor demonstrating socket connections with MetaTrader 5. 2 | 3 | This example uses stochastic oscillator and moving average indicators to generate trading signals. 4 | """ 5 | 6 | import logging 7 | 8 | import MetaTrader5 as Mt5 9 | from include.indicator_connector import Indicator 10 | from include.rates import Rates 11 | from include.tick import Tick 12 | from include.trade import Trade 13 | 14 | # Configure logging 15 | logger = logging.getLogger(__name__) 16 | logger.setLevel(logging.INFO) 17 | 18 | # You need this MQL5 service to use indicator: 19 | # https://www.mql5.com/en/market/product/57574 20 | indicator = Indicator() 21 | 22 | trade = Trade( 23 | "Example", # Expert name 24 | 0.1, # Expert Version 25 | "PETR4", # symbol 26 | 567, # Magic number 27 | 100.0, # lot 28 | 10, # stop loss - 10 cents 29 | 30, # emergency stop loss - 30 cents 30 | 10, # take profit - 10 cents 31 | 30, # emergency take profit - 30 cents 32 | "9:15", # It is allowed to trade after that hour. Do not use zeros, like: 09 33 | "17:30", # It is not allowed to trade after that hour but let open all the position already opened. 34 | "17:50", # It closes all the position opened. Do not use zeros, like: 09 35 | 0.5, # average fee 36 | ) 37 | 38 | time = 0 39 | while True: 40 | # You need this MQL5 service to use indicator: 41 | # https://www.mql5.com/en/market/product/57574 42 | 43 | # Example of calling the same indicator with different parameters. 44 | stochastic_now = indicator.stochastic(symbol=trade.symbol, time_frame=Mt5.TIMEFRAME_M1) 45 | stochastic_past3 = indicator.stochastic(symbol=trade.symbol, time_frame=Mt5.TIMEFRAME_M1, start_position=3) 46 | 47 | moving_average = indicator.moving_average(symbol=trade.symbol, period=50) 48 | 49 | tick = Tick(trade.symbol) 50 | rates = Rates(trade.symbol, 1, 0, 1) 51 | 52 | # It uses "try" and catch because sometimes it returns None. 53 | try: 54 | # When in doubt how to handle the indicator, it returns a Dictionary. 55 | k_now = stochastic_now["k_result"] 56 | d_now = stochastic_now["d_result"] 57 | 58 | k_past3 = stochastic_now["k_result"] 59 | d_past3 = stochastic_now["d_result"] 60 | 61 | if tick.time_msc != time: 62 | # It is trading of the time frame of one minute. 63 | # 64 | # Stochastic logic: 65 | # To do the buy it checks if the K value at present is higher than the D value and 66 | # if the K at 3 candles before now was lower than the D value. 67 | # For the selling logic, it is the opposite of the buy logic. 68 | # 69 | # Moving Average Logic: 70 | # If the last price is higher than the Moving Average it allows to open a buy position. 71 | # If the last price is lower than the Moving Average it allows to open a sell position. 72 | # 73 | # To open a position this expert combines the Stochastic logic and Moving Average. 74 | # When Stochastic logic and Moving Average logic are true, it open position to the determined direction. 75 | 76 | # It is the buy logic. 77 | buy = ( 78 | # Stochastic 79 | (k_now > d_now and k_past3 < d_past3) 80 | and 81 | # Moving Average 82 | (tick.last > moving_average["moving_average_result"]) 83 | ) # End of buy logic. 84 | 85 | # -------------------------------------------------------------------- # 86 | 87 | # It is the sell logic. 88 | sell = ( 89 | # Stochastic 90 | (k_now < d_now and k_past3 > d_past3) 91 | and 92 | # Moving Average 93 | (tick.last < moving_average["moving_average_result"]) 94 | ) # End of sell logic. 95 | 96 | # -------------------------------------------------------------------- # 97 | 98 | # When buy or sell are true, it open a position. 99 | trade.open_position(buy, sell, "Example Advisor Comment, the comment here can be seen in MetaTrader5") 100 | 101 | except TypeError: 102 | pass 103 | 104 | time = tick.time_msc 105 | 106 | if trade.days_end(): 107 | trade.close_position("End of the trading day reached.") 108 | break 109 | 110 | logger.info("Finishing the program.") 111 | logger.info("Program finished.") 112 | -------------------------------------------------------------------------------- /docs/examples/fimathe/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Fimathe 3 | 4 | This is a simple version that I made for Fimathe strategy to be used as a base for your strategy 5 | and your improvements. 6 | 7 | This strategy does not cover price reversion. 8 | It covers only continuity. 9 | 10 | # Table of contents: 11 | 12 | - [Strategy](#strategy) 13 | 14 | - [Example](#example) 15 | - [Forex:](#forex) 16 | - [EURUSD](#eurusd) 17 | - [B3 - Brazilian Stock Exchange:](#b3---brazilian-stock-exchange) 18 | - [WIN](#win) 19 | 20 | 21 | 22 | 23 | ## Strategy: 24 | 25 | The initial setup is the area that the price has the freedom to move, which goes from 0, the most recent candle, 26 | to the 5 past candles. 27 | ```python 28 | space_to_trade = 5 29 | ``` 30 | 31 | How many bars in the past the code will be check to calculate the Fibonacci Zones. 32 | ```python 33 | period = 15 34 | ``` 35 | The period starts after the space_to_trade. (5 + 15) 36 | 37 | 38 | The trading zones are calculated with the difference from high and low price inside of period, in this example 39 | the period is 15 after the freedom movement. 40 | 41 | The difference is multiplied by their percentage amount desired to find the zone. 42 | ```python 43 | # Zones: 44 | zone_236 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - 45 | np.amin(rates.low)) * 0.236) / int(trade.sl_tp_steps)) # 23.60% 46 | 47 | zone_382 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - 48 | np.amin(rates.low)) * 0.381) / int(trade.sl_tp_steps)) # 38.20% 49 | 50 | zone_500 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - 51 | np.amin(rates.low)) * 0.500) / int(trade.sl_tp_steps)) # 50.00% 52 | 53 | zone_618 = int(trade.sl_tp_steps) * round(((np.amax(rates.high) - 54 | np.amin(rates.low)) * 0.618) / int(trade.sl_tp_steps)) # 61.80% 55 | ``` 56 | 57 | The strategy to open a position check the trend: 58 | ```python 59 | # Bull trend: 60 | if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] < 0: 61 | 62 | # Bear trend: 63 | if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] > 0: 64 | ``` 65 | The strategy looks for the minimum and maximum values and identifies their array position. 66 | 67 | With the array position, it is possible to identify the slop direction. 68 | 69 | Example 1: 70 | Low position = 8 71 | High position = 10 72 | 8 - 10 = -2, 73 | which means bull trend, because the high price are farther than the low price. 74 | 75 | Example 2: 76 | Low position = 12 77 | High position = 7 78 | 12 - 7 = 5, 79 | which meas bear trend, because the low price are farther than the high price. 80 | 81 | For open a BUY position the strategy waits the price goes 38.2% above the highest price in 15 period. 82 | ```python 83 | buy = tick.last > np.amax(rates.high) + zone_382 and \ 84 | util.minutes_counter_after_trade(trade.symbol, delay_after_trade) 85 | ``` 86 | 87 | To open the SELL position, the logic is the same, however, it waits the price goes below 38.2% the 88 | lowest price in 15 periods. 89 | ```python 90 | sell = tick.last < np.amin(rates.low) - zone_382 and \ 91 | util.minutes_counter_after_trade(trade.symbol, delay_after_trade) 92 | ``` 93 | Also, for buy and sell, it checks if some operation recently happened. 94 | When a recent operation has happened, it will wait for the number of minutes to return True to trade again. 95 | ```python 96 | util.minutes_counter_after_trade(trade.symbol, delay_after_trade) 97 | ``` 98 | The util.minutes_counter_after_trade, when not used as a condition to open a position, it prints how many minutes 99 | remains to be able to trade again. 100 | 101 | The amount of minutes the strategy waits to be able to open a new position is set by the user 102 | through the variable delay_after_trade = 5 103 | 104 | After the position is opened the StopLoss and TakeProfit are applied. 105 | The StopLoss will be at 38.2% of the opened price, and the TakeProfit will be at 61.8% of the opened price. 106 | ```python 107 | trade.stop_loss = zone_382 108 | trade.take_profit = zone_618 109 | ``` 110 | 111 | Also, the stop is moved when the price goes to right direction, when the price moved more than 23.6% for the right 112 | direction, the stop is moved to the nearest price to zero. 113 | ```python 114 | if len(Mt5.positions_get(symbol=trade.symbol)) == 1: 115 | 116 | if Mt5.positions_get(symbol=trade.symbol)[0].type == 0: # if Buy 117 | if tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236: 118 | trade.stop_loss = trade.sl_tp_steps 119 | 120 | elif Mt5.positions_get(symbol=trade.symbol)[0].type == 1: # if Sell 121 | if tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236: 122 | trade.stop_loss = trade.sl_tp_steps 123 | ``` 124 | 125 | ## Example: 126 | 127 | - ## Forex: 128 | - ## [EURUSD](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/master/examples_of_expert_advisor/Fimathe/eurusd_fimathe.py) 129 | - ## B3 - Brazilian Stock Exchange: 130 | - ## [WIN](https://github.com/Joaopeuko/Mql5-Python-Integration/blob/master/examples_of_expert_advisor/Fimathe/win_fimathe.py) 131 | -------------------------------------------------------------------------------- /docs/examples/fimathe/__init__.py: -------------------------------------------------------------------------------- 1 | """FiMathe strategy examples for MetaTrader 5. 2 | 3 | This package contains examples of Expert Advisors using Fibonacci retracement levels. 4 | """ 5 | -------------------------------------------------------------------------------- /docs/examples/fimathe/eurusd_fimathe.py: -------------------------------------------------------------------------------- 1 | """EURUSD FiMathe Expert Advisor for MetaTrader 5. 2 | 3 | This Expert Advisor uses Fibonacci retracement levels to determine entry and exit points. 4 | """ 5 | 6 | import MetaTrader5 as Mt5 7 | import numpy as np 8 | from include.rates import Rates 9 | from include.tick import Tick 10 | from include.trade import Trade 11 | from include.utilities import Utilities 12 | 13 | util = Utilities() 14 | 15 | trade = Trade( 16 | "Example", # Expert name 17 | 0.1, # Expert Version 18 | "EURUSD", # symbol 19 | 567, # Magic number 20 | 0.01, # lot, it is a floating point. 21 | 25, # stop loss 22 | 300, # emergency stop loss 23 | 25, # take profit 24 | 300, # emergency take profit 25 | "00:10", # It is allowed to trade after that hour. Do not use zeros, like: 09 26 | "23:50", # It is not allowed to trade after that hour but let open all the position already opened. 27 | "23:50", # It closes all the position opened. Do not use zeros, like: 09 28 | 0.0, # average fee 29 | ) 30 | 31 | buy = False 32 | sell = False 33 | 34 | delay_after_trade = 3 35 | space_to_trade = 3 36 | period = 10 37 | 38 | time = 0 39 | while True: 40 | tick = Tick(trade.symbol) 41 | rates = Rates(trade.symbol, Mt5.TIMEFRAME_M1, space_to_trade, period) 42 | 43 | util.minutes_counter_after_trade(trade.symbol, delay_after_trade) 44 | 45 | if tick.time_msc != time: 46 | # Zones: 47 | zone_236 = round(((np.amax(rates.high) - np.amin(rates.low)) * 23.6) * 1000) # 23.60% 48 | 49 | zone_382 = round(((np.amax(rates.high) - np.amin(rates.low)) * 38.1) * 1000) # 38.20% 50 | 51 | zone_500 = round(((np.amax(rates.high) - np.amin(rates.low)) * 50.0) * 1000) # 50.00% 52 | 53 | zone_618 = round(((np.amax(rates.high) - np.amin(rates.low)) * 61.8) * 1000) # 61.80% 54 | 55 | # Bull trend: 56 | if (np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0]) < 0: 57 | # Buy 58 | buy = tick.ask > np.amax(rates.high) + (zone_382 / 100000) and util.minutes_counter_after_trade( 59 | trade.symbol, delay_after_trade 60 | ) 61 | if buy: 62 | trade.stop_loss = zone_236 63 | trade.take_profit = zone_618 64 | 65 | # Bear trend: 66 | if (np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0]) > 0: 67 | # Sell 68 | sell = tick.bid < np.amin(rates.low) - (zone_382 / 100000) and util.minutes_counter_after_trade( 69 | trade.symbol, delay_after_trade 70 | ) 71 | if sell: 72 | trade.stop_loss = zone_236 73 | trade.take_profit = zone_618 74 | 75 | if len(Mt5.positions_get(symbol=trade.symbol)) == 1 and ( 76 | ( 77 | Mt5.positions_get(symbol=trade.symbol)[0].type == 0 78 | and tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236 79 | ) 80 | or ( 81 | Mt5.positions_get(symbol=trade.symbol)[0].type == 1 82 | and tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236 83 | ) 84 | ): 85 | trade.stop_loss = trade.sl_tp_steps 86 | 87 | trade.emergency_stop_loss = trade.stop_loss + zone_236 88 | trade.emergency_take_profit = trade.take_profit + zone_236 89 | trade.open_position(buy, sell, "") 90 | 91 | time = tick.time_msc 92 | 93 | if trade.days_end(): 94 | trade.close_position("End of the trading day reached.") 95 | break 96 | -------------------------------------------------------------------------------- /docs/examples/fimathe/win_fimathe.py: -------------------------------------------------------------------------------- 1 | """WIN FiMathe Expert Advisor for MetaTrader 5. 2 | 3 | This Expert Advisor is designed for WING21 futures, using Fibonacci retracement levels to 4 | determine entry and exit points. 5 | """ 6 | 7 | import MetaTrader5 as Mt5 8 | import numpy as np 9 | from include.rates import Rates 10 | from include.tick import Tick 11 | from include.trade import Trade 12 | from include.utilities import Utilities 13 | 14 | util = Utilities() 15 | 16 | trade = Trade( 17 | "Example", # Expert name 18 | 0.1, # Expert Version 19 | "WING21", # symbol 20 | 567, # Magic number 21 | 1.0, # lot, it is a floating point. 22 | 25, # stop loss 23 | 300, # emergency stop loss 24 | 25, # take profit 25 | 300, # emergency take profit 26 | "9:25", # It is allowed to trade after that hour. Do not use zeros, like: 09 27 | "17:45", # It is not allowed to trade after that hour but let open all the position already opened. 28 | "17:50", # It closes all the position opened. Do not use zeros, like: 09 29 | 0.0, # average fee 30 | ) 31 | 32 | buy = False 33 | sell = False 34 | 35 | delay_after_trade = 5 36 | space_to_trade = 5 37 | period = 15 38 | 39 | time = 0 40 | while True: 41 | tick = Tick(trade.symbol) 42 | rates = Rates(trade.symbol, Mt5.TIMEFRAME_M1, space_to_trade, period) 43 | 44 | if tick.time_msc != time: 45 | util.minutes_counter_after_trade(trade.symbol, delay_after_trade) 46 | 47 | # Zones: 48 | zone_236 = int(trade.sl_tp_steps) * round( 49 | ((np.amax(rates.high) - np.amin(rates.low)) * 0.236) / int(trade.sl_tp_steps) 50 | ) # 23.60% 51 | 52 | zone_382 = int(trade.sl_tp_steps) * round( 53 | ((np.amax(rates.high) - np.amin(rates.low)) * 0.381) / int(trade.sl_tp_steps) 54 | ) # 38.20% 55 | 56 | zone_500 = int(trade.sl_tp_steps) * round( 57 | ((np.amax(rates.high) - np.amin(rates.low)) * 0.500) / int(trade.sl_tp_steps) 58 | ) # 50.00% 59 | 60 | zone_618 = int(trade.sl_tp_steps) * round( 61 | ((np.amax(rates.high) - np.amin(rates.low)) * 0.618) / int(trade.sl_tp_steps) 62 | ) # 61.80% 63 | 64 | # Bull trend: 65 | if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] < 0: 66 | # Buy 67 | buy = tick.last > np.amax(rates.high) + zone_382 and util.minutes_counter_after_trade( 68 | trade.symbol, delay_after_trade 69 | ) 70 | 71 | if buy: 72 | trade.stop_loss = zone_382 73 | trade.take_profit = zone_618 74 | 75 | # Bear trend: 76 | if np.where(rates.low == np.amin(rates.low))[0][0] - np.where(rates.high == np.amax(rates.high))[0][0] > 0: 77 | # Sell 78 | sell = tick.last < np.amin(rates.low) - zone_382 and util.minutes_counter_after_trade( 79 | trade.symbol, delay_after_trade 80 | ) 81 | if sell: 82 | trade.stop_loss = zone_382 83 | trade.take_profit = zone_618 84 | 85 | if len(Mt5.positions_get(symbol=trade.symbol)) == 1 and ( 86 | ( 87 | Mt5.positions_get(symbol=trade.symbol)[0].type == 0 88 | and tick.last > Mt5.positions_get(symbol=trade.symbol)[0].price_open + zone_236 89 | ) 90 | or ( 91 | Mt5.positions_get(symbol=trade.symbol)[0].type == 1 92 | and tick.last < Mt5.positions_get(symbol=trade.symbol)[0].price_open - zone_236 93 | ) 94 | ): 95 | trade.stop_loss = trade.sl_tp_steps 96 | 97 | trade.emergency_stop_loss = trade.stop_loss 98 | trade.emergency_take_profit = trade.take_profit 99 | trade.open_position(buy, sell, "") 100 | 101 | time = tick.time_msc 102 | 103 | if trade.days_end(): 104 | trade.close_position("End of the trading day reached.") 105 | break 106 | -------------------------------------------------------------------------------- /docs/examples/getting_started.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Getting Started with MQPy. 3 | 4 | This example demonstrates the basic usage of the MQPy framework for algorithmic trading 5 | with MetaTrader 5. It shows how to fetch market data, access price information, and 6 | execute basic trading operations. 7 | """ 8 | 9 | import logging 10 | 11 | from mqpy.rates import Rates 12 | from mqpy.tick import Tick 13 | from mqpy.trade import Trade 14 | 15 | # Configure logging 16 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def main() -> None: 21 | """Main execution function for the Getting Started example.""" 22 | # Step 1: Initialize the trading strategy with basic parameters 23 | # This creates a Trade object that will handle your trading operations 24 | trade = Trade( 25 | expert_name="Getting Started Example", # Name of your trading strategy 26 | version="1.0", # Version of your strategy 27 | symbol="EURUSD", # Trading symbol/instrument 28 | magic_number=123, # Unique identifier for your strategy's trades 29 | lot=0.01, # Trade size in lots (0.01 = micro lot) 30 | stop_loss=20, # Stop loss in points 31 | emergency_stop_loss=100, # Emergency stop loss in points 32 | take_profit=40, # Take profit in points 33 | emergency_take_profit=120, # Emergency take profit in points 34 | start_time="9:00", # Trading session start time 35 | finishing_time="17:00", # Time to stop opening new positions 36 | ending_time="17:30", # Time to close all positions 37 | fee=0.0, # Commission fee 38 | ) 39 | 40 | # Log that we're starting the strategy 41 | logger.info(f"Starting Getting Started example on {trade.symbol}") 42 | 43 | # Step 2: Get current market data 44 | # Fetch the current tick data (latest price) for our trading symbol 45 | current_tick = Tick(trade.symbol) 46 | 47 | # Display the current price information 48 | logger.info(f"Current price for {trade.symbol}:") 49 | logger.info(f" Bid price: {current_tick.bid}") # Price at which you can sell 50 | logger.info(f" Ask price: {current_tick.ask}") # Price at which you can buy 51 | logger.info(f" Last price: {current_tick.last}") # Last executed price 52 | 53 | # Step 3: Get historical price data 54 | # Fetch the last 10 candles of price data 55 | historical_rates = Rates(trade.symbol, 10, 0, 1) # 10 candles, starting from the most recent (0), timeframe M1 56 | 57 | # Display the historical price data 58 | logger.info(f"Last 10 candles for {trade.symbol}:") 59 | for i in range(min(3, len(historical_rates.time))): # Show only first 3 candles for brevity 60 | logger.info(f" Candle {i+1}:") 61 | logger.info(f" Open: {historical_rates.open[i]}") 62 | logger.info(f" High: {historical_rates.high[i]}") 63 | logger.info(f" Low: {historical_rates.low[i]}") 64 | logger.info(f" Close: {historical_rates.close[i]}") 65 | logger.info(f" Volume: {historical_rates.tick_volume[i]}") 66 | 67 | # Step 4: Prepare the trading environment 68 | # This ensures the symbol is ready for trading 69 | trade.prepare_symbol() 70 | 71 | # Step 5: Implement a very simple trading logic 72 | # For this example, we'll use a simple condition: 73 | # Buy if the current price is higher than the average of the last 10 candles 74 | # Sell if the current price is lower than the average of the last 10 candles 75 | 76 | # Calculate the average price of the last 10 candles 77 | average_price = sum(historical_rates.close) / len(historical_rates.close) 78 | logger.info(f"Average closing price of last 10 candles: {average_price}") 79 | 80 | # Set up our trading signals 81 | should_buy = current_tick.ask > average_price 82 | should_sell = current_tick.bid < average_price 83 | 84 | # Log our trading decision 85 | if should_buy: 86 | logger.info(f"Buy signal generated: Current price ({current_tick.ask}) > Average price ({average_price})") 87 | elif should_sell: 88 | logger.info(f"Sell signal generated: Current price ({current_tick.bid}) < Average price ({average_price})") 89 | else: 90 | logger.info("No trading signal generated") 91 | 92 | # Step 6: Execute a trade if we're within trading hours 93 | if trade.trading_time(): 94 | logger.info("Within trading hours, executing trade if signals are present") 95 | trade.open_position(should_buy=should_buy, should_sell=should_sell, comment="Getting Started Example Trade") 96 | else: 97 | logger.info("Outside trading hours, not executing any trades") 98 | 99 | # Step 7: Update trading statistics and log current positions 100 | trade.statistics() 101 | 102 | # Step 8: Demonstrate how to close all positions at the end of the trading day 103 | logger.info("Demonstrating position closing (not actually closing any positions)") 104 | 105 | # NOTE: In a real strategy, you would check if it's the end of the day and close positions 106 | # This would be implemented by checking days_end() and calling close_position() 107 | 108 | logger.info("Getting started example completed") 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /docs/examples/indicator_connector_strategy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Indicator Connector Strategy Example. 3 | 4 | This example demonstrates how to use the Indicator Connector to retrieve indicator 5 | values from MT5 custom indicators. The strategy uses a simple moving average 6 | crossover logic based on indicator values received through the connector. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import logging 12 | import time 13 | from typing import Any 14 | 15 | from mqpy.indicators import Indicators 16 | from mqpy.tick import Tick 17 | from mqpy.trade import Trade 18 | 19 | # Configure logging 20 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def get_indicator_values( 25 | indicators: Indicators, indicator_name: str, indicator_args: dict[str, Any] 26 | ) -> list[float] | None: 27 | """Get indicator values from MT5 using the IndicatorConnector. 28 | 29 | Args: 30 | indicators: The Indicators connector instance 31 | indicator_name: Name of the MT5 indicator 32 | indicator_args: Dictionary of arguments for the indicator 33 | 34 | Returns: 35 | List of indicator values or None if the request failed 36 | """ 37 | try: 38 | # Request indicator data from MT5 39 | response = indicators.get_indicator_data( 40 | indicator_name=indicator_name, 41 | symbol=indicator_args.get("symbol", ""), 42 | timeframe=indicator_args.get("timeframe", 0), 43 | count=indicator_args.get("count", 0), 44 | lines=indicator_args.get("lines", [0]), 45 | args=indicator_args.get("args", []), 46 | ) 47 | except Exception: 48 | logger.exception("Exception while getting indicator values") 49 | return None 50 | 51 | # Check if the request was successful (outside the try/except block) 52 | if response is not None and response.error_code == 0: 53 | return response.data[0] # Return the first line's data 54 | 55 | # Handle error case 56 | logger.error(f"Error getting indicator values: {response.error_message if response else 'No response'}") 57 | return None 58 | 59 | 60 | def wait_for_connection(indicators: Indicators, max_attempts: int = 10) -> bool: 61 | """Wait for the indicator connector to establish a connection. 62 | 63 | Args: 64 | indicators: The Indicators connector instance 65 | max_attempts: Maximum number of connection attempts 66 | 67 | Returns: 68 | True if connection was established, False otherwise 69 | """ 70 | for attempt in range(max_attempts): 71 | if indicators.is_connected(): 72 | logger.info("Indicator connector successfully connected") 73 | return True 74 | 75 | logger.info(f"Waiting for indicator connector to connect... Attempt {attempt + 1}/{max_attempts}") 76 | time.sleep(1) 77 | 78 | logger.error("Failed to connect to indicator connector after maximum attempts") 79 | return False 80 | 81 | 82 | def analyze_moving_averages(fast_ma: list[float], slow_ma: list[float]) -> tuple[bool, bool]: 83 | """Analyze moving averages to generate trading signals. 84 | 85 | Args: 86 | fast_ma: Fast moving average values 87 | slow_ma: Slow moving average values 88 | 89 | Returns: 90 | Tuple of (buy_signal, sell_signal) 91 | """ 92 | if len(fast_ma) < 2 or len(slow_ma) < 2: 93 | return False, False 94 | 95 | # Current values 96 | current_fast = fast_ma[-1] 97 | current_slow = slow_ma[-1] 98 | 99 | # Previous values 100 | previous_fast = fast_ma[-2] 101 | previous_slow = slow_ma[-2] 102 | 103 | # Generate signals based on crossover 104 | buy_signal = previous_fast <= previous_slow and current_fast > current_slow 105 | sell_signal = previous_fast >= previous_slow and current_fast < current_slow 106 | 107 | return buy_signal, sell_signal 108 | 109 | 110 | def main() -> None: 111 | """Main execution function for the Indicator Connector strategy.""" 112 | # Initialize the trading strategy 113 | trade = Trade( 114 | expert_name="Indicator Connector Strategy", 115 | version="1.0", 116 | symbol="EURUSD", 117 | magic_number=572, 118 | lot=0.1, 119 | stop_loss=30, 120 | emergency_stop_loss=90, 121 | take_profit=60, 122 | emergency_take_profit=180, 123 | start_time="9:15", 124 | finishing_time="17:30", 125 | ending_time="17:50", 126 | fee=0.5, 127 | ) 128 | 129 | logger.info(f"Starting Indicator Connector strategy on {trade.symbol}") 130 | 131 | # Initialize the indicator connector 132 | indicators = Indicators() 133 | 134 | # Wait for connection to be established 135 | if not wait_for_connection(indicators): 136 | logger.error("Could not connect to indicator connector - exiting") 137 | return 138 | 139 | # Strategy parameters 140 | prev_tick_time = 0 141 | # We'll use Moving Average indicator with different periods 142 | fast_ma_period = 14 143 | slow_ma_period = 50 144 | 145 | try: 146 | while True: 147 | # Prepare the symbol for trading 148 | trade.prepare_symbol() 149 | 150 | # Fetch current tick data 151 | current_tick = Tick(trade.symbol) 152 | 153 | # Only process if we have a new tick 154 | if current_tick.time_msc != prev_tick_time: 155 | # Set up indicator arguments for fast MA 156 | fast_ma_args = { 157 | "symbol": trade.symbol, 158 | "timeframe": 1, # 1-minute timeframe 159 | "count": 10, # Get 10 values 160 | "lines": [0], # The first line of the indicator 161 | "args": [fast_ma_period, 0, 0], # Period, shift, MA method 162 | } 163 | 164 | # Set up indicator arguments for slow MA 165 | slow_ma_args = { 166 | "symbol": trade.symbol, 167 | "timeframe": 1, # 1-minute timeframe 168 | "count": 10, # Get 10 values 169 | "lines": [0], # The first line of the indicator 170 | "args": [slow_ma_period, 0, 0], # Period, shift, MA method 171 | } 172 | 173 | # Get indicator values 174 | fast_ma_values = get_indicator_values(indicators, "Moving Average", fast_ma_args) 175 | slow_ma_values = get_indicator_values(indicators, "Moving Average", slow_ma_args) 176 | 177 | # Check if we got valid data 178 | if fast_ma_values and slow_ma_values: 179 | # Log the current indicator values 180 | logger.info(f"Fast MA ({fast_ma_period}): {fast_ma_values[-1]:.5f}") 181 | logger.info(f"Slow MA ({slow_ma_period}): {slow_ma_values[-1]:.5f}") 182 | 183 | # Generate signals based on moving average crossovers 184 | buy_signal, sell_signal = analyze_moving_averages(fast_ma_values, slow_ma_values) 185 | 186 | # Log signals 187 | if buy_signal: 188 | logger.info("Buy signal: Fast MA crossed above Slow MA") 189 | elif sell_signal: 190 | logger.info("Sell signal: Fast MA crossed below Slow MA") 191 | 192 | # Execute trading positions based on signals 193 | if trade.trading_time(): # Only trade during allowed hours 194 | trade.open_position( 195 | should_buy=buy_signal, should_sell=sell_signal, comment="Indicator Connector Strategy" 196 | ) 197 | 198 | # Update trading statistics 199 | trade.statistics() 200 | 201 | prev_tick_time = current_tick.time_msc 202 | 203 | # Check if it's the end of the trading day 204 | if trade.days_end(): 205 | trade.close_position("End of the trading day reached.") 206 | break 207 | 208 | # Add a short delay to avoid excessive CPU usage 209 | time.sleep(0.1) 210 | 211 | except KeyboardInterrupt: 212 | logger.info("Strategy execution interrupted by user.") 213 | trade.close_position("User interrupted the strategy.") 214 | except Exception: 215 | logger.exception("Error in strategy execution") 216 | finally: 217 | logger.info("Finishing the program.") 218 | # Make sure to disconnect from the indicator connector 219 | indicators.disconnect() 220 | 221 | 222 | if __name__ == "__main__": 223 | main() 224 | -------------------------------------------------------------------------------- /docs/examples/rate_converter_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Rate Converter Example. 3 | 4 | This example demonstrates how to use the MQPy rate converter to convert between different 5 | timeframes in MetaTrader 5, which is essential for multi-timeframe analysis strategies. 6 | """ 7 | 8 | import logging 9 | 10 | import matplotlib.pyplot as plt 11 | import MetaTrader5 as Mt5 12 | import numpy as np 13 | import pandas as pd 14 | 15 | from mqpy.rate_converter import RateConverter 16 | from mqpy.rates import Rates 17 | 18 | # Configure logging 19 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def print_rates_info(rates: Rates, timeframe_name: str) -> None: 24 | """Print information about the rates data. 25 | 26 | Args: 27 | rates: The rates data object 28 | timeframe_name: Name of the timeframe for display 29 | """ 30 | logger.info(f"{timeframe_name} Timeframe Data:") 31 | logger.info(f" Number of candles: {len(rates.time)}") 32 | 33 | if len(rates.time) > 0: 34 | # Convert first timestamp to readable format 35 | first_time = pd.to_datetime(rates.time[0], unit="s") 36 | last_time = pd.to_datetime(rates.time[-1], unit="s") 37 | 38 | logger.info(f" Time range: {first_time} to {last_time}") 39 | logger.info( 40 | f" First candle: Open={rates.open[0]}, High={rates.high[0]}, Low={rates.low[0]}, Close={rates.close[0]}" 41 | ) 42 | logger.info( 43 | f" Last candle: Open={rates.open[-1]}, High={rates.high[-1]}, Low={rates.low[-1]}, Close={rates.close[-1]}" 44 | ) 45 | 46 | 47 | def plot_multi_timeframe_data(m1_rates: Rates, m5_rates: Rates, h1_rates: Rates) -> None: 48 | """Create a simple visualization of multi-timeframe data. 49 | 50 | Args: 51 | m1_rates: 1-minute timeframe rates 52 | m5_rates: 5-minute timeframe rates 53 | h1_rates: 1-hour timeframe rates 54 | """ 55 | try: 56 | # Create figure with 3 subplots 57 | fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=False) 58 | 59 | # Convert timestamps to datetime for better x-axis labels 60 | m1_times = pd.to_datetime(m1_rates.time, unit="s") 61 | m5_times = pd.to_datetime(m5_rates.time, unit="s") 62 | h1_times = pd.to_datetime(h1_rates.time, unit="s") 63 | 64 | # Plot M1 data 65 | axes[0].plot(m1_times, m1_rates.close) 66 | axes[0].set_title("1-Minute Timeframe") 67 | axes[0].set_ylabel("Price") 68 | axes[0].grid(visible=True) 69 | 70 | # Plot M5 data 71 | axes[1].plot(m5_times, m5_rates.close) 72 | axes[1].set_title("5-Minute Timeframe") 73 | axes[1].set_ylabel("Price") 74 | axes[1].grid(visible=True) 75 | 76 | # Plot H1 data 77 | axes[2].plot(h1_times, h1_rates.close) 78 | axes[2].set_title("1-Hour Timeframe") 79 | axes[2].set_ylabel("Price") 80 | axes[2].set_xlabel("Time") 81 | axes[2].grid(visible=True) 82 | 83 | # Adjust layout 84 | plt.tight_layout() 85 | 86 | # Save the plot 87 | plt.savefig("multi_timeframe_analysis.png") 88 | logger.info("Saved visualization to 'multi_timeframe_analysis.png'") 89 | 90 | # Show the plot if in interactive mode 91 | plt.show() 92 | 93 | except Exception: 94 | logger.exception("Error creating visualization") 95 | 96 | 97 | def main() -> None: 98 | """Main execution function for the Rate Converter example.""" 99 | # Define the symbol to analyze 100 | symbol = "EURUSD" 101 | logger.info(f"Starting Rate Converter example for {symbol}") 102 | 103 | # Step 1: Get 1-minute timeframe data (100 candles) 104 | m1_rates = Rates(symbol, 100, 0, Mt5.TIMEFRAME_M1) 105 | print_rates_info(m1_rates, "M1") 106 | 107 | # Step 2: Use RateConverter to convert M1 to M5 timeframe 108 | logger.info("Converting M1 to M5 timeframe...") 109 | rate_converter = RateConverter() 110 | 111 | # Convert M1 data to M5 timeframe 112 | m5_rates = rate_converter.convert_rates(rates=m1_rates, new_timeframe=Mt5.TIMEFRAME_M5, price_type="close") 113 | print_rates_info(m5_rates, "M5 (converted from M1)") 114 | 115 | # Step 3: Use RateConverter to convert M1 to H1 timeframe 116 | logger.info("Converting M1 to H1 timeframe...") 117 | h1_rates = rate_converter.convert_rates(rates=m1_rates, new_timeframe=Mt5.TIMEFRAME_H1, price_type="close") 118 | print_rates_info(h1_rates, "H1 (converted from M1)") 119 | 120 | # Step 4: Demonstrate a simple multi-timeframe analysis 121 | logger.info("Performing multi-timeframe analysis...") 122 | 123 | # Calculate simple moving averages for different timeframes 124 | # For M1 data, calculate a 20-period SMA 125 | m1_sma = np.mean(m1_rates.close[-20:]) if len(m1_rates.close) >= 20 else None 126 | 127 | # For M5 data, calculate a 10-period SMA 128 | m5_sma = np.mean(m5_rates.close[-10:]) if len(m5_rates.close) >= 10 else None 129 | 130 | # For H1 data, calculate a 5-period SMA 131 | h1_sma = np.mean(h1_rates.close[-5:]) if len(h1_rates.close) >= 5 else None 132 | 133 | logger.info("Moving Average Results:") 134 | logger.info(f" M1 20-period SMA: {m1_sma:.5f}" if m1_sma is not None else " M1 SMA: Not enough data") 135 | logger.info(f" M5 10-period SMA: {m5_sma:.5f}" if m5_sma is not None else " M5 SMA: Not enough data") 136 | logger.info(f" H1 5-period SMA: {h1_sma:.5f}" if h1_sma is not None else " H1 SMA: Not enough data") 137 | 138 | # Step 5: Create a simple visualization 139 | logger.info("Creating visualization of multi-timeframe data...") 140 | plot_multi_timeframe_data(m1_rates, m5_rates, h1_rates) 141 | 142 | logger.info("Rate Converter example completed") 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /docs/examples/rsi_strategy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RSI (Relative Strength Index) Strategy Example. 3 | 4 | This example demonstrates an RSI-based trading strategy using the mqpy framework. 5 | The strategy enters long positions when RSI is below the oversold threshold and 6 | enters short positions when RSI is above the overbought threshold. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import logging 12 | 13 | import numpy as np 14 | 15 | from mqpy.rates import Rates 16 | from mqpy.tick import Tick 17 | from mqpy.trade import Trade 18 | 19 | # Configure logging 20 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def calculate_rsi(prices: list[float], period: int = 14) -> float | None: 25 | """Calculate the Relative Strength Index. 26 | 27 | Args: 28 | prices: A list of closing prices 29 | period: The RSI period (default: 14) 30 | 31 | Returns: 32 | The RSI value (0-100) or None if insufficient data 33 | """ 34 | if len(prices) < period + 1: 35 | return None 36 | 37 | # Calculate price changes 38 | deltas = np.diff(prices) 39 | 40 | # Separate gains and losses 41 | gains = np.where(deltas > 0, deltas, 0) 42 | losses = np.where(deltas < 0, -deltas, 0) 43 | 44 | # Calculate initial average gain and loss 45 | avg_gain = np.mean(gains[:period]) 46 | avg_loss = np.mean(losses[:period]) 47 | 48 | # Avoid division by zero 49 | if avg_loss == 0: 50 | return 100 51 | 52 | # Calculate RS and RSI 53 | rs = avg_gain / avg_loss 54 | rsi = 100 - (100 / (1 + rs)) 55 | 56 | return rsi 57 | 58 | 59 | def main() -> None: 60 | """Main execution function for the RSI strategy.""" 61 | # Initialize the trading strategy 62 | trade = Trade( 63 | expert_name="RSI Strategy", 64 | version="1.0", 65 | symbol="EURUSD", 66 | magic_number=568, 67 | lot=0.1, 68 | stop_loss=30, 69 | emergency_stop_loss=90, 70 | take_profit=60, 71 | emergency_take_profit=180, 72 | start_time="9:15", 73 | finishing_time="17:30", 74 | ending_time="17:50", 75 | fee=0.5, 76 | ) 77 | 78 | logger.info(f"Starting RSI strategy on {trade.symbol}") 79 | 80 | # Strategy parameters 81 | prev_tick_time = 0 82 | rsi_period = 14 83 | overbought_threshold = 70 84 | oversold_threshold = 30 85 | 86 | try: 87 | while True: 88 | # Prepare the symbol for trading 89 | trade.prepare_symbol() 90 | 91 | # Fetch tick and rates data 92 | current_tick = Tick(trade.symbol) 93 | historical_rates = Rates(trade.symbol, rsi_period + 20, 0, 1) # Get extra data for reliability 94 | 95 | # Only process if we have a new tick and enough data for RSI calculation 96 | if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= rsi_period + 1: 97 | # Calculate RSI 98 | rsi_value = calculate_rsi(historical_rates.close, rsi_period) 99 | 100 | if rsi_value is not None: 101 | # Generate signals based on RSI thresholds 102 | is_buy_signal = rsi_value < oversold_threshold 103 | is_sell_signal = rsi_value > overbought_threshold 104 | 105 | # Log RSI values and signals 106 | if is_buy_signal: 107 | logger.info(f"Oversold condition: RSI = {rsi_value:.2f} (< {oversold_threshold})") 108 | elif is_sell_signal: 109 | logger.info(f"Overbought condition: RSI = {rsi_value:.2f} (> {overbought_threshold})") 110 | else: 111 | logger.debug(f"Current RSI: {rsi_value:.2f}") 112 | 113 | # Execute trading positions based on signals during allowed trading hours 114 | if trade.trading_time(): 115 | trade.open_position( 116 | should_buy=is_buy_signal, 117 | should_sell=is_sell_signal, 118 | comment=f"RSI Strategy: {rsi_value:.2f}", 119 | ) 120 | 121 | # Update trading statistics periodically 122 | trade.statistics() 123 | 124 | prev_tick_time = current_tick.time_msc 125 | 126 | # Check if it's the end of the trading day 127 | if trade.days_end(): 128 | trade.close_position("End of the trading day reached.") 129 | break 130 | 131 | except KeyboardInterrupt: 132 | logger.info("Strategy execution interrupted by user.") 133 | trade.close_position("User interrupted the strategy.") 134 | except Exception: 135 | logger.exception("Error in strategy execution") 136 | finally: 137 | logger.info("Finishing the program.") 138 | 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /docs/extra.js: -------------------------------------------------------------------------------- 1 | function cleanupClipboardText(targetSelector) { 2 | const targetElement = document.querySelector(targetSelector); 3 | 4 | // exclude "Generic Prompt" and "Generic Output" spans from copy 5 | const excludedClasses = ["gp", "go"]; 6 | 7 | const clipboardText = Array.from(targetElement.childNodes) 8 | .filter( 9 | (node) => 10 | !excludedClasses.some((className) => 11 | node?.classList?.contains(className), 12 | ), 13 | ) 14 | .map((node) => node.textContent) 15 | .filter((s) => s != ""); 16 | return clipboardText.join("").trim(); 17 | } 18 | 19 | // Sets copy text to attributes lazily using an Intersection Observer. 20 | function setCopyText() { 21 | // The `data-clipboard-text` attribute allows for customized content in the copy 22 | // See: https://www.npmjs.com/package/clipboard#copy-text-from-attribute 23 | const attr = "clipboardText"; 24 | // all "copy" buttons whose target selector is a element 25 | const elements = document.querySelectorAll( 26 | 'button[data-clipboard-target$="code"]', 27 | ); 28 | const observer = new IntersectionObserver((entries) => { 29 | entries.forEach((entry) => { 30 | // target in the viewport that have not been patched 31 | if ( 32 | entry.intersectionRatio > 0 && 33 | entry.target.dataset[attr] === undefined 34 | ) { 35 | entry.target.dataset[attr] = cleanupClipboardText( 36 | entry.target.dataset.clipboardTarget, 37 | ); 38 | } 39 | }); 40 | }); 41 | 42 | elements.forEach((elt) => { 43 | observer.observe(elt); 44 | }); 45 | } 46 | 47 | // Show sponsor popup on first visit 48 | function showSponsorPopup() { 49 | // Check if user has seen the popup before 50 | if (!localStorage.getItem('mqpy_sponsor_popup_shown')) { 51 | // Create popup container 52 | const popup = document.createElement('div'); 53 | popup.className = 'sponsor-popup'; 54 | 55 | // Create popup content 56 | popup.innerHTML = ` 57 | 78 | `; 79 | 80 | // Add popup to body 81 | document.body.appendChild(popup); 82 | 83 | // Show popup with animation 84 | setTimeout(() => { 85 | popup.classList.add('active'); 86 | }, 1000); 87 | 88 | // Close button event 89 | const closeBtn = popup.querySelector('.sponsor-popup-close'); 90 | closeBtn.addEventListener('click', () => { 91 | popup.classList.remove('active'); 92 | 93 | // Check if "don't show again" is checked 94 | const dontShowAgain = document.getElementById('sponsor-popup-dont-show').checked; 95 | if (dontShowAgain) { 96 | localStorage.setItem('mqpy_sponsor_popup_shown', 'true'); 97 | } 98 | 99 | // Remove popup after animation 100 | setTimeout(() => { 101 | popup.remove(); 102 | }, 300); 103 | }); 104 | } 105 | } 106 | 107 | // Using the document$ observable is particularly important if you are using instant loading since 108 | // it will not result in a page refresh in the browser 109 | // See `How to integrate with third-party JavaScript libraries` guideline: 110 | // https://squidfunk.github.io/mkdocs-material/customization/?h=javascript#additional-javascript 111 | document$.subscribe(function () { 112 | setCopyText(); 113 | showSponsorPopup(); 114 | }); 115 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | MQPy Logo 3 |
4 | 5 | !!! sponsor "Support MQPy Development" 6 | MQPy is a free and open-source project that needs your support to continue development! 7 | 8 | [💛 Become a Sponsor](https://github.com/sponsors/Joaopeuko){ .md-button .md-button--primary } 9 | 10 | # MQPy 11 | 12 | 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. 13 | 14 | !!! warning "Trading Risk Warning" 15 | **IMPORTANT: Trading involves substantial risk of loss and is not suitable for all investors.** 16 | 17 | - Always use a **demo account** with fake money when testing strategies 18 | - MQPy is provided for **educational purposes only** 19 | - Past performance is not indicative of future results 20 | - Never trade with money you cannot afford to lose 21 | - The developers are not responsible for any financial losses 22 | 23 | ## Examples and Strategies 24 | 25 | MQPy comes with a variety of [example strategies](examples.md) to help you get started, including: 26 | 27 | - Basic Moving Average Crossover 28 | - RSI-based trading 29 | - Bollinger Bands strategies 30 | - Fibonacci Retracement patterns 31 | - Multi-timeframe analysis 32 | - Custom indicator integration 33 | 34 | Check out the [Examples](examples.md) page to see all available strategies. 35 | 36 | ## Need Help or Found an Issue? 37 | 38 | If you need help or have found an issue, you can: 39 | 40 | - [Report a bug 🐛](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=fix.yaml&title=fix%3A+) 41 | - [Request documentation improvements 📚](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=docs.yaml&title=docs%3A+) 42 | - [Suggest a new feature ✨](https://github.com/Joaopeuko/Mql5-Python-Integration/issues/new?template=feat.yaml&title=feat%3A+) 43 | 44 | See the [Contributing](contributing.md) page for all available issue templates and more ways to contribute. 45 | -------------------------------------------------------------------------------- /docs/strategies/bollinger_bands.md: -------------------------------------------------------------------------------- 1 | # Bollinger Bands Trading Strategy 2 | 3 | !!! danger "Trading Risk Warning" 4 | **IMPORTANT: All examples should be tested using demo accounts only!** 5 | 6 | - Trading involves substantial risk of loss 7 | - These examples are for educational purposes only 8 | - Always test with fake money before using real funds 9 | 10 | ## Overview 11 | 12 | Bollinger Bands are a versatile technical indicator created by John Bollinger that consist of three lines: 13 | 14 | | Component | Description | 15 | |-----------|-------------| 16 | | **Middle Band** | A simple moving average (SMA) of the price | 17 | | **Upper Band** | The middle band plus a specific number of standard deviations (typically 2) | 18 | | **Lower Band** | The middle band minus the same number of standard deviations | 19 | 20 | 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: 21 | 22 | | Signal Type | Description | 23 | |-------------|-------------| 24 | | **Buy signal** | When price breaks below the lower band (suggesting the market is oversold) | 25 | | **Sell signal** | When price breaks above the upper band (suggesting the market is overbought) | 26 | 27 | ## Strategy Logic 28 | 29 | | Step | Description | 30 | |------|-------------| 31 | | 1 | Calculate the Bollinger Bands (middle, upper, and lower bands) | 32 | | 2 | Generate buy signals when price falls below the lower band | 33 | | 3 | Generate sell signals when price rises above the upper band | 34 | | 4 | Execute trades only during specified trading hours | 35 | 36 | ### Strategy Flow 37 | 38 | ```mermaid 39 | flowchart TD 40 | A[Start] --> B[Fetch Market Data] 41 | B --> C[Calculate Simple Moving Average] 42 | C --> D[Calculate Price Standard Deviation] 43 | D --> E["Calculate Upper Band:
SMA + 2×StdDev"] 44 | D --> F["Calculate Lower Band:
SMA - 2×StdDev"] 45 | E --> G{"Price > Upper
Band?"} 46 | F --> H{"Price < Lower
Band?"} 47 | G -->|Yes| I[Generate Sell Signal] 48 | G -->|No| J[No Sell Signal] 49 | H -->|Yes| K[Generate Buy Signal] 50 | H -->|No| L[No Buy Signal] 51 | I --> M{"Within Trading
Hours?"} 52 | J --> M 53 | K --> M 54 | L --> M 55 | M -->|Yes| N[Execute Trade] 56 | M -->|No| O[Skip Trade Execution] 57 | N --> P[Update Statistics] 58 | O --> P 59 | P --> Q[Wait for Next Tick] 60 | Q --> B 61 | ``` 62 | 63 | ## Code Implementation 64 | 65 | Let's break down the implementation step by step: 66 | 67 | ### Step 1: Required Imports 68 | 69 | ```python 70 | from __future__ import annotations 71 | 72 | import logging 73 | import numpy as np 74 | 75 | from mqpy.rates import Rates 76 | from mqpy.tick import Tick 77 | from mqpy.trade import Trade 78 | 79 | # Configure logging 80 | logging.basicConfig( 81 | level=logging.INFO, 82 | format='%(asctime)s - %(levelname)s - %(message)s' 83 | ) 84 | logger = logging.getLogger(__name__) 85 | ``` 86 | 87 | We import the necessary modules: 88 | - Core MQPy modules for trading operations and data access 89 | - `numpy` for efficient calculations of means and standard deviations 90 | - `logging` for detailed tracking of the strategy's operation 91 | 92 | ### Step 2: Bollinger Bands Calculation Function 93 | 94 | ```python 95 | def calculate_bollinger_bands(prices: list[float], period: int = 20, num_std_dev: float = 2.0) -> tuple[float, float, float] | None: 96 | """Calculate Bollinger Bands (middle, upper, lower).""" 97 | if len(prices) < period: 98 | return None 99 | 100 | # Convert to numpy array for vectorized calculations 101 | price_array = np.array(prices[-period:]) 102 | 103 | # Calculate SMA (middle band) 104 | sma = np.mean(price_array) 105 | 106 | # Calculate standard deviation 107 | std_dev = np.std(price_array) 108 | 109 | # Calculate upper and lower bands 110 | upper_band = sma + (num_std_dev * std_dev) 111 | lower_band = sma - (num_std_dev * std_dev) 112 | 113 | return (sma, upper_band, lower_band) 114 | ``` 115 | 116 | This function calculates the three components of the Bollinger Bands: 117 | 1. First, it checks if we have enough price data for the specified period 118 | 2. It converts the price data into a numpy array for more efficient calculations 119 | 3. It calculates the middle band, which is just the simple moving average (SMA) of the prices 120 | 4. It calculates the standard deviation of the prices over the specified period 121 | 5. It calculates the upper and lower bands by adding/subtracting the standard deviation (multiplied by a factor) from the middle band 122 | 6. It returns all three values as a tuple, or `None` if there's not enough data 123 | 124 | ### Step 3: Initialize the Trading Strategy 125 | 126 | ```python 127 | trade = Trade( 128 | expert_name="Bollinger Bands Strategy", 129 | version="1.0", 130 | symbol="EURUSD", 131 | magic_number=569, 132 | lot=0.1, 133 | stop_loss=50, 134 | emergency_stop_loss=150, 135 | take_profit=100, 136 | emergency_take_profit=300, 137 | start_time="9:15", 138 | finishing_time="17:30", 139 | ending_time="17:50", 140 | fee=0.5, 141 | ) 142 | ``` 143 | 144 | We configure our trading strategy with: 145 | - Identification parameters: name, version, magic number 146 | - Trading parameters: symbol, lot size 147 | - Risk management parameters: stop loss and take profit (notice the take profit is 2x the stop loss) 148 | - Trading session times: when to start, when to stop opening new positions, and when to close all positions 149 | 150 | ### Step 4: Set Strategy Parameters 151 | 152 | ```python 153 | # Strategy parameters 154 | prev_tick_time = 0 155 | bb_period = 20 156 | bb_std_dev = 2.0 157 | ``` 158 | 159 | The key parameters for our Bollinger Bands strategy are: 160 | - `bb_period`: The period for the calculation of the SMA and standard deviation (standard is 20) 161 | - `bb_std_dev`: The number of standard deviations for the bands (standard is 2.0) 162 | 163 | ### Step 5: Main Trading Loop 164 | 165 | ```python 166 | try: 167 | while True: 168 | # Prepare the symbol for trading 169 | trade.prepare_symbol() 170 | 171 | # Fetch tick and rates data 172 | current_tick = Tick(trade.symbol) 173 | historical_rates = Rates(trade.symbol, bb_period + 10, 0, 1) # Get extra data for reliability 174 | ``` 175 | 176 | In the main loop, we: 177 | - Prepare the symbol for trading 178 | - Get the current market price 179 | - Retrieve historical price data (we get bb_period + 10 bars for reliable calculations) 180 | 181 | ### Step 6: Calculate Bollinger Bands 182 | 183 | ```python 184 | # Only process if we have a new tick 185 | if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= bb_period: 186 | # Calculate Bollinger Bands 187 | bb_result = calculate_bollinger_bands( 188 | historical_rates.close, 189 | period=bb_period, 190 | num_std_dev=bb_std_dev 191 | ) 192 | 193 | if bb_result: 194 | middle_band, upper_band, lower_band = bb_result 195 | current_price = current_tick.last 196 | ``` 197 | 198 | For each new tick, we: 199 | - Check that it's different from the previous tick to avoid redundant calculations 200 | - Ensure we have enough historical data 201 | - Calculate the Bollinger Bands using our custom function 202 | - Extract the individual band values and the current price for signal generation 203 | 204 | ### Step 7: Generate Trading Signals 205 | 206 | ```python 207 | # Generate signals based on price position relative to bands 208 | # Buy when price crosses below lower band (potential bounce) 209 | is_buy_signal = current_price < lower_band 210 | 211 | # Sell when price crosses above upper band (potential reversal) 212 | is_sell_signal = current_price > upper_band 213 | 214 | # Log band data and signals 215 | logger.info(f"Current price: {current_price:.5f}") 216 | logger.info(f"Bollinger Bands - Middle: {middle_band:.5f}, Upper: {upper_band:.5f}, Lower: {lower_band:.5f}") 217 | 218 | if is_buy_signal: 219 | logger.info(f"Buy signal: Price ({current_price:.5f}) below lower band ({lower_band:.5f})") 220 | elif is_sell_signal: 221 | logger.info(f"Sell signal: Price ({current_price:.5f}) above upper band ({upper_band:.5f})") 222 | ``` 223 | 224 | The signal generation logic is based on price comparison with the bands: 225 | 1. We generate a buy signal when the current price falls below the lower band 226 | 2. We generate a sell signal when the current price rises above the upper band 227 | 3. We log the current values of price and bands, as well as any signals generated 228 | 229 | ### Step 8: Execute Trades 230 | 231 | ```python 232 | # Execute trading positions based on signals 233 | if trade.trading_time(): # Only trade during allowed hours 234 | trade.open_position( 235 | should_buy=is_buy_signal, 236 | should_sell=is_sell_signal, 237 | comment="Bollinger Bands Strategy" 238 | ) 239 | ``` 240 | 241 | When a signal is detected: 242 | - We check if we're within the allowed trading hours 243 | - If yes, we execute the appropriate trade based on our signals 244 | - The comment identifies the strategy in the trading terminal 245 | 246 | ### Step 9: Update State and Check for End of Day 247 | 248 | ```python 249 | # Update trading statistics 250 | trade.statistics() 251 | 252 | prev_tick_time = current_tick.time_msc 253 | 254 | # Check if it's the end of the trading day 255 | if trade.days_end(): 256 | trade.close_position("End of the trading day reached.") 257 | break 258 | ``` 259 | 260 | After processing each tick, we: 261 | - Update the trading statistics for monitoring 262 | - Store the current tick time for the next iteration 263 | - Check if it's the end of the trading day, and if so, close positions and exit 264 | 265 | ### Step 10: Error Handling 266 | 267 | ```python 268 | except KeyboardInterrupt: 269 | logger.info("Strategy execution interrupted by user.") 270 | trade.close_position("User interrupted the strategy.") 271 | except Exception as e: 272 | logger.error(f"Error in strategy execution: {e}") 273 | finally: 274 | logger.info("Finishing the program.") 275 | ``` 276 | 277 | Our error handling ensures: 278 | - Clean exit when the user interrupts the program 279 | - Logging of any errors that occur 280 | - Proper cleanup in the `finally` block 281 | 282 | ## Full Source Code 283 | 284 | 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). 285 | 286 | ## Optimization Opportunities 287 | 288 | This strategy can be improved by: 289 | 290 | | Improvement | Description | 291 | |-------------|-------------| 292 | | **Trend Filter** | Using a longer-term moving average to only take trades in the direction of the overall trend | 293 | | **Band Width Analysis** | Trading based on the width of the bands (narrowing and widening) to identify volatility changes | 294 | | **Band Touch Strategy** | Waiting for the price to return to the middle band after touching an outer band | 295 | | **Volume Confirmation** | Using volume information to confirm potential reversals | 296 | | **Dynamic Deviation** | Adjusting the number of standard deviations based on market volatility | 297 | 298 | ## Next Steps 299 | 300 | Try experimenting with: 301 | 302 | | Experiment | Options | 303 | |------------|---------| 304 | | Period Length | Shorter periods for more signals, longer periods for fewer but stronger signals | 305 | | Standard Deviation | Higher values (2.5 or 3.0) for fewer but more reliable signals | 306 | | Indicator Combinations | Combine with momentum indicators like RSI to confirm signals | 307 | | Entry Refinement | Wait for reversal candle patterns after price breaks a band before entering | 308 | -------------------------------------------------------------------------------- /docs/strategies/moving_average.md: -------------------------------------------------------------------------------- 1 | # Moving Average Crossover Strategy 2 | 3 | !!! danger "Trading Risk Warning" 4 | **IMPORTANT: All examples should be tested using demo accounts only!** 5 | 6 | - Trading involves substantial risk of loss 7 | - These examples are for educational purposes only 8 | - Always test with fake money before using real funds 9 | 10 | ## Overview 11 | 12 | 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: 13 | 14 | | Signal Type | Description | 15 | |-------------|-------------| 16 | | **Buy signal** | When the faster (shorter-period) moving average crosses above the slower (longer-period) moving average | 17 | | **Sell signal** | When the faster moving average crosses below the slower moving average | 18 | 19 | This strategy aims to identify potential trend changes in the market. 20 | 21 | ## Strategy Logic 22 | 23 | | Step | Description | 24 | |------|-------------| 25 | | 1 | Calculate two Simple Moving Averages (SMA): a short-period SMA and a long-period SMA | 26 | | 2 | Compare current and previous values of both SMAs to detect crossovers | 27 | | 3 | Generate buy signals when short SMA crosses above long SMA | 28 | | 4 | Generate sell signals when short SMA crosses below long SMA | 29 | | 5 | Execute trades only during specified trading hours | 30 | 31 | ### Strategy Flow 32 | 33 | ```mermaid 34 | flowchart TD 35 | A[Start] --> B[Fetch Market Data] 36 | B --> C[Calculate Short-Period SMA] 37 | B --> D[Calculate Long-Period SMA] 38 | C --> E[Compare Current & Previous SMAs] 39 | D --> E 40 | E --> F{"Short MA Crossed
Above Long MA?"} 41 | F -->|Yes| G[Generate Buy Signal] 42 | F -->|No| H{"Short MA Crossed
Below Long MA?"} 43 | H -->|Yes| I[Generate Sell Signal] 44 | H -->|No| J[No Trading Signal] 45 | G --> K{"Within Trading
Hours?"} 46 | I --> K 47 | J --> K 48 | K -->|Yes| L[Execute Trade] 49 | K -->|No| M[Skip Trade Execution] 50 | L --> N[Update Statistics] 51 | M --> N 52 | N --> O[Wait for Next Tick] 53 | O --> B 54 | ``` 55 | 56 | ## Code Implementation 57 | 58 | Let's break down the implementation step by step: 59 | 60 | ### Step 1: Required Imports 61 | 62 | ```python 63 | from __future__ import annotations 64 | 65 | import logging 66 | 67 | from mqpy.rates import Rates 68 | from mqpy.tick import Tick 69 | from mqpy.trade import Trade 70 | 71 | # Configure logging 72 | logging.basicConfig( 73 | level=logging.INFO, 74 | format='%(asctime)s - %(levelname)s - %(message)s' 75 | ) 76 | logger = logging.getLogger(__name__) 77 | ``` 78 | 79 | We import the necessary modules from MQPy: 80 | - `Rates`: For accessing historical price data 81 | - `Tick`: For accessing current market prices 82 | - `Trade`: For executing trading operations 83 | - `logging`: For keeping track of what the strategy is doing 84 | 85 | ### Step 2: Define SMA Calculation Function 86 | 87 | ```python 88 | def calculate_sma(prices: list[float], period: int) -> float | None: 89 | """Calculate Simple Moving Average.""" 90 | if len(prices) < period: 91 | return None 92 | return sum(prices[-period:]) / period 93 | ``` 94 | 95 | This function calculates a Simple Moving Average (SMA) from a list of prices: 96 | - It first checks if we have enough price data for the requested period 97 | - If not, it returns `None` 98 | - Otherwise, it calculates the average of the last `period` prices 99 | 100 | ### Step 3: Initialize the Trading Strategy 101 | 102 | ```python 103 | trade = Trade( 104 | expert_name="Moving Average Crossover", 105 | version="1.0", 106 | symbol="EURUSD", 107 | magic_number=567, 108 | lot=0.1, 109 | stop_loss=25, 110 | emergency_stop_loss=300, 111 | take_profit=25, 112 | emergency_take_profit=300, 113 | start_time="9:15", 114 | finishing_time="17:30", 115 | ending_time="17:50", 116 | fee=0.5, 117 | ) 118 | ``` 119 | 120 | Here we initialize the `Trade` object with our strategy parameters: 121 | - `expert_name`: The name of our trading strategy 122 | - `symbol`: The trading instrument (EURUSD in this case) 123 | - `magic_number`: A unique identifier for this strategy's trades 124 | - `lot`: The trading volume 125 | - `stop_loss`/`take_profit`: Risk management parameters in points 126 | - `emergency_stop_loss`/`emergency_take_profit`: Larger safety values if regular ones fail 127 | - `start_time`/`finishing_time`/`ending_time`: Define the trading session hours 128 | 129 | ### Step 4: Set Strategy Parameters 130 | 131 | ```python 132 | # Strategy parameters 133 | prev_tick_time = 0 134 | short_period = 5 135 | long_period = 20 136 | 137 | # Variables to track previous state for crossover detection 138 | prev_short_ma = None 139 | prev_long_ma = None 140 | ``` 141 | 142 | We define the key parameters for our strategy: 143 | - `short_period`: The period for the fast moving average (5 bars) 144 | - `long_period`: The period for the slow moving average (20 bars) 145 | - We also initialize variables to track the previous MA values for crossover detection 146 | 147 | ### Step 5: Main Trading Loop 148 | 149 | ```python 150 | try: 151 | while True: 152 | # Prepare the symbol for trading 153 | trade.prepare_symbol() 154 | 155 | # Fetch tick and rates data 156 | current_tick = Tick(trade.symbol) 157 | historical_rates = Rates(trade.symbol, long_period + 10, 0, 1) # Get extra data for reliability 158 | ``` 159 | 160 | The main loop: 161 | - Prepares the symbol for trading 162 | - Gets the current market price via `Tick` 163 | - Retrieves historical price data via `Rates`. We request slightly more data (long_period + 10) for reliability 164 | 165 | ### Step 6: Calculate Moving Averages 166 | 167 | ```python 168 | # Only process if we have a new tick 169 | if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= long_period: 170 | # Calculate moving averages 171 | short_ma = calculate_sma(historical_rates.close, short_period) 172 | long_ma = calculate_sma(historical_rates.close, long_period) 173 | ``` 174 | 175 | For each new tick: 176 | - We check that it's different from the previous tick to avoid redundant calculations 177 | - We ensure we have enough historical data 178 | - We calculate both the short and long moving averages 179 | 180 | ### Step 7: Detect Crossovers 181 | 182 | ```python 183 | # Check if we have enough data for comparison 184 | if short_ma and long_ma and prev_short_ma and prev_long_ma: 185 | # Detect crossover (short MA crosses above long MA) 186 | cross_above = prev_short_ma <= prev_long_ma and short_ma > long_ma 187 | 188 | # Detect crossunder (short MA crosses below long MA) 189 | cross_below = prev_short_ma >= prev_long_ma and short_ma < long_ma 190 | 191 | # Log crossover events 192 | if cross_above: 193 | logger.info(f"Bullish crossover detected: Short MA ({short_ma:.5f}) crossed above Long MA ({long_ma:.5f})") 194 | elif cross_below: 195 | logger.info(f"Bearish crossover detected: Short MA ({short_ma:.5f}) crossed below Long MA ({long_ma:.5f})") 196 | ``` 197 | 198 | To detect crossovers, we need both current and previous MA values: 199 | - `cross_above`: Occurs when the short MA was below (or equal to) the long MA in the previous tick, but is now above it 200 | - `cross_below`: Occurs when the short MA was above (or equal to) the long MA in the previous tick, but is now below it 201 | - We log these events for monitoring the strategy 202 | 203 | ### Step 8: Execute Trades 204 | 205 | ```python 206 | # Execute trading positions based on signals 207 | if trade.trading_time(): # Only trade during allowed hours 208 | trade.open_position( 209 | should_buy=cross_above, 210 | should_sell=cross_below, 211 | comment="Moving Average Crossover Strategy" 212 | ) 213 | ``` 214 | 215 | When a signal is detected: 216 | - We first check if we're within the allowed trading hours using `trade.trading_time()` 217 | - If yes, we call `open_position()` with our buy/sell signals 218 | - The `comment` parameter helps identify the strategy in the trading terminal 219 | 220 | ### Step 9: Update State and Check End of Day 221 | 222 | ```python 223 | # Update previous MA values for next comparison 224 | prev_short_ma = short_ma 225 | prev_long_ma = long_ma 226 | 227 | # Update trading statistics periodically 228 | trade.statistics() 229 | 230 | prev_tick_time = current_tick.time_msc 231 | 232 | # Check if it's the end of the trading day 233 | if trade.days_end(): 234 | trade.close_position("End of the trading day reached.") 235 | break 236 | ``` 237 | 238 | After processing each tick: 239 | - We update the previous MA values for the next iteration 240 | - We update trading statistics for monitoring 241 | - We update the previous tick time 242 | - We check if it's the end of the trading day, and if so, close positions and exit 243 | 244 | ### Step 10: Error Handling 245 | 246 | ```python 247 | except KeyboardInterrupt: 248 | logger.info("Strategy execution interrupted by user.") 249 | trade.close_position("User interrupted the strategy.") 250 | except Exception as e: 251 | logger.error(f"Error in strategy execution: {e}") 252 | finally: 253 | logger.info("Finishing the program.") 254 | ``` 255 | 256 | Proper error handling ensures: 257 | - Clean exit when the user interrupts the program 258 | - Logging of any errors that occur 259 | - Proper cleanup in the `finally` block 260 | 261 | ## Full Source Code 262 | 263 | 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). 264 | 265 | ## Backtesting and Optimization 266 | 267 | This strategy can be improved by: 268 | 269 | | Improvement | Description | 270 | |-------------|-------------| 271 | | Period Optimization | Finding the optimal MA periods for specific instruments | 272 | | Filter Addition | Adding filters to avoid false signals in ranging markets | 273 | | Position Sizing | Implementing dynamic position sizing based on market volatility | 274 | | Stop Management | Adding trailing stop-loss to secure profits as the trend develops | 275 | 276 | ## Next Steps 277 | 278 | Try experimenting with different: 279 | 280 | | Experiment | Options | 281 | |------------|---------| 282 | | MA Periods | Try pairs like 9 and 21, or 50 and 200 for different timeframes | 283 | | MA Types | Test Exponential, Weighted, or other MA types for potential improvements | 284 | | Instruments | Apply the strategy to various forex pairs, stocks, or commodities | 285 | | Timeframes | Scale from M1 (1-minute) to D1 (daily) charts for different trading styles | 286 | -------------------------------------------------------------------------------- /docs/strategies/rsi_strategy.md: -------------------------------------------------------------------------------- 1 | # RSI Trading Strategy 2 | 3 | !!! danger "Trading Risk Warning" 4 | **IMPORTANT: All examples should be tested using demo accounts only!** 5 | 6 | - Trading involves substantial risk of loss 7 | - These examples are for educational purposes only 8 | - Always test with fake money before using real funds 9 | 10 | ## Overview 11 | 12 | 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. 13 | 14 | | Signal Type | Description | 15 | |-------------|-------------| 16 | | **Buy signal** | When RSI falls below the oversold threshold (typically 30) | 17 | | **Sell signal** | When RSI rises above the overbought threshold (typically 70) | 18 | 19 | ## Strategy Logic 20 | 21 | | Step | Description | 22 | |------|-------------| 23 | | 1 | Calculate the RSI indicator using price data | 24 | | 2 | Generate buy signals when RSI falls below the oversold threshold | 25 | | 3 | Generate sell signals when RSI rises above the overbought threshold | 26 | | 4 | Execute trades only during specified trading hours | 27 | 28 | ### Strategy Flow 29 | 30 | ```mermaid 31 | flowchart TD 32 | A[Start] --> B[Fetch Market Data] 33 | B --> C[Calculate Price Changes] 34 | C --> D[Separate Gains and Losses] 35 | D --> E[Calculate Average Gain and Loss] 36 | E --> F[Calculate RSI Value] 37 | F --> G{"RSI < Oversold
Threshold?"} 38 | G -->|Yes| H[Generate Buy Signal] 39 | G -->|No| I{"RSI > Overbought
Threshold?"} 40 | I -->|Yes| J[Generate Sell Signal] 41 | I -->|No| K[No Trading Signal] 42 | H --> L{"Within Trading
Hours?"} 43 | J --> L 44 | K --> L 45 | L -->|Yes| M[Execute Trade] 46 | L -->|No| N[Skip Trade Execution] 47 | M --> O[Update Statistics] 48 | N --> O 49 | O --> P[Wait for Next Tick] 50 | P --> B 51 | ``` 52 | 53 | ## Code Implementation 54 | 55 | Let's break down the implementation step by step: 56 | 57 | ### Step 1: Required Imports 58 | 59 | ```python 60 | from __future__ import annotations 61 | 62 | import logging 63 | import numpy as np 64 | 65 | from mqpy.rates import Rates 66 | from mqpy.tick import Tick 67 | from mqpy.trade import Trade 68 | 69 | # Configure logging 70 | logging.basicConfig( 71 | level=logging.INFO, 72 | format='%(asctime)s - %(levelname)s - %(message)s' 73 | ) 74 | logger = logging.getLogger(__name__) 75 | ``` 76 | 77 | We import the necessary modules: 78 | - Core MQPy modules for trading and data access 79 | - `numpy` for efficient calculations in our RSI function 80 | - `logging` for tracking the strategy's operation 81 | 82 | ### Step 2: RSI Calculation Function 83 | 84 | ```python 85 | def calculate_rsi(prices: list[float], period: int = 14) -> float | None: 86 | """Calculate the Relative Strength Index.""" 87 | if len(prices) < period + 1: 88 | return None 89 | 90 | # Calculate price changes 91 | deltas = np.diff(prices) 92 | 93 | # Separate gains and losses 94 | gains = np.where(deltas > 0, deltas, 0) 95 | losses = np.where(deltas < 0, -deltas, 0) 96 | 97 | # Calculate initial average gain and loss 98 | avg_gain = np.mean(gains[:period]) 99 | avg_loss = np.mean(losses[:period]) 100 | 101 | # Avoid division by zero 102 | if avg_loss == 0: 103 | return 100 104 | 105 | # Calculate RS and RSI 106 | rs = avg_gain / avg_loss 107 | rsi = 100 - (100 / (1 + rs)) 108 | 109 | return rsi 110 | ``` 111 | 112 | This function implements the RSI formula: 113 | 1. First, it checks if we have enough price data (at least period + 1 values) 114 | 2. It calculates price changes between consecutive closes using `np.diff()` 115 | 3. It separates the price changes into gains (positive changes) and losses (negative changes) 116 | 4. It calculates the average gain and average loss over the specified period 117 | 5. It computes the Relative Strength (RS) as the ratio of average gain to average loss 118 | 6. Finally, it converts RS to RSI using the formula: RSI = 100 - (100 / (1 + RS)) 119 | 120 | ### Step 3: Initialize the Trading Strategy 121 | 122 | ```python 123 | trade = Trade( 124 | expert_name="RSI Strategy", 125 | version="1.0", 126 | symbol="EURUSD", 127 | magic_number=568, 128 | lot=0.1, 129 | stop_loss=30, 130 | emergency_stop_loss=90, 131 | take_profit=60, 132 | emergency_take_profit=180, 133 | start_time="9:15", 134 | finishing_time="17:30", 135 | ending_time="17:50", 136 | fee=0.5, 137 | ) 138 | ``` 139 | 140 | We configure our trading strategy with: 141 | - Identification parameters: name, version, magic number 142 | - Trading parameters: symbol, lot size 143 | - Risk management parameters: stop loss and take profit (notice the take profit is 2x the stop loss) 144 | - Trading session times: when to start, when to stop opening new positions, and when to close all positions 145 | 146 | ### Step 4: Set Strategy Parameters 147 | 148 | ```python 149 | # Strategy parameters 150 | prev_tick_time = 0 151 | rsi_period = 14 152 | overbought_threshold = 70 153 | oversold_threshold = 30 154 | ``` 155 | 156 | The key parameters for our RSI strategy are: 157 | - `rsi_period`: The number of periods for RSI calculation (standard is 14) 158 | - `overbought_threshold`: The RSI level above which we consider the market overbought (70) 159 | - `oversold_threshold`: The RSI level below which we consider the market oversold (30) 160 | 161 | ### Step 5: Main Trading Loop 162 | 163 | ```python 164 | try: 165 | while True: 166 | # Prepare the symbol for trading 167 | trade.prepare_symbol() 168 | 169 | # Fetch tick and rates data 170 | current_tick = Tick(trade.symbol) 171 | historical_rates = Rates(trade.symbol, rsi_period + 20, 0, 1) # Get extra data for reliability 172 | ``` 173 | 174 | In the main loop, we: 175 | - Prepare the symbol for trading 176 | - Get the current market price 177 | - Retrieve historical price data (we get rsi_period + 20 bars for reliable calculations) 178 | 179 | ### Step 6: Calculate RSI and Generate Signals 180 | 181 | ```python 182 | # Only process if we have a new tick and enough data for RSI calculation 183 | if current_tick.time_msc != prev_tick_time and len(historical_rates.close) >= rsi_period + 1: 184 | # Calculate RSI 185 | rsi_value = calculate_rsi(historical_rates.close, rsi_period) 186 | 187 | if rsi_value is not None: 188 | # Generate signals based on RSI thresholds 189 | is_buy_signal = rsi_value < oversold_threshold 190 | is_sell_signal = rsi_value > overbought_threshold 191 | 192 | # Log RSI values and signals 193 | if is_buy_signal: 194 | logger.info(f"Oversold condition: RSI = {rsi_value:.2f} (< {oversold_threshold})") 195 | elif is_sell_signal: 196 | logger.info(f"Overbought condition: RSI = {rsi_value:.2f} (> {overbought_threshold})") 197 | else: 198 | logger.debug(f"Current RSI: {rsi_value:.2f}") 199 | ``` 200 | 201 | For each new tick, we: 202 | 1. Calculate the RSI value using our custom function 203 | 2. Generate trading signals based on simple threshold comparisons: 204 | - Buy when RSI < oversold threshold (30) 205 | - Sell when RSI > overbought threshold (70) 206 | 3. Log the RSI values and any signals generated for monitoring 207 | 208 | ### Step 7: Execute Trades 209 | 210 | ```python 211 | # Execute trading positions based on signals during allowed trading hours 212 | if trade.trading_time(): 213 | trade.open_position( 214 | should_buy=is_buy_signal, 215 | should_sell=is_sell_signal, 216 | comment=f"RSI Strategy: {rsi_value:.2f}" 217 | ) 218 | ``` 219 | 220 | When a signal is detected: 221 | - We check if we're within the allowed trading hours 222 | - If yes, we execute the appropriate trade based on our signals 223 | - The comment includes the RSI value for reference in the trading terminal 224 | 225 | ### Step 8: Update State and Check for End of Day 226 | 227 | ```python 228 | # Update trading statistics periodically 229 | trade.statistics() 230 | 231 | prev_tick_time = current_tick.time_msc 232 | 233 | # Check if it's the end of the trading day 234 | if trade.days_end(): 235 | trade.close_position("End of the trading day reached.") 236 | break 237 | ``` 238 | 239 | After processing each tick, we: 240 | - Update the trading statistics for monitoring 241 | - Store the current tick time for the next iteration 242 | - Check if it's the end of the trading day, and if so, close positions and exit 243 | 244 | ### Step 9: Error Handling 245 | 246 | ```python 247 | except KeyboardInterrupt: 248 | logger.info("Strategy execution interrupted by user.") 249 | trade.close_position("User interrupted the strategy.") 250 | except Exception as e: 251 | logger.error(f"Error in strategy execution: {e}") 252 | finally: 253 | logger.info("Finishing the program.") 254 | ``` 255 | 256 | Our error handling ensures: 257 | - Proper handling of user interruptions 258 | - Logging of any errors that occur 259 | - Clean program termination in the `finally` block 260 | 261 | ## Full Source Code 262 | 263 | 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). 264 | 265 | ## Optimization Opportunities 266 | 267 | This strategy can be improved by: 268 | 269 | | Improvement | Description | 270 | |-------------|-------------| 271 | | **Smoothing** | Using a smoothed RSI or applying an additional moving average to filter out noise | 272 | | **Trend Filters** | Only taking trades in the direction of the longer-term trend | 273 | | **Divergence** | Looking for divergence between price and RSI for stronger signals | 274 | | **Dynamic Thresholds** | Adjusting the overbought/oversold thresholds based on market volatility | 275 | | **Position Management** | Taking partial profits when RSI reaches extreme levels | 276 | 277 | ## Next Steps 278 | 279 | Try experimenting with: 280 | 281 | | Experiment | Options | 282 | |------------|---------| 283 | | RSI Periods | Shorter periods (9) for more signals, longer periods (21) for fewer but stronger signals | 284 | | Threshold Levels | Test different levels like 20/80 for stronger signals but fewer trades | 285 | | Complementary Indicators | Add moving averages or other oscillators to confirm RSI signals | 286 | | Position Sizing | Implement different sizing based on the distance of RSI from thresholds | 287 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: MQPy 2 | repo_url: https://github.com/Joaopeuko/Mql5-Python-Integration 3 | repo_name: MQPy 4 | 5 | # Enable GitHub repository statistics 6 | edit_uri: "" # Disable edit button by setting to empty string 7 | # Settings for GitHub stars and forks display 8 | extra: 9 | social: 10 | - icon: fontawesome/brands/github 11 | link: https://github.com/Joaopeuko/Mql5-Python-Integration 12 | name: GitHub 13 | # analytics: 14 | # provider: google 15 | # property: !ENV GOOGLE_ANALYTICS_KEY 16 | status: 17 | new: Recently added 18 | deprecated: Deprecated 19 | markdown_extensions: 20 | - pymdownx.snippets 21 | - pymdownx.tasklist: 22 | custom_checkbox: true 23 | - pymdownx.superfences: 24 | custom_fences: 25 | - name: mermaid 26 | class: mermaid 27 | - admonition 28 | - pymdownx.details 29 | - toc: 30 | permalink: "#" 31 | - pymdownx.magiclink: 32 | repo_url_shorthand: true 33 | user: Joaopeuko 34 | repo: Mql5-Python-Integration 35 | - attr_list: 36 | - md_in_html: 37 | - pymdownx.highlight: 38 | anchor_linenums: true 39 | - pymdownx.inlinehilite: 40 | - markdown.extensions.attr_list: 41 | - pymdownx.keys: 42 | - pymdownx.tabbed: 43 | alternate_style: true 44 | 45 | theme: 46 | name: material 47 | favicon: assets/favicon.svg 48 | logo: assets/logo.svg 49 | features: 50 | - navigation.instant 51 | - navigation.instant.prefetch 52 | - navigation.instant.progress 53 | - navigation.sections 54 | - navigation.indexes 55 | - navigation.tracking 56 | - content.code.annotate 57 | - toc.follow 58 | - navigation.footer 59 | - navigation.top 60 | - content.code.copy 61 | - content.tabs.link 62 | - content.action.edit 63 | - content.action.view 64 | - content.tooltips 65 | # Enable GitHub repository statistics 66 | - content.action.edit 67 | - content.action.view 68 | - navigation.footer 69 | # Shows the GitHub icon with repository stats 70 | - header.autohide 71 | icon: 72 | repo: fontawesome/brands/github 73 | palette: 74 | - media: "(prefers-color-scheme)" 75 | toggle: 76 | icon: material/brightness-auto 77 | name: Switch to light mode 78 | - media: "(prefers-color-scheme: light)" 79 | scheme: astral-light 80 | toggle: 81 | icon: material/brightness-7 82 | name: Switch to dark mode 83 | - media: "(prefers-color-scheme: dark)" 84 | scheme: astral-dark 85 | toggle: 86 | icon: material/brightness-4 87 | name: Switch to system preference 88 | extra_css: 89 | - extra.css 90 | extra_javascript: 91 | - extra.js 92 | plugins: 93 | - search 94 | - gen-files: 95 | scripts: 96 | - scripts/gen_ref_pages.py 97 | - literate-nav: 98 | nav_file: SUMMARY.md 99 | - section-index 100 | - mkdocstrings: 101 | default_handler: python 102 | handlers: 103 | python: 104 | paths: [.] 105 | options: 106 | show_root_heading: false 107 | show_root_full_path: false 108 | show_object_full_path: false 109 | show_category_heading: false 110 | show_if_no_docstring: false 111 | show_source: true 112 | show_bases: true 113 | show_signature: true 114 | heading_level: 2 115 | members_order: source 116 | docstring_style: google 117 | docstring_section_style: table 118 | separate_signature: true 119 | merge_init_into_class: true 120 | show_submodules: false 121 | filters: ["!^_[^_]", "!^__init__"] 122 | show_inherited_members: false 123 | annotations_path: source 124 | docstring_options: 125 | ignore_init_summary: true 126 | line_length: 80 127 | show_root_members_full_path: false 128 | show_module_member_docstring: true 129 | - mkdocs-jupyter 130 | nav: 131 | - Home: index.md 132 | - Contributing: contributing.md 133 | - Examples: examples.md 134 | - Strategy Explanations: 135 | - Moving Average Crossover: strategies/moving_average.md 136 | - RSI Strategy: strategies/rsi_strategy.md 137 | - Bollinger Bands Strategy: strategies/bollinger_bands.md 138 | - Fibonacci Retracement Strategy: strategies/fibonacci_retracement.md 139 | - Market Depth Analysis: strategies/market_depth_analysis.md 140 | - Code documentation: reference/ 141 | -------------------------------------------------------------------------------- /mqpy/__init__.py: -------------------------------------------------------------------------------- 1 | """Python integration package. 2 | 3 | This package provides a bridge between Python and MetaTrader 5, allowing users to 4 | create Expert Advisors and implement trading strategies in Python. 5 | """ 6 | 7 | from mqpy.logger import get_logger 8 | 9 | __all__ = ["get_logger"] 10 | -------------------------------------------------------------------------------- /mqpy/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point for the MQL5-Python integration package.""" 2 | 3 | from .template import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /mqpy/book.py: -------------------------------------------------------------------------------- 1 | """Module for managing a market book for a financial instrument. 2 | 3 | Provides the Book class for accessing market depth information. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import Any 9 | 10 | import MetaTrader5 as Mt5 11 | 12 | from mqpy.logger import get_logger 13 | 14 | # Configure logging 15 | logger = get_logger(__name__) 16 | 17 | 18 | class Book: 19 | """Represents a market book for a financial instrument.""" 20 | 21 | def __init__(self, symbol: str) -> None: 22 | """Initialize a Book object. 23 | 24 | Args: 25 | symbol (str): The financial instrument symbol. 26 | 27 | Returns: 28 | None 29 | """ 30 | self.symbol: str = symbol 31 | if Mt5.market_book_add(self.symbol): 32 | logger.info(f"The symbol {self.symbol} was successfully added to the market book.") 33 | else: 34 | logger.error(f"Error adding {self.symbol} to the market book. Error: {Mt5.last_error()}") 35 | 36 | def get(self) -> list[Any] | None: 37 | """Get the market book for the financial instrument. 38 | 39 | Returns: 40 | list[Any] | None: The market book data if successful, None otherwise. 41 | """ 42 | return Mt5.market_book_get(self.symbol) 43 | 44 | def release(self) -> bool: 45 | """Release the market book for the financial instrument. 46 | 47 | Returns: 48 | bool: True if successful, False otherwise. 49 | """ 50 | result = Mt5.market_book_release(self.symbol) 51 | return False if result is None else result 52 | -------------------------------------------------------------------------------- /mqpy/logger.py: -------------------------------------------------------------------------------- 1 | """Logging configuration for the MqPy application. 2 | 3 | This module provides a centralized logging configuration for the entire application. 4 | Import this module instead of directly importing the logging module to ensure consistent 5 | logging behavior across the application. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | import sys 12 | 13 | 14 | def get_logger(name: str, level: int | None = None) -> logging.Logger: 15 | """Get a logger with the specified name and level. 16 | 17 | Args: 18 | name (str): The name of the logger, typically __name__. 19 | level (int | None): The logging level. Defaults to INFO if None. 20 | 21 | Returns: 22 | logging.Logger: A configured logger instance. 23 | """ 24 | logger = logging.getLogger(name) 25 | 26 | # Only configure the logger if it doesn't already have handlers 27 | if not logger.handlers: 28 | # Set default level if not specified 29 | if level is None: 30 | level = logging.INFO 31 | 32 | logger.setLevel(level) 33 | 34 | # Create console handler with a specific format 35 | console_handler = logging.StreamHandler(sys.stdout) 36 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 37 | console_handler.setFormatter(formatter) 38 | logger.addHandler(console_handler) 39 | 40 | return logger 41 | 42 | 43 | # Configure the root logger 44 | root_logger = logging.getLogger() 45 | if not root_logger.handlers: 46 | root_logger.setLevel(logging.WARNING) 47 | console_handler = logging.StreamHandler(sys.stdout) 48 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 49 | console_handler.setFormatter(formatter) 50 | root_logger.addHandler(console_handler) 51 | -------------------------------------------------------------------------------- /mqpy/rates.py: -------------------------------------------------------------------------------- 1 | """Module for retrieving and managing historical price data from MetaTrader 5. 2 | 3 | Provides the Rates class for accessing historical price information. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import Any 9 | 10 | import MetaTrader5 as Mt5 11 | 12 | 13 | class Rates: 14 | """Represents historical price data for a financial instrument.""" 15 | 16 | def __init__(self, symbol: str, time_frame: int, start_position: int, count: int) -> None: 17 | """Initializes a Rates object. 18 | 19 | Args: 20 | symbol (str): The financial instrument symbol. 21 | time_frame (int): The time frame for the rates. 22 | start_position (int): The starting position for the rates. 23 | count (int): The number of rates to retrieve. 24 | 25 | Returns: 26 | None 27 | """ 28 | 29 | def _raise_value_error(msg: str) -> None: 30 | raise ValueError(msg) 31 | 32 | try: 33 | rates = Mt5.copy_rates_from_pos(symbol, time_frame, start_position, count) 34 | if rates is None: 35 | _raise_value_error(f"Failed to retrieve rates for {symbol}") 36 | 37 | self._time = [rate[0] for rate in rates] 38 | self._open = [rate[1] for rate in rates] 39 | self._high = [rate[2] for rate in rates] 40 | self._low = [rate[3] for rate in rates] 41 | self._close = [rate[4] for rate in rates] 42 | self._tick_volume = [rate[5] for rate in rates] 43 | self._spread = [rate[6] for rate in rates] 44 | self._real_volume = [rate[7] for rate in rates] 45 | except Exception as e: 46 | raise ValueError(f"Failed to create Rates object for symbol {symbol}") from e 47 | 48 | @property 49 | def time(self) -> list[Any]: 50 | """List of timestamps.""" 51 | return self._time 52 | 53 | @property 54 | def open(self) -> list[float]: 55 | """List of open prices.""" 56 | return self._open 57 | 58 | @property 59 | def high(self) -> list[float]: 60 | """List of high prices.""" 61 | return self._high 62 | 63 | @property 64 | def low(self) -> list[float]: 65 | """List of low prices.""" 66 | return self._low 67 | 68 | @property 69 | def close(self) -> list[float]: 70 | """List of close prices.""" 71 | return self._close 72 | 73 | @property 74 | def tick_volume(self) -> list[int | float]: 75 | """List of tick volumes.""" 76 | return self._tick_volume 77 | 78 | @property 79 | def spread(self) -> list[int | float]: 80 | """List of spreads.""" 81 | return self._spread 82 | 83 | @property 84 | def real_volume(self) -> list[int | float]: 85 | """List of real volumes.""" 86 | return self._real_volume 87 | -------------------------------------------------------------------------------- /mqpy/tick.py: -------------------------------------------------------------------------------- 1 | """Module for retrieving and managing real-time tick data from MetaTrader 5. 2 | 3 | Provides the Tick class for accessing current market price information. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import MetaTrader5 as Mt5 9 | 10 | 11 | class Tick: 12 | """Represents real-time tick data for a financial instrument.""" 13 | 14 | def __init__(self, symbol: str) -> None: 15 | """Initializes a Tick object. 16 | 17 | Args: 18 | symbol (str): The financial instrument symbol. 19 | 20 | Returns: 21 | None 22 | """ 23 | tick_info = Mt5.symbol_info_tick(symbol) 24 | 25 | self._symbol = symbol 26 | self._time = tick_info.time 27 | self._bid = tick_info.bid 28 | self._ask = tick_info.ask 29 | self._last = tick_info.last 30 | self._volume = tick_info.volume 31 | self._time_msc = tick_info.time_msc 32 | self._flags = tick_info.flags 33 | self._volume_real = tick_info.volume_real 34 | 35 | @property 36 | def symbol(self) -> str: 37 | """The financial instrument symbol.""" 38 | return self._symbol 39 | 40 | @property 41 | def time(self) -> int: 42 | """Timestamp of the tick data.""" 43 | return self._time 44 | 45 | @property 46 | def bid(self) -> float: 47 | """Current bid price.""" 48 | return self._bid 49 | 50 | @property 51 | def ask(self) -> float: 52 | """Current ask price.""" 53 | return self._ask 54 | 55 | @property 56 | def last(self) -> float: 57 | """Last traded price.""" 58 | return self._last 59 | 60 | @property 61 | def volume(self) -> int: 62 | """Tick volume.""" 63 | return self._volume 64 | 65 | @property 66 | def time_msc(self) -> int: 67 | """Timestamp in milliseconds.""" 68 | return self._time_msc 69 | 70 | @property 71 | def flags(self) -> int: 72 | """Flags indicating tick data attributes.""" 73 | return self._flags 74 | 75 | @property 76 | def volume_real(self) -> float | None: 77 | """Real volume (if available).""" 78 | return self._volume_real 79 | 80 | def real_volume(self) -> list[int]: 81 | """List of real volumes.""" 82 | return self._real_volume 83 | -------------------------------------------------------------------------------- /mqpy/utilities.py: -------------------------------------------------------------------------------- 1 | """Utility module for MetaTrader 5 integration. 2 | 3 | Provides helper functions and classes for trading operations. 4 | """ 5 | 6 | from datetime import datetime, timezone 7 | 8 | import MetaTrader5 as Mt5 9 | 10 | from mqpy.logger import get_logger 11 | 12 | # Configure logging 13 | logger = get_logger(__name__) 14 | 15 | 16 | class Utilities: 17 | """A utility class for handling trading-related functionalities.""" 18 | 19 | def __init__(self) -> None: 20 | """Initialize the Utilities class.""" 21 | # Variables for minutes_counter 22 | self.__minutes_counter: int = 0 23 | self.__counter_flag: bool = True 24 | self.__allowed_to_trade: bool = True 25 | self.__allow_to_count: bool = False 26 | self.__recent_trade: bool = False 27 | 28 | def check_trade_availability(self, symbol: str, count_until: int) -> bool: 29 | """Check if trading is allowed based on specified conditions. 30 | 31 | Args: 32 | symbol (str): The financial instrument symbol. 33 | count_until (int): The number of minutes until trading is allowed. 34 | 35 | Returns: 36 | bool: True if trading is allowed, False otherwise. 37 | """ 38 | if len(Mt5.positions_get(symbol=symbol)) == 1: 39 | self.__recent_trade = True 40 | 41 | if len(Mt5.positions_get(symbol=symbol)) != 1 and self.__recent_trade and not self.__allow_to_count: 42 | self.__allow_to_count = True 43 | self.__allowed_to_trade = False 44 | 45 | if datetime.now(timezone.utc).second == 0 and self.__counter_flag and self.__allow_to_count: 46 | logger.info(f"Trading will be allowed in {count_until - self.__minutes_counter} minutes.") 47 | self.__minutes_counter += 1 48 | self.__counter_flag = False 49 | 50 | if datetime.now(timezone.utc).second == 59: 51 | self.__counter_flag = True 52 | 53 | if self.__minutes_counter == count_until: 54 | logger.info("Trading is allowed.") 55 | self.__reset_counters() 56 | 57 | return self.__allowed_to_trade 58 | 59 | def __reset_counters(self) -> None: 60 | """Reset counters after trading is allowed.""" 61 | self.__minutes_counter = 0 62 | self.__counter_flag = True 63 | self.__allow_to_count = False 64 | self.__allowed_to_trade = True 65 | 66 | # Test-only methods 67 | def _test_get_minutes_counter(self) -> int: 68 | """Get the minutes counter (for testing only).""" 69 | logger.warning("This method is for testing purposes only and should not be used in production code.") 70 | return self.__minutes_counter 71 | 72 | def _test_get_counter_flag(self) -> bool: 73 | """Get the counter flag (for testing only).""" 74 | logger.warning("This method is for testing purposes only and should not be used in production code.") 75 | return self.__counter_flag 76 | 77 | def _test_get_allowed_to_trade(self) -> bool: 78 | """Get the allowed to trade flag (for testing only).""" 79 | logger.warning("This method is for testing purposes only and should not be used in production code.") 80 | return self.__allowed_to_trade 81 | 82 | def _test_get_allow_to_count(self) -> bool: 83 | """Get the allow to count flag (for testing only).""" 84 | logger.warning("This method is for testing purposes only and should not be used in production code.") 85 | return self.__allow_to_count 86 | 87 | def _test_get_recent_trade(self) -> bool: 88 | """Get the recent trade flag (for testing only).""" 89 | logger.warning("This method is for testing purposes only and should not be used in production code.") 90 | return self.__recent_trade 91 | 92 | def _test_set_minutes_counter(self, value: int) -> None: 93 | """Set the minutes counter (for testing only).""" 94 | logger.warning("This method is for testing purposes only and should not be used in production code.") 95 | self.__minutes_counter = value 96 | 97 | def _test_set_counter_flag(self, value: bool) -> None: 98 | """Set the counter flag (for testing only).""" 99 | logger.warning("This method is for testing purposes only and should not be used in production code.") 100 | self.__counter_flag = value 101 | 102 | def _test_set_allowed_to_trade(self, value: bool) -> None: 103 | """Set the allowed to trade flag (for testing only).""" 104 | logger.warning("This method is for testing purposes only and should not be used in production code.") 105 | self.__allowed_to_trade = value 106 | 107 | def _test_set_allow_to_count(self, value: bool) -> None: 108 | """Set the allow to count flag (for testing only).""" 109 | logger.warning("This method is for testing purposes only and should not be used in production code.") 110 | self.__allow_to_count = value 111 | 112 | def _test_set_recent_trade(self, value: bool) -> None: 113 | """Set the recent trade flag (for testing only).""" 114 | logger.warning("This method is for testing purposes only and should not be used in production code.") 115 | self.__recent_trade = value 116 | 117 | def _test_reset_counters(self) -> None: 118 | """Reset counters (for testing only).""" 119 | logger.warning("This method is for testing purposes only and should not be used in production code.") 120 | self.__reset_counters() 121 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.0", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "mqpy" 10 | authors = [ 11 | {email = "joao@example.com", name = "Joao Euko"}, 12 | ] 13 | version = "0.6.11" 14 | 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." 15 | requires-python = ">=3.8" 16 | dependencies = [] 17 | 18 | readme = "README.md" 19 | license = {text = "MIT"} 20 | 21 | [project.scripts] 22 | mqpy = "mqpy.__main__:main" 23 | 24 | [project.optional-dependencies] 25 | dev = [ 26 | "pre-commit>=4.0.1", 27 | "pylint>=3.3.3", 28 | "pytest>=8.3.4", 29 | "pytest-cov>=6.0.0", 30 | ] 31 | docs = [ 32 | "mkdocs>=1.6.1", 33 | "mkdocs-gen-files>=0.5.0", 34 | "mkdocs-jupyter>=0.25.1", 35 | "mkdocs-literate-nav>=0.6.2", 36 | "mkdocs-material>=9.6.12", 37 | "mkdocs-section-index>=0.3.10", 38 | "mkdocstrings[python]>=0.29.1", 39 | ] 40 | 41 | [tool.setuptools] 42 | packages = ["mqpy"] 43 | 44 | [tool.semantic_release] 45 | version_variable = [ 46 | "mqpy/version.py:__version__", 47 | ] 48 | version_toml = [ 49 | "pyproject.toml:project.version", 50 | ] 51 | commit_message = "chore(release): v{version}" 52 | 53 | [tool.semantic_release.changelog] 54 | retain_old_entries = true 55 | 56 | # pre-commit 57 | [tool.pytest.ini_options] 58 | markers = [] 59 | 60 | [tool.mypy] 61 | warn_unused_configs = true 62 | ignore_missing_imports = true 63 | disable_error_code = [ 64 | "attr-defined", 65 | "name-defined", 66 | "assignment", 67 | "return-value", 68 | "arg-type", 69 | "index", 70 | "misc", 71 | "operator" 72 | ] 73 | files = "**/*.py" 74 | exclude = [ 75 | "venv", 76 | "mt5", 77 | "site-packages", 78 | "^build/", 79 | "^dist/" 80 | ] 81 | 82 | [[tool.mypy.overrides]] 83 | module = "MetaTrader5.*" 84 | ignore_errors = true 85 | disallow_untyped_defs = false 86 | disallow_incomplete_defs = false 87 | disallow_untyped_decorators = false 88 | disallow_any_generics = false 89 | disallow_untyped_calls = false 90 | check_untyped_defs = false 91 | 92 | [[tool.mypy.overrides]] 93 | module = "_virtualenv" 94 | ignore_errors = true 95 | disallow_untyped_defs = false 96 | disallow_incomplete_defs = false 97 | disallow_untyped_decorators = false 98 | disallow_any_generics = false 99 | disallow_untyped_calls = false 100 | check_untyped_defs = false 101 | 102 | [tool.ruff] 103 | line-length = 120 104 | # Enable Pyflakes `E` and `F` codes by default. 105 | lint.select = ["ALL"] 106 | lint.ignore = [ 107 | "COM812", # Missing trailing comma, conflicting with the formatter 108 | "ISC001", # Single line string concatenation, conflicting with the formatter that does it automatically 109 | "ANN002", # MissingTypeArgs 110 | "ANN003", # MissingTypeKwargs 111 | "ANN101", # MissingTypeSelf 112 | "EM101", # Exception must not use a string literal, assign to variable first 113 | "EM102", # Exception must not use an f-string literal, assign to variable first 114 | "RET504", # Unnecessary variable assignment before `return` statement 115 | "S301", # `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue 116 | "PLR0913", # Too many arguments to function call 117 | "PLR0915", # Too many statements 118 | "PLE0605", # Invalid format for `__all__`, must be `tuple` or `list` 119 | "PLR0912", # Too many branches 120 | "G004", # Logging statement uses an f-string 121 | "TD002", # Missing author in TODO 122 | "TD003", # Missing issue link on the line following this TODO 123 | "TRY003", # Long messages outside exception class 124 | "FIX", # Not allowed to use TODO 125 | "DTZ007", # Naive datetime constructed without %z 126 | ] 127 | # Exclude a variety of commonly ignored directories. 128 | lint.exclude = [ 129 | ".bzr", 130 | ".direnv", 131 | ".eggs", 132 | ".git", 133 | ".hg", 134 | ".mypy_cache", 135 | ".nox", 136 | ".pants.d", 137 | ".ruff_cache", 138 | ".svn", 139 | ".tox", 140 | ".venv", 141 | "__pypackages__", 142 | "_build", 143 | "buck-out", 144 | "build", 145 | "dist", 146 | "node_modules", 147 | "venv", 148 | ] 149 | lint.fixable = ["ALL"] 150 | # Allow unused variables when underscore-prefixed. 151 | lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 152 | 153 | [tool.ruff.lint.mccabe] 154 | # Unlike Flake8, default to a complexity level of 10. 155 | max-complexity = 10 156 | 157 | [tool.ruff.lint.per-file-ignores] 158 | "conftest.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001"] 159 | "test_*.py" = ["S101", "D100", "D103", "D417", "FBT001", "INP001", "SLF001", "FBT003"] 160 | "mqpy/utilities.py" = ["FBT001"] 161 | "docs/examples/*.py" = ["C901"] 162 | 163 | [tool.ruff.lint.pydocstyle] 164 | convention = "google" 165 | 166 | [tool.ruff.lint.pylint] 167 | allow-magic-value-types = [ 168 | "int", 169 | "str", 170 | ] 171 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """Scripts for the project.""" 2 | -------------------------------------------------------------------------------- /scripts/build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to build documentation for MQPy 4 | 5 | # Export Python path to include the project root directory 6 | export PYTHONPATH=$(pwd) 7 | 8 | # Install required packages if needed 9 | if [ "$1" == "--install" ]; then 10 | echo "Installing required packages..." 11 | pip install mkdocs mkdocs-material mkdocstrings mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index mkdocstrings-python mkdocs-jupyter 12 | fi 13 | 14 | # Create necessary directories 15 | mkdir -p docs/reference 16 | mkdir -p docs/css 17 | 18 | # Build the documentation 19 | echo "Building documentation..." 20 | mkdocs build 21 | 22 | # If the build was successful, optionally serve 23 | if [ $? -eq 0 ]; then 24 | echo "Documentation built successfully." 25 | 26 | if [ "$1" == "--serve" ] || [ "$2" == "--serve" ]; then 27 | echo "Serving documentation at http://localhost:8000" 28 | mkdocs serve 29 | fi 30 | else 31 | echo "Error building documentation." 32 | fi 33 | -------------------------------------------------------------------------------- /scripts/gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Generate the API reference pages for the MQPy package.""" 3 | 4 | import sys 5 | from pathlib import Path 6 | 7 | import mkdocs_gen_files 8 | 9 | # Add the project root to the Python path so we can import mqpy 10 | project_dir = Path(__file__).parent.parent 11 | sys.path.insert(0, str(project_dir)) 12 | 13 | package_dir = project_dir / "mqpy" 14 | package_name = "mqpy" 15 | 16 | # Create a navigation structure 17 | nav = mkdocs_gen_files.Nav() 18 | 19 | # Ensure the reference directory exists 20 | reference_dir = project_dir / "docs" / "reference" 21 | reference_dir.mkdir(parents=True, exist_ok=True) 22 | 23 | # Create an index page with a better layout 24 | index_path = Path("reference", "index.md") 25 | with mkdocs_gen_files.open(index_path, "w") as index_file: 26 | index_file.write("# API Reference\n\n") 27 | index_file.write( 28 | f"This section contains the complete API reference for all public modules and classes in {package_name}.\n\n" 29 | ) 30 | index_file.write("## Available Modules\n\n") 31 | 32 | # Create documentation for each module 33 | for path in sorted(package_dir.glob("**/*.py")): 34 | module_path = path.relative_to(project_dir).with_suffix("") 35 | doc_path = path.relative_to(project_dir).with_suffix(".md") 36 | full_doc_path = Path("reference", doc_path) 37 | 38 | # Skip __init__.py and __main__.py for individual pages 39 | parts = module_path.parts 40 | if parts[-1] in ["__init__", "__main__"]: 41 | continue 42 | 43 | # Generate proper import path 44 | import_path = ".".join(parts) 45 | 46 | # Create directory for the documentation 47 | full_doc_path.parent.mkdir(parents=True, exist_ok=True) 48 | 49 | # Write the page content 50 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 51 | fd.write("\n\n") 52 | 53 | # Write documentation for classes and functions 54 | fd.write(f"::: {import_path}\n") 55 | 56 | # Create title case version of the module name for navigation 57 | title_case_parts = list(parts) 58 | title_case_parts[-1] = parts[-1].replace("_", " ").title() 59 | 60 | # Add to navigation with title case name 61 | nav[title_case_parts] = doc_path.as_posix() 62 | 63 | # Update index file with simple links 64 | with mkdocs_gen_files.open(index_path, "a") as index_file: 65 | rel_path = doc_path.as_posix() 66 | module_name = parts[-1] 67 | title_case_name = module_name.replace("_", " ").title() 68 | index_file.write(f"- [{title_case_name}]({rel_path})\n") 69 | 70 | # Generate and write the navigation file 71 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: 72 | nav_file.write("# API Reference\n\n") 73 | nav_file.writelines(nav.build_literate_nav()) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for the mqpy package. 2 | 3 | This module contains the setup configuration for the mqpy package, which provides a Python interface 4 | for creating Expert Advisors in MetaTrader 5. 5 | """ 6 | 7 | from pathlib import Path 8 | 9 | from setuptools import find_packages, setup 10 | 11 | # Read the README for the long description 12 | with Path("README.md").open(encoding="utf-8") as f: 13 | long_description = f.read() 14 | 15 | setup( 16 | name="mqpy", 17 | version="0.6.9", 18 | packages=find_packages(), 19 | install_requires=[], 20 | author="Joao Euko", 21 | author_email="", 22 | description="A library to simplify creating Expert Advisors in MQL5", 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | license="MIT", 26 | entry_points={ 27 | "console_scripts": [ 28 | "mqpy=mqpy.__main__:main", 29 | ], 30 | }, 31 | python_requires=">=3.8", 32 | ) 33 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures and configuration shared across test modules.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ctypes 6 | import logging 7 | import time 8 | from typing import TYPE_CHECKING, Generator 9 | 10 | import pytest 11 | 12 | if TYPE_CHECKING: 13 | from mqpy.trade import Trade 14 | 15 | VK_CONTROL = 0x11 16 | VK_E = 0x45 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | def send_ctrl_e() -> None: 22 | """Send CTRL+E to MetaTrader 5 to enable Expert Advisors.""" 23 | user32 = ctypes.windll.user32 24 | # Press CTRL 25 | user32.keybd_event(VK_CONTROL, 0, 0, 0) 26 | # Press E 27 | user32.keybd_event(VK_E, 0, 0, 0) 28 | # Release E 29 | user32.keybd_event(VK_E, 0, 2, 0) 30 | # Release CTRL 31 | user32.keybd_event(VK_CONTROL, 0, 2, 0) 32 | time.sleep(1) 33 | 34 | 35 | @pytest.fixture 36 | def test_symbols() -> dict[str, str]: 37 | """Provides common test symbols that can be used across tests.""" 38 | return {"forex": "EURUSD", "indices": "US500", "commodities": "XAUUSD", "crypto": "BTCUSD", "invalid": "INVALID"} 39 | 40 | 41 | @pytest.fixture 42 | def configure_logging() -> Generator[None, None, None]: 43 | """Sets up logging configuration for tests.""" 44 | root = logging.getLogger() 45 | for handler in root.handlers[:]: 46 | root.removeHandler(handler) 47 | 48 | handler = logging.StreamHandler() 49 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 50 | handler.setFormatter(formatter) 51 | root.addHandler(handler) 52 | 53 | root.setLevel(logging.INFO) 54 | 55 | yield 56 | 57 | for handler in root.handlers[:]: 58 | root.removeHandler(handler) 59 | 60 | 61 | @pytest.fixture 62 | def enable_autotrade(trade: Trade) -> Trade: 63 | """Enables autotrade for testing purposes.""" 64 | send_ctrl_e() 65 | 66 | trade.start_time_hour = "0" 67 | trade.start_time_minutes = "00" 68 | trade.finishing_time_hour = "23" 69 | trade.finishing_time_minutes = "59" 70 | 71 | return trade 72 | -------------------------------------------------------------------------------- /tests/integration/test_mt5_connection.py: -------------------------------------------------------------------------------- 1 | """Integration test for MetaTrader5 connection. 2 | 3 | This module tests the ability to establish a connection with MT5 platform. 4 | """ 5 | 6 | import logging 7 | import os 8 | import sys 9 | import time 10 | 11 | import MetaTrader5 as mt5 # noqa: N813 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(logging.INFO) 15 | 16 | console_handler = logging.StreamHandler(sys.stdout) 17 | console_handler.setLevel(logging.INFO) 18 | 19 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 20 | console_handler.setFormatter(formatter) 21 | logger.addHandler(console_handler) 22 | 23 | logger.info("Testing MT5 initialization...") 24 | 25 | success = False 26 | for attempt in range(10): 27 | try: 28 | if mt5.initialize( 29 | login=int(os.getenv("MT5_LOGIN")), # type: ignore[arg-type] 30 | password=os.getenv("MT5_PASSWORD"), 31 | server=os.getenv("MT5_SERVER"), 32 | path=os.getenv("MT5_PATH"), 33 | ): 34 | logger.info("MT5 initialized successfully") 35 | mt5.shutdown() 36 | success = True 37 | break 38 | except (ConnectionError, ValueError, TypeError) as e: 39 | logger.info(f"Connection error: {e}") 40 | try: 41 | mt5.initialize() 42 | except (ConnectionError, ValueError, TypeError) as e: 43 | logger.info(f"Attempt {attempt+1}: Not ready yet, sleeping... Error: {e}") 44 | time.sleep(5) 45 | 46 | if not success: 47 | logger.info("Failed to initialize MT5 after waiting.") 48 | mt5.shutdown() 49 | -------------------------------------------------------------------------------- /tests/test_book.py: -------------------------------------------------------------------------------- 1 | """Tests for the Book class that manages market depth information from MetaTrader 5.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import time 7 | from typing import TYPE_CHECKING, Generator 8 | 9 | import MetaTrader5 as Mt5 10 | import pytest 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.logging import LogCaptureFixture 14 | 15 | from mqpy.book import Book 16 | 17 | 18 | @pytest.fixture(scope="module", autouse=True) 19 | def setup_teardown() -> Generator[None, None, None]: 20 | """Set up and tear down MetaTrader5 connection for the test module.""" 21 | if not Mt5.initialize(): 22 | pytest.skip("MetaTrader5 could not be initialized") 23 | 24 | time.sleep(5) 25 | 26 | yield 27 | 28 | Mt5.shutdown() 29 | 30 | 31 | @pytest.fixture 32 | def symbol() -> str: 33 | """Provides a valid trading symbol for testing.""" 34 | time.sleep(1) 35 | 36 | symbols = Mt5.symbols_get() 37 | if not symbols: 38 | pytest.skip("No symbols available for testing") 39 | 40 | for symbol in symbols: 41 | if symbol.name == "EURUSD": 42 | return "EURUSD" 43 | 44 | return symbols[0].name 45 | 46 | 47 | def test_book_initialization(symbol: str, caplog: LogCaptureFixture) -> None: 48 | """Test initialization of Book with a real symbol.""" 49 | caplog.set_level(logging.INFO) 50 | # Create book instance (used to trigger log message) 51 | Book(symbol) 52 | 53 | assert f"The symbol {symbol} was successfully added to the market book" in caplog.text 54 | 55 | 56 | def test_book_get(symbol: str) -> None: 57 | """Test getting real market book data.""" 58 | book = Book(symbol) 59 | 60 | time.sleep(1) 61 | 62 | market_data = book.get() 63 | 64 | assert market_data is not None 65 | 66 | if market_data: 67 | assert isinstance(market_data, (list, tuple)) 68 | 69 | # Loop separately to check for bids and asks 70 | has_bids = False 71 | has_asks = False 72 | 73 | for item in market_data: 74 | if item.type == Mt5.BOOK_TYPE_SELL: 75 | has_bids = True 76 | if item.type == Mt5.BOOK_TYPE_BUY: 77 | has_asks = True 78 | 79 | if not (has_bids or has_asks): 80 | logging.warning(f"No bids or asks found in market book for {symbol}") 81 | 82 | book.release() 83 | 84 | 85 | def test_book_release(symbol: str) -> None: 86 | """Test releasing the market book.""" 87 | book = Book(symbol) 88 | 89 | result = book.release() 90 | 91 | assert result is True 92 | 93 | 94 | def test_full_workflow(symbol: str) -> None: 95 | """Test a complete workflow with the real market book.""" 96 | book = Book(symbol) 97 | 98 | time.sleep(1) 99 | 100 | market_data = book.get() 101 | 102 | assert market_data is not None 103 | 104 | release_result = book.release() 105 | assert release_result is True 106 | 107 | time.sleep(1) 108 | data_after_release = book.get() 109 | 110 | if data_after_release is not None and len(data_after_release) > 0: 111 | logging.info("Market book data still available after release") 112 | 113 | 114 | def test_multiple_symbols() -> None: 115 | """Test using Book with multiple symbols simultaneously.""" 116 | symbols = Mt5.symbols_get() 117 | if len(symbols) < 2: 118 | pytest.skip("Need at least 2 symbols for this test") 119 | 120 | symbol1 = symbols[0].name 121 | symbol2 = symbols[1].name 122 | 123 | book1 = Book(symbol1) 124 | book2 = Book(symbol2) 125 | 126 | time.sleep(1) 127 | 128 | data1 = book1.get() 129 | data2 = book2.get() 130 | 131 | assert data1 is not None 132 | assert data2 is not None 133 | 134 | book1.release() 135 | book2.release() 136 | 137 | 138 | def test_unavailable_symbol(caplog: LogCaptureFixture) -> None: 139 | """Test behavior with an unavailable symbol.""" 140 | caplog.set_level(logging.ERROR) 141 | 142 | invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" 143 | 144 | book = Book(invalid_symbol) 145 | 146 | assert "Error adding INVALID_SYMBOL_THAT_DOESNT_EXIST to the market book" in caplog.text 147 | 148 | release_result = book.release() 149 | assert release_result is False 150 | -------------------------------------------------------------------------------- /tests/test_rates.py: -------------------------------------------------------------------------------- 1 | """Tests for the Rates class that retrieves historical price data from MetaTrader 5.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import time 7 | from typing import Generator 8 | 9 | import MetaTrader5 as Mt5 10 | import pytest 11 | 12 | from mqpy.rates import Rates 13 | 14 | 15 | @pytest.fixture(scope="module", autouse=True) 16 | def setup_teardown() -> Generator[None, None, None]: 17 | """Set up and tear down MetaTrader5 connection for the test module.""" 18 | if not Mt5.initialize(): 19 | pytest.skip("MetaTrader5 could not be initialized") 20 | 21 | time.sleep(5) 22 | 23 | yield 24 | 25 | Mt5.shutdown() 26 | 27 | 28 | @pytest.fixture 29 | def symbol() -> str: 30 | """Provides a valid trading symbol for testing.""" 31 | time.sleep(1) 32 | 33 | symbols = Mt5.symbols_get() 34 | if not symbols: 35 | pytest.skip("No symbols available for testing") 36 | 37 | for symbol in symbols: 38 | if symbol.name == "EURUSD": 39 | return "EURUSD" 40 | 41 | return symbols[0].name 42 | 43 | 44 | @pytest.fixture 45 | def timeframe() -> int: 46 | """Provides a valid timeframe for testing.""" 47 | return Mt5.TIMEFRAME_H1 48 | 49 | 50 | def test_rates_initialization(symbol: str, timeframe: int) -> None: 51 | """Test initialization of Rates with a real symbol.""" 52 | rates = Rates(symbol, timeframe, 0, 10) 53 | 54 | assert len(rates.time) == 10 55 | assert len(rates.open) == 10 56 | assert len(rates.high) == 10 57 | assert len(rates.low) == 10 58 | assert len(rates.close) == 10 59 | assert len(rates.tick_volume) == 10 60 | assert len(rates.spread) == 10 61 | assert len(rates.real_volume) == 10 62 | 63 | 64 | def test_rates_data_types(symbol: str, timeframe: int) -> None: 65 | """Test data types of all Rates properties.""" 66 | rates = Rates(symbol, timeframe, 0, 5) 67 | 68 | assert rates.time[0] is not None 69 | assert isinstance(rates.open[0], float) 70 | assert isinstance(rates.high[0], float) 71 | assert isinstance(rates.low[0], float) 72 | assert isinstance(rates.close[0], float) 73 | 74 | assert rates.tick_volume[0] >= 0 75 | assert rates.spread[0] >= 0 76 | assert rates.real_volume[0] >= 0 77 | 78 | logging.info(f"Type of tick_volume: {type(rates.tick_volume[0])}") 79 | logging.info(f"Type of spread: {type(rates.spread[0])}") 80 | logging.info(f"Type of real_volume: {type(rates.real_volume[0])}") 81 | 82 | 83 | def test_rates_data_relationships(symbol: str, timeframe: int) -> None: 84 | """Test relationships between rate data points.""" 85 | rates = Rates(symbol, timeframe, 0, 10) 86 | 87 | for i in range(len(rates.high)): 88 | assert rates.high[i] >= rates.open[i] 89 | assert rates.high[i] >= rates.close[i] 90 | assert rates.high[i] >= rates.low[i] 91 | 92 | assert rates.low[i] <= rates.open[i] 93 | assert rates.low[i] <= rates.close[i] 94 | assert rates.low[i] <= rates.high[i] 95 | 96 | assert rates.spread[i] >= 0 97 | assert rates.tick_volume[i] >= 0 98 | assert rates.real_volume[i] >= 0 99 | 100 | 101 | def test_different_timeframes(symbol: str) -> None: 102 | """Test retrieving rates with different timeframes.""" 103 | timeframes = [Mt5.TIMEFRAME_M1, Mt5.TIMEFRAME_M5, Mt5.TIMEFRAME_H1, Mt5.TIMEFRAME_D1] 104 | 105 | for tf in timeframes: 106 | rates = Rates(symbol, tf, 0, 5) 107 | 108 | assert len(rates.time) == 5 109 | assert len(rates.open) == 5 110 | assert len(rates.close) == 5 111 | 112 | logging.info(f"Successfully retrieved rates for {symbol} with timeframe {tf}") 113 | 114 | 115 | def test_different_counts(symbol: str, timeframe: int) -> None: 116 | """Test retrieving different number of rates.""" 117 | counts = [1, 10, 50] 118 | 119 | for count in counts: 120 | rates = Rates(symbol, timeframe, 0, count) 121 | 122 | assert len(rates.time) == count 123 | assert len(rates.open) == count 124 | assert len(rates.close) == count 125 | 126 | logging.info(f"Successfully retrieved {count} rates for {symbol}") 127 | 128 | 129 | def test_different_start_positions(symbol: str, timeframe: int) -> None: 130 | """Test retrieving rates from different start positions.""" 131 | start_positions = [0, 10, 50] 132 | 133 | for pos in start_positions: 134 | rates = Rates(symbol, timeframe, pos, 5) 135 | 136 | assert len(rates.time) == 5 137 | assert len(rates.open) == 5 138 | assert len(rates.close) == 5 139 | 140 | logging.info(f"Successfully retrieved rates for {symbol} from position {pos}") 141 | 142 | 143 | def test_invalid_symbol() -> None: 144 | """Test behavior with an invalid symbol.""" 145 | invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" 146 | 147 | with pytest.raises(ValueError, match="Failed to create Rates object"): 148 | Rates(invalid_symbol, Mt5.TIMEFRAME_H1, 0, 10) 149 | 150 | 151 | def test_invalid_parameters(symbol: str) -> None: 152 | """Test behavior with invalid parameter values.""" 153 | with pytest.raises(ValueError, match="Failed to create Rates object"): 154 | Rates(symbol, Mt5.TIMEFRAME_H1, 0, -1) 155 | 156 | with pytest.raises(ValueError, match="Failed to create Rates object"): 157 | Rates(symbol, 9999, 0, 10) 158 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | """Tests for the template module that generates MT5 expert advisor template files.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import shutil 7 | import subprocess 8 | import sys 9 | import tempfile 10 | from pathlib import Path 11 | 12 | # Import the template functions directly for function-level testing 13 | from mqpy.template import main 14 | 15 | 16 | def test_template_generates_file_with_default_values() -> None: 17 | """Test that the template generates a file with default values.""" 18 | # Create a temporary directory for testing 19 | temp_dir = tempfile.mkdtemp() 20 | try: 21 | # Save current directory and move to temp directory 22 | original_dir = Path.cwd() 23 | os.chdir(temp_dir) 24 | 25 | # Run the main function directly 26 | sys.argv = ["template.py"] # Reset sys.argv 27 | main() 28 | 29 | # Verify file was created with default name 30 | assert Path("demo.py").exists() 31 | 32 | # Check the content contains expected defaults 33 | with Path("demo.py").open(encoding="utf-8") as file: 34 | content = file.read() 35 | assert 'symbol="EURUSD"' in content 36 | assert "Moving Average Crossover" in content 37 | assert "short_window_size = 5" in content 38 | assert "long_window_size = 20" in content 39 | 40 | finally: 41 | # Clean up: restore original directory and remove temp directory 42 | os.chdir(original_dir) 43 | shutil.rmtree(temp_dir) 44 | 45 | 46 | def test_template_generates_file_with_custom_values() -> None: 47 | """Test that the template generates a file with custom values.""" 48 | # Create a temporary directory for testing 49 | temp_dir = tempfile.mkdtemp() 50 | try: 51 | # Save current directory and move to temp directory 52 | original_dir = Path.cwd() 53 | os.chdir(temp_dir) 54 | 55 | # Set custom command line arguments 56 | sys.argv = ["template.py", "--file_name", "custom_strategy", "--symbol", "BTCUSD"] 57 | main() 58 | 59 | # Verify file was created with custom name 60 | assert Path("custom_strategy.py").exists() 61 | 62 | # Check the content contains the custom symbol 63 | with Path("custom_strategy.py").open(encoding="utf-8") as file: 64 | content = file.read() 65 | assert 'symbol="BTCUSD"' in content 66 | 67 | finally: 68 | # Clean up: restore original directory and remove temp directory 69 | os.chdir(original_dir) 70 | shutil.rmtree(temp_dir) 71 | 72 | 73 | def test_template_overwrites_existing_file() -> None: 74 | """Test that the template overwrites an existing file.""" 75 | # Create a temporary directory for testing 76 | temp_dir = tempfile.mkdtemp() 77 | try: 78 | # Save current directory and move to temp directory 79 | original_dir = Path.cwd() 80 | os.chdir(temp_dir) 81 | 82 | # Create an existing file with known content 83 | Path("overwrite_test.py").write_text("ORIGINAL CONTENT THAT SHOULD BE REPLACED", encoding="utf-8") 84 | 85 | # Run the template generator with the same filename 86 | sys.argv = ["template.py", "--file_name", "overwrite_test", "--symbol", "EURUSD"] 87 | main() 88 | 89 | # Verify the file was overwritten 90 | with Path("overwrite_test.py").open(encoding="utf-8") as file: 91 | content = file.read() 92 | assert "ORIGINAL CONTENT THAT SHOULD BE REPLACED" not in content 93 | assert 'symbol="EURUSD"' in content 94 | assert "Moving Average Crossover" in content 95 | 96 | finally: 97 | # Clean up: restore original directory and remove temp directory 98 | os.chdir(original_dir) 99 | shutil.rmtree(temp_dir) 100 | 101 | 102 | def test_template_runs_as_script() -> None: 103 | """Test that the template script can be run through Python.""" 104 | # Create a temporary directory for testing 105 | temp_dir = tempfile.mkdtemp() 106 | try: 107 | # Save current directory and move to temp directory 108 | original_dir = Path.cwd() 109 | os.chdir(temp_dir) 110 | 111 | # Run the template script using subprocess with custom parameters 112 | result = subprocess.run( # noqa: S603 113 | [sys.executable, "-m", "mqpy.template", "--file_name", "script_test", "--symbol", "XAUUSD"], 114 | capture_output=True, 115 | text=True, 116 | check=True, 117 | ) 118 | 119 | # Check that the process ran successfully 120 | assert result.returncode == 0 121 | 122 | # Verify file was created with the right name 123 | assert Path("script_test.py").exists() 124 | 125 | # Check the content contains the custom symbol 126 | with Path("script_test.py").open(encoding="utf-8") as file: 127 | content = file.read() 128 | assert 'symbol="XAUUSD"' in content 129 | 130 | finally: 131 | # Clean up: restore original directory and remove temp directory 132 | os.chdir(original_dir) 133 | shutil.rmtree(temp_dir) 134 | 135 | 136 | def test_template_creates_valid_python_file() -> None: 137 | """Test that the generated template is valid Python code.""" 138 | # Create a temporary directory for testing 139 | temp_dir = tempfile.mkdtemp() 140 | try: 141 | # Save current directory and move to temp directory 142 | original_dir = Path.cwd() 143 | os.chdir(temp_dir) 144 | 145 | # Generate a template file 146 | sys.argv = ["template.py", "--file_name", "syntax_test"] 147 | main() 148 | 149 | # Try to compile the generated file to check for syntax errors 150 | with Path("syntax_test.py").open(encoding="utf-8") as file: 151 | content = file.read() 152 | 153 | # This will raise a SyntaxError if the code is not valid 154 | compile(content, "syntax_test.py", "exec") 155 | 156 | finally: 157 | # Clean up: restore original directory and remove temp directory 158 | os.chdir(original_dir) 159 | shutil.rmtree(temp_dir) 160 | 161 | 162 | # CLI-specific tests 163 | def test_cli_command_help() -> None: 164 | """Test that the CLI command --help option works correctly.""" 165 | result = subprocess.run( # noqa: S603 166 | [sys.executable, "-m", "mqpy.template", "--help"], capture_output=True, text=True, check=False 167 | ) 168 | 169 | # Check that the command ran successfully 170 | assert result.returncode == 0 171 | 172 | # Check that the help output contains expected content 173 | assert "Generate MetaTrader 5 expert advisor templates" in result.stdout 174 | assert "--file_name" in result.stdout 175 | assert "--symbol" in result.stdout 176 | assert "--strategy" in result.stdout 177 | assert "moving_average" in result.stdout 178 | 179 | 180 | def test_cli_different_strategies() -> None: 181 | """Test generating all different strategy types via CLI.""" 182 | # Create a temporary directory for testing 183 | temp_dir = tempfile.mkdtemp() 184 | try: 185 | # Save current directory and move to temp directory 186 | original_dir = Path.cwd() 187 | os.chdir(temp_dir) 188 | 189 | # Test each strategy type 190 | strategies = ["moving_average", "rsi", "macd", "bollinger"] 191 | 192 | for strategy in strategies: 193 | # Run the CLI command 194 | result = subprocess.run( # noqa: S603 195 | [sys.executable, "-m", "mqpy.template", "--strategy", strategy, "--file_name", f"test_{strategy}"], 196 | capture_output=True, 197 | text=True, 198 | check=False, 199 | ) 200 | 201 | # Check the command ran successfully 202 | assert result.returncode == 0 203 | 204 | # Verify file was created 205 | assert Path(f"test_{strategy}.py").exists() 206 | 207 | # Check strategy-specific content 208 | with Path(f"test_{strategy}.py").open(encoding="utf-8") as file: 209 | content = file.read() 210 | 211 | # Check for strategy-specific indicators 212 | if strategy == "moving_average": 213 | assert "short_window_size = 5" in content 214 | assert "long_window_size = 20" in content 215 | elif strategy == "rsi": 216 | assert "rsi_period = 14" in content 217 | assert "oversold_threshold = 30" in content 218 | assert "calculate_rsi(" in content 219 | elif strategy == "macd": 220 | assert "fast_period = 12" in content 221 | assert "slow_period = 26" in content 222 | assert "calculate_macd(" in content 223 | elif strategy == "bollinger": 224 | assert "bb_period = 20" in content 225 | assert "std_dev_multiplier = 2.0" in content 226 | assert "calculate_bollinger_bands(" in content 227 | 228 | finally: 229 | # Clean up: restore original directory and remove temp directory 230 | os.chdir(original_dir) 231 | shutil.rmtree(temp_dir) 232 | 233 | 234 | def test_cli_custom_directory() -> None: 235 | """Test that the --directory option works correctly.""" 236 | # Create a temporary directory for testing 237 | temp_dir = tempfile.mkdtemp() 238 | try: 239 | # Save current directory and move to temp directory 240 | original_dir = Path.cwd() 241 | os.chdir(temp_dir) 242 | 243 | # Create a subdirectory path that doesn't exist yet 244 | custom_dir = Path(temp_dir) / "custom_output_dir" 245 | 246 | # Run the CLI command with custom directory 247 | result = subprocess.run( # noqa: S603 248 | [sys.executable, "-m", "mqpy.template", "--file_name", "dir_test", "--directory", str(custom_dir)], 249 | capture_output=True, 250 | text=True, 251 | check=False, 252 | ) 253 | 254 | # Check the command ran successfully 255 | assert result.returncode == 0 256 | 257 | # Verify directory was created 258 | assert custom_dir.exists() 259 | 260 | # Verify file was created in the custom directory 261 | assert (custom_dir / "dir_test.py").exists() 262 | 263 | finally: 264 | # Clean up: restore original directory and remove temp directory 265 | os.chdir(original_dir) 266 | shutil.rmtree(temp_dir) 267 | 268 | 269 | def test_cli_custom_parameters() -> None: 270 | """Test that the CLI command accepts custom trading parameters.""" 271 | # Create a temporary directory for testing 272 | temp_dir = tempfile.mkdtemp() 273 | try: 274 | # Save current directory and move to temp directory 275 | original_dir = Path.cwd() 276 | os.chdir(temp_dir) 277 | 278 | # Run the CLI command with custom parameters 279 | result = subprocess.run( # noqa: S603 280 | [ 281 | sys.executable, 282 | "-m", 283 | "mqpy.template", 284 | "--file_name", 285 | "params_test", 286 | "--symbol", 287 | "GBPJPY", 288 | "--magic_number", 289 | "12345", 290 | "--lot", 291 | "0.25", 292 | "--stop_loss", 293 | "30", 294 | "--take_profit", 295 | "60", 296 | ], 297 | capture_output=True, 298 | text=True, 299 | check=False, 300 | ) 301 | 302 | # Check the command ran successfully 303 | assert result.returncode == 0 304 | 305 | # Verify file was created 306 | assert Path("params_test.py").exists() 307 | 308 | # Check custom parameters in the content 309 | with Path("params_test.py").open(encoding="utf-8") as file: 310 | content = file.read() 311 | assert 'symbol="GBPJPY"' in content 312 | assert "magic_number=12345" in content 313 | assert "lot=0.25" in content 314 | assert "stop_loss=30" in content 315 | assert "take_profit=60" in content 316 | 317 | finally: 318 | # Clean up: restore original directory and remove temp directory 319 | os.chdir(original_dir) 320 | shutil.rmtree(temp_dir) 321 | -------------------------------------------------------------------------------- /tests/test_tick.py: -------------------------------------------------------------------------------- 1 | """Tests for the Tick class that retrieves real-time tick data from MetaTrader 5.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import time 7 | from typing import Generator 8 | 9 | import MetaTrader5 as Mt5 10 | import pytest 11 | 12 | from mqpy.tick import Tick 13 | 14 | 15 | @pytest.fixture(scope="module", autouse=True) 16 | def setup_teardown() -> Generator[None, None, None]: 17 | """Set up and tear down MetaTrader5 connection for the test module.""" 18 | if not Mt5.initialize(): 19 | pytest.skip("MetaTrader5 could not be initialized") 20 | 21 | time.sleep(5) 22 | 23 | yield 24 | 25 | Mt5.shutdown() 26 | 27 | 28 | @pytest.fixture 29 | def symbol() -> str: 30 | """Provides a valid trading symbol for testing.""" 31 | time.sleep(1) 32 | 33 | symbols = Mt5.symbols_get() 34 | if not symbols: 35 | pytest.skip("No symbols available for testing") 36 | 37 | for symbol in symbols: 38 | if symbol.name == "EURUSD": 39 | return "EURUSD" 40 | 41 | return symbols[0].name 42 | 43 | 44 | def test_tick_initialization(symbol: str) -> None: 45 | """Test initialization of Tick with a real symbol.""" 46 | tick = Tick(symbol) 47 | 48 | assert tick.symbol == symbol 49 | 50 | assert isinstance(tick.time, int) 51 | assert isinstance(tick.bid, float) 52 | assert isinstance(tick.ask, float) 53 | assert tick.ask >= 0 54 | assert tick.bid >= 0 55 | assert tick.ask >= tick.bid 56 | 57 | 58 | def test_tick_properties(symbol: str) -> None: 59 | """Test all Tick properties with a real symbol.""" 60 | tick = Tick(symbol) 61 | 62 | assert isinstance(tick.symbol, str) 63 | assert isinstance(tick.time, int) 64 | assert isinstance(tick.bid, float) 65 | assert isinstance(tick.ask, float) 66 | assert isinstance(tick.time_msc, int) 67 | assert isinstance(tick.flags, int) 68 | 69 | # Check last property 70 | if tick.last is not None: 71 | assert isinstance(tick.last, float) 72 | 73 | # Check volume property 74 | if tick.volume is not None: 75 | assert isinstance(tick.volume, int) 76 | 77 | if tick.volume_real is not None: 78 | assert isinstance(tick.volume_real, float) 79 | 80 | 81 | def test_updated_tick_data(symbol: str) -> None: 82 | """Test getting updated tick data after waiting.""" 83 | first_tick = Tick(symbol) 84 | first_time = first_tick.time 85 | 86 | time.sleep(2) 87 | 88 | second_tick = Tick(symbol) 89 | second_time = second_tick.time 90 | 91 | if first_time == second_time: 92 | # Log instead of print 93 | logging.info(f"No tick update for {symbol} after 2 seconds") 94 | 95 | 96 | def test_multiple_symbols() -> None: 97 | """Test Tick with multiple symbols simultaneously.""" 98 | symbols = Mt5.symbols_get() 99 | if len(symbols) < 2: 100 | pytest.skip("Need at least 2 symbols for this test") 101 | 102 | symbol1 = symbols[0].name 103 | symbol2 = symbols[1].name 104 | 105 | tick1 = Tick(symbol1) 106 | tick2 = Tick(symbol2) 107 | 108 | assert tick1.symbol == symbol1 109 | assert tick2.symbol == symbol2 110 | 111 | assert isinstance(tick1.bid, float) 112 | assert isinstance(tick1.ask, float) 113 | assert isinstance(tick2.bid, float) 114 | assert isinstance(tick2.ask, float) 115 | 116 | 117 | def test_invalid_symbol() -> None: 118 | """Test behavior with an invalid symbol.""" 119 | invalid_symbol = "INVALID_SYMBOL_THAT_DOESNT_EXIST" 120 | 121 | with pytest.raises(AttributeError, match="'NoneType' object has no attribute 'time'"): 122 | Tick(invalid_symbol) 123 | 124 | 125 | def test_spread_calculation(symbol: str) -> None: 126 | """Test spread calculation from bid/ask values.""" 127 | tick = Tick(symbol) 128 | 129 | spread = tick.ask - tick.bid 130 | 131 | assert spread >= 0 132 | 133 | logging.info(f"Spread for {symbol}: {spread}") 134 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | """Tests for the Utilities class that provides helper functions for trading operations.""" 2 | 3 | from __future__ import annotations 4 | 5 | import time 6 | from typing import Generator 7 | 8 | import MetaTrader5 as Mt5 9 | import pytest 10 | 11 | from mqpy.utilities import Utilities 12 | 13 | 14 | @pytest.fixture(scope="module", autouse=True) 15 | def setup_teardown() -> Generator[None, None, None]: 16 | """Set up and tear down MetaTrader5 connection for the test module.""" 17 | if not Mt5.initialize(): 18 | pytest.skip("MetaTrader5 could not be initialized") 19 | 20 | time.sleep(5) 21 | 22 | yield 23 | 24 | Mt5.shutdown() 25 | 26 | 27 | @pytest.fixture 28 | def symbol() -> str: 29 | """Provides a valid trading symbol for testing.""" 30 | time.sleep(1) 31 | 32 | symbols = Mt5.symbols_get() 33 | if not symbols: 34 | pytest.skip("No symbols available for testing") 35 | 36 | for symbol in symbols: 37 | if symbol.name == "EURUSD": 38 | return "EURUSD" 39 | 40 | return symbols[0].name 41 | 42 | 43 | @pytest.fixture 44 | def utilities() -> Utilities: 45 | """Provides a Utilities instance for testing.""" 46 | return Utilities() 47 | 48 | 49 | def test_utilities_initialization() -> None: 50 | """Test initialization of Utilities class.""" 51 | utilities = Utilities() 52 | assert isinstance(utilities, Utilities) 53 | symbol = "EURUSD" 54 | assert utilities.check_trade_availability(symbol, 5) is True 55 | 56 | 57 | def test_multiple_utilities_instances() -> None: 58 | """Test that multiple Utilities instances work independently.""" 59 | utilities1 = Utilities() 60 | utilities2 = Utilities() 61 | assert utilities1.check_trade_availability("EURUSD", 5) is True 62 | assert utilities2.check_trade_availability("EURUSD", 5) is True 63 | assert utilities1 is not utilities2 64 | 65 | 66 | def test_utilities_attributes(utilities: Utilities) -> None: 67 | """Test that the Utilities class has the expected attributes.""" 68 | assert hasattr(utilities, "_test_get_minutes_counter") 69 | assert hasattr(utilities, "_test_get_counter_flag") 70 | assert hasattr(utilities, "_test_get_allowed_to_trade") 71 | assert hasattr(utilities, "_test_get_allow_to_count") 72 | assert hasattr(utilities, "_test_get_recent_trade") 73 | 74 | assert utilities._test_get_minutes_counter() == 0 75 | assert utilities._test_get_counter_flag() is True 76 | assert utilities._test_get_allowed_to_trade() is True 77 | assert utilities._test_get_allow_to_count() is False 78 | assert utilities._test_get_recent_trade() is False 79 | 80 | assert hasattr(utilities, "check_trade_availability") 81 | assert callable(utilities.check_trade_availability) 82 | 83 | 84 | def test_reset_counters_functionality(utilities: Utilities) -> None: 85 | """Test the reset_counters functionality directly.""" 86 | utilities._test_set_minutes_counter(5) 87 | utilities._test_set_counter_flag(False) 88 | utilities._test_set_allowed_to_trade(False) 89 | utilities._test_set_allow_to_count(True) 90 | 91 | utilities._test_reset_counters() 92 | 93 | assert utilities._test_get_minutes_counter() == 0 94 | assert utilities._test_get_counter_flag() is True 95 | assert utilities._test_get_allowed_to_trade() is True 96 | assert utilities._test_get_allow_to_count() is False 97 | --------------------------------------------------------------------------------