name: Freqtrade CI

on:
  push:
    branches:
      - stable
      - develop
      - ci/*
    tags:
  release:
    types: [published]
  pull_request:
  schedule:
    - cron:  '0 3 * * 4'

concurrency:
  group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
  cancel-in-progress: true
permissions:
  repository-projects: read
jobs:
  build-linux:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ "ubuntu-22.04", "ubuntu-24.04" ]
        python-version: ["3.11", "3.12", "3.13"]

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install uv
      uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
      with:
        activate-environment: true
        enable-cache: true
        python-version: ${{ matrix.python-version }}
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "${{ matrix.python-version }}"
        prune-cache: false

    - name: Installation - *nix
      run: |
        uv pip install --upgrade wheel
        uv pip install -r requirements-dev.txt
        uv pip install -e ft_client/
        uv pip install -e .

    - name: Check for version alignment
      run: |
        python build_helpers/freqtrade_client_version_align.py

    - name: Tests
      if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'))
      run: |
        pytest --random-order

    - name: Tests with Coveralls
      if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
      run: |
        pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc

    - name: Coveralls
      if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
      env:
        # Coveralls token. Not used as secret due to github not providing secrets to forked repositories
        COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
      run: |
        # Allow failure for coveralls
        uv pip install coveralls
        coveralls || true

    - name: Run json schema extract
      # This should be kept before the repository check to ensure that the schema is up-to-date
      run: |
        python build_helpers/extract_config_json_schema.py

    - name: Run command docs partials extract
      # This should be kept before the repository check to ensure that the docs are up-to-date
      run: |
        python build_helpers/create_command_partials.py

    - name: Check for repository changes
      # TODO: python 3.13 slightly changed the output of argparse.
      if: (matrix.python-version != '3.13')
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "Repository is dirty, changes detected:"
          git status
          git diff
          exit 1
        else
          echo "Repository is clean, no changes detected."
        fi

    - name: Backtesting (multi)
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade new-strategy -s AwesomeStrategy
        freqtrade new-strategy -s AwesomeStrategyMin --template minimal
        freqtrade backtesting --datadir tests/testdata --strategy-list AwesomeStrategy AwesomeStrategyMin -i 5m

    - name: Hyperopt
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all

    - name: Sort imports (isort)
      run: |
        isort --check .

    - name: Run Ruff
      run: |
        ruff check --output-format=github

    - name: Run Ruff format check
      run: |
        ruff format --check

    - name: Mypy
      if: matrix.os == 'ubuntu-24.04'
      run: |
        mypy freqtrade scripts tests

    - name: Discord notification
      uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
      if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          severity: error
          details: Freqtrade CI failed on ${{ matrix.os }}
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}

  build-macos:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ "macos-14", "macos-15" ]
        python-version: ["3.11", "3.12", "3.13"]

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
        check-latest: true

    - name: Install uv
      uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
      with:
        activate-environment: true
        enable-cache: true
        python-version: ${{ matrix.python-version }}
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "${{ matrix.python-version }}"
        prune-cache: false

    - name: Installation - macOS (Brew)
      run: |
        # brew update
        # TODO: Should be the brew upgrade
        # homebrew fails to update python due to unlinking failures
        # https://github.com/actions/runner-images/issues/6817
        rm /usr/local/bin/2to3 || true
        rm /usr/local/bin/2to3-3.11 || true
        rm /usr/local/bin/2to3-3.12 || true
        rm /usr/local/bin/idle3 || true
        rm /usr/local/bin/idle3.11 || true
        rm /usr/local/bin/idle3.12 || true
        rm /usr/local/bin/pydoc3 || true
        rm /usr/local/bin/pydoc3.11 || true
        rm /usr/local/bin/pydoc3.12 || true
        rm /usr/local/bin/python3 || true
        rm /usr/local/bin/python3.11 || true
        rm /usr/local/bin/python3.12 || true
        rm /usr/local/bin/python3-config || true
        rm /usr/local/bin/python3.11-config || true
        rm /usr/local/bin/python3.12-config || true

        brew install libomp

    - name: Installation (python)
      run: |
        uv pip install wheel
        uv pip install -r requirements-dev.txt
        uv pip install -e ft_client/
        uv pip install -e .

    - name: Tests
      run: |
        pytest --random-order

    - name: Check for repository changes
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "Repository is dirty, changes detected:"
          git status
          git diff
          exit 1
        else
          echo "Repository is clean, no changes detected."
        fi

    - name: Backtesting
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
        freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv

    - name: Hyperopt
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all

    - name: Sort imports (isort)
      run: |
        isort --check .

    - name: Run Ruff
      run: |
        ruff check --output-format=github

    - name: Run Ruff format check
      run: |
        ruff format --check

    - name: Mypy
      if: matrix.os == 'macos-15'
      run: |
        mypy freqtrade scripts

    - name: Discord notification
      uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
      if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          severity: info
          details: Test Succeeded!
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}

  build-windows:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ "windows-2022", "windows-2025" ]
        python-version: ["3.11", "3.12", "3.13"]

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install uv
      uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
      with:
        activate-environment: true
        enable-cache: true
        python-version: ${{ matrix.python-version }}
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "${{ matrix.python-version }}"
        prune-cache: false

    - name: Installation
      run: |
        function uvpipFunction { uv pip $args }
        Set-Alias -name pip -value uvpipFunction

        python -m pip install --upgrade pip
        pip install -r requirements-dev.txt
        pip install -e .

    - name: Tests
      run: |
        pytest --random-order --durations 20 -n auto

    - name: Check for repository changes
      run: |
        if (git status --porcelain) {
          Write-Host "Repository is dirty, changes detected:"
          git status
          git diff
          exit 1
        }
        else {
          Write-Host "Repository is clean, no changes detected."
        }

    - name: Backtesting
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy

    - name: Hyperopt
      run: |
        cp tests/testdata/config.tests.json config.json
        freqtrade create-userdir --userdir user_data
        freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all

    - name: Run Ruff
      run: |
        ruff check --output-format=github

    - name: Run Ruff format check
      run: |
        ruff format --check

    - name: Mypy
      run: |
        mypy freqtrade scripts tests

    - name: Run Pester tests (PowerShell)
      run: |
        $PSVersionTable
        Set-PSRepository psgallery -InstallationPolicy trusted
        Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
        $Error.clear()
        Invoke-Pester -Path "tests" -CI
        if ($Error.Length -gt 0) {exit 1}

      shell: powershell

    - name: Discord notification
      uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
      if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          severity: error
          details: Test Failed
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}

  mypy-version-check:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: pre-commit dependencies
      run: |
        pip install pyaml
        python build_helpers/pre_commit_update.py

  pre-commit:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - uses: actions/setup-python@v5
      with:
        python-version: "3.12"
    - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1

  docs-check:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Documentation syntax
      run: |
        ./tests/test_docs.sh

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: Documentation build
      run: |
        pip install -r docs/requirements-docs.txt
        mkdocs build

    - name: Discord notification
      uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
      if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          severity: error
          details: Freqtrade doc test failed!
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}


  build-linux-online:
    # Run pytest with "live" checks
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: Install uv
      uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
      with:
        activate-environment: true
        enable-cache: true
        python-version: "3.12"
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "3.12"
        prune-cache: false

    - name: Installation - *nix
      run: |
        uv pip install --upgrade wheel
        uv pip install -r requirements-dev.txt
        uv pip install -e ft_client/
        uv pip install -e .

    - name: Tests incl. ccxt compatibility tests
      env:
        CI_WEB_PROXY: http://152.67.78.211:13128
      run: |
        pytest --random-order --longrun --durations 20 -n auto


  # Notify only once - when CI completes (and after deploy) in case it's successful
  notify-complete:
    needs: [
      build-linux,
      build-macos,
      build-windows,
      docs-check,
      mypy-version-check,
      pre-commit,
      build-linux-online
    ]
    runs-on: ubuntu-22.04
    # Discord notification can't handle schedule events
    if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
    permissions:
      repository-projects: read
    steps:

    - name: Check user permission
      id: check
      continue-on-error: true
      uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
      with:
        permission: "write"
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Discord notification
      uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
      if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          severity: info
          details: Test Completed!
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}

  build:
    name: "Build"
    needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
    runs-on: ubuntu-22.04

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: Build distribution
      run: |
        pip install -U build
        python -m build --sdist --wheel

    - name: Upload artifacts 📦
      uses: actions/upload-artifact@v4
      with:
        name: freqtrade-build
        path: |
          dist
        retention-days: 10

    - name: Build Client distribution
      run: |
        pip install -U build
        python -m build --sdist --wheel ft_client

    - name: Upload artifacts 📦
      uses: actions/upload-artifact@v4
      with:
        name: freqtrade-client-build
        path: |
          ft_client/dist
        retention-days: 10

  deploy-test-pypi:
    name: "Publish Python 🐍 distribution 📦 to TestPyPI"
    needs: [ build ]
    runs-on: ubuntu-22.04
    if: (github.event_name == 'release')
    environment:
      name: testpypi
      url: https://test.pypi.org/p/freqtrade
    permissions:
      id-token: write

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Download artifact  📦
      uses: actions/download-artifact@v5
      with:
        pattern: freqtrade*-build
        path: dist
        merge-multiple: true

    - name: Publish to PyPI (Test)
      uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
      with:
        repository-url: https://test.pypi.org/legacy/


  deploy-pypi:
    name: "Publish Python 🐍 distribution 📦 to PyPI"
    needs: [ build ]
    runs-on: ubuntu-22.04
    if: (github.event_name == 'release')
    environment:
      name: pypi
      url: https://pypi.org/p/freqtrade
    permissions:
      id-token: write

    steps:
    - uses: actions/checkout@v5
      with:
        persist-credentials: false

    - name: Download artifact  📦
      uses: actions/download-artifact@v5
      with:
        pattern: freqtrade*-build
        path: dist
        merge-multiple: true

    - name: Publish to PyPI
      uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4


  docker-build:
    name: "Docker Build and Deploy"
    needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
    if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
    uses: ./.github/workflows/docker-build.yml
    permissions:
      packages: write
      contents: read
    secrets:
      DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
      DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
      DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
