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: {}

jobs:
  tests:
    name: "Tests and Linting"
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ "ubuntu-22.04", "ubuntu-24.04", "ubuntu-26.04", "ubuntu-24.04-arm", "macos-15" , "macos-26", "windows-2022", "windows-2025" ]
        python-version: ["3.12", "3.13", "3.14"]
        exclude:
          - os: "ubuntu-22.04"
            python-version: "3.13"
          - os: "ubuntu-22.04"
            python-version: "3.14"
          # Reduce ARM runs to newer python versions
          - os: "ubuntu-24.04-arm"
            python-version: "3.12"
          # ubuntu 26.04 has 3.14 as default python version
          - os: "ubuntu-26.04"
            python-version: "3.12"
          - os: "ubuntu-26.04"
            python-version: "3.13"


    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Install uv and Python 🐍
      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      with:
        activate-environment: true
        enable-cache: true # zizmor: ignore[cache-poisoning] - this job is not used for release builds
        prune-cache: true
        python-version: ${{ matrix.python-version }}
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "${{ matrix.python-version }}"

    - name: Installation - macOS (Brew)
      if: ${{ runner.os == 'macOS' }}
      run: |
        # brew update
        # TODO: Should be the brew upgrade
        brew install libomp

    - name: Installation (python)
      run: |
        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.14' && matrix.os == 'ubuntu-24.04'))
      run: |
        pytest --random-order --durations 20 -n auto

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

    - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
      if: (runner.os == 'Linux' && matrix.python-version == '3.14' && matrix.os == 'ubuntu-24.04')
      with:
        fail_ci_if_error: true
        token: ${{ secrets.CODECOV_TOKEN }}  # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.

    - name: Cleanup codecov dirty state files
      if: (runner.os == 'Linux' && matrix.python-version == '3.14' && matrix.os == 'ubuntu-24.04')
      run: |
        # See https://github.com/codecov/codecov-action/issues/1851
        rm -rf codecov codecov.SHA256SUM codecov.SHA256SUM.sig

    - name: Run json schema extract
      # This must 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 must be kept before the repository check to ensure that the docs are up-to-date
      if: ${{ (matrix.python-version == '3.13') }}
      run: |
        python build_helpers/create_command_partials.py

    - name: Check for repository changes - *nix
      if: ${{ (runner.os != 'Windows') }}
      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: Check for repository changes - Windows
      if: ${{ runner.os == 'Windows' }}
      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 (multi)
      run: |
        freqtrade create-userdir --userdir user_data
        cp tests/testdata/config.tests.json user_data/config.json
        freqtrade new-strategy -s AwesomeStrategy
        freqtrade new-strategy -s AwesomeStrategyMin --template minimal
        freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
        freqtrade backtesting --datadir tests/testdata --strategy-list AwesomeStrategy AwesomeStrategyMin AwesomeStrategyAdv -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: 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' || matrix.os == 'macos-15' }}
      run: |
        mypy freqtrade scripts tests

    - name: Run Pester tests (PowerShell)
      if: ${{ runner.os == 'Windows' }}
      shell: pwsh
      run: |
        $PSVersionTable
        Install-PSResource -Name Pester -Version '[5.7.1]' -TrustRepository -Reinstall
        $Error.clear()

        # Override Clear-Host globally to prevent headless console crashes
        Function Clear-Host { }

        Invoke-Pester -Path "tests" -CI
        if ($Error.Length -gt 0) {exit 1}

    - name: Discord notification
      uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
      if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
      with:
          color: '#FF0000' # red
          title: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
          webhook: ${{ secrets.DISCORD_WEBHOOK }}  # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.

  mypy-version-check:
    name: "Mypy Version Check"
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Install uv and Python 🐍
      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      with:
        activate-environment: true
        python-version: "3.13"
        enable-cache: false

    - name: pre-commit dependencies
      run: |
        uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
        python build_helpers/pre_commit_update.py

  pre-commit:
    name: "Pre-commit checks"
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Set up Python 🐍
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: "3.14"

    - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1

  docs-check:
    name: "Documentation build"
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

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

    - name: Install uv and Python 🐍
      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      with:
        activate-environment: true
        python-version: "3.13"
        enable-cache: false

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

    - name: Discord notification
      uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
      if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
      with:
          color: '#FF0000' # red
          title: Freqtrade doc test failed!
          webhook: ${{ secrets.DISCORD_WEBHOOK }}  # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.


  build-linux-online:
    # Run pytest with "live" checks
    name: "Online / live tests"
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        python-version: ["3.14"]
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Install uv and Python 🐍
      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      with:
        activate-environment: true
        enable-cache: true # zizmor: ignore[cache-poisoning] - this job is not used for release builds
        prune-cache: true
        python-version: "${{ matrix.python-version }}"
        cache-dependency-glob: "requirements**.txt"
        cache-suffix: "${{ matrix.python-version }}"

    - name: Installation - *nix
      run: |
        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://140.238.213.2: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:
    name: "Notify CI Completion"
    needs: [
      build,
      build-linux-online
    ]
    runs-on: ubuntu-24.04
    # Discord notification can't handle schedule events
    if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
    steps:

    - name: Discord notification
      uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
      if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
      with:
          color: '#00FF00' # green
          title: Test Completed!
          webhook: ${{ secrets.DISCORD_WEBHOOK }}  # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.

  build:
    if: always()
    name: "Build"
    needs: [
      tests,
      docs-check,
      mypy-version-check,
      pre-commit,
    ]
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        python-version: ["3.13"]

    steps:

    - name: Decide whether the needed jobs succeeded or failed
      uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35  # v1.3.0
      with:
        jobs: ${{ toJSON(needs) }}

    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Install uv and Python 🐍
      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
      with:
        activate-environment: true
        python-version: "${{ matrix.python-version }}"
        # Don't cache to avoid cache poisoning
        enable-cache: false

    - name: Build distribution
      run: |
        uv pip install $(grep -E "^build==" requirements-dev.txt)
        python -m build --sdist --wheel

    - name: Upload artifacts 📦
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
      with:
        name: freqtrade-build
        path: |
          dist
        retention-days: 10

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

    - name: Upload artifacts 📦
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
      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 # Needed for pypa/gh-action-pypi-publish

    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Download artifact  📦
      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
      with:
        pattern: freqtrade*-build
        path: dist
        merge-multiple: true

    - name: Publish to PyPI (Test)
      uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
      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 # Needed for pypa/gh-action-pypi-publish

    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false

    - name: Download artifact  📦
      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
      with:
        pattern: freqtrade*-build
        path: dist
        merge-multiple: true

    - name: Publish to PyPI
      uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2


  docker-build:
    name: "Docker Build and Deploy"
    needs: [
     build,
    ]
    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 # Needed to push package versions
      contents: read
    secrets:
      DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}  # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}


  packages-cleanup:
    name: "Docker Package Cleanup"
    uses: $/.github/workflows/packages-cleanup.yml
    # Only run on push, schedule, or release events
    if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
    permissions:
      packages: write # Needed to delete package versions
    with:
      package_name: 'freqtrade'