Merge pull request #12182 from freqtrade/new_release

New release 2025.8
This commit is contained in:
Matthias
2025-08-31 08:25:25 +02:00
committed by GitHub
116 changed files with 8823 additions and 6247 deletions

View File

@@ -29,6 +29,10 @@ updates:
mkdocs: mkdocs:
patterns: patterns:
- "mkdocs*" - "mkdocs*"
scipy:
patterns:
- "scipy"
- "scipy-stubs"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"

View File

@@ -15,7 +15,7 @@ jobs:
environment: environment:
name: develop name: develop
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ["3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -47,24 +47,9 @@ jobs:
cache-suffix: "${{ matrix.python-version }}" cache-suffix: "${{ matrix.python-version }}"
prune-cache: false prune-cache: false
- name: Cache_dependencies
uses: actions/cache@v4
id: cache
with:
path: ~/dependencies/
key: ${{ runner.os }}-dependencies
- name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix - name: Installation - *nix
run: | run: |
uv pip install --upgrade wheel uv pip install --upgrade wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
uv pip install -r requirements-dev.txt uv pip install -r requirements-dev.txt
uv pip install -e ft_client/ uv pip install -e ft_client/
uv pip install -e . uv pip install -e .
@@ -163,7 +148,7 @@ jobs:
python-version: ["3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -174,7 +159,7 @@ jobs:
check-latest: true check-latest: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -183,18 +168,6 @@ jobs:
cache-suffix: "${{ matrix.python-version }}" cache-suffix: "${{ matrix.python-version }}"
prune-cache: false prune-cache: false
- name: Cache_dependencies
uses: actions/cache@v4
id: cache
with:
path: ~/dependencies/
key: ${{ matrix.os }}-dependencies
- name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - macOS (Brew) - name: Installation - macOS (Brew)
run: | run: |
# brew update # brew update
@@ -222,9 +195,6 @@ jobs:
- name: Installation (python) - name: Installation (python)
run: | run: |
uv pip install wheel uv pip install wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
uv pip install -r requirements-dev.txt uv pip install -r requirements-dev.txt
uv pip install -e ft_client/ uv pip install -e ft_client/
uv pip install -e . uv pip install -e .
@@ -287,11 +257,11 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-latest ] os: [ "windows-2022", "windows-2025" ]
python-version: ["3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -301,7 +271,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -315,7 +285,9 @@ jobs:
function uvpipFunction { uv pip $args } function uvpipFunction { uv pip $args }
Set-Alias -name pip -value uvpipFunction Set-Alias -name pip -value uvpipFunction
./build_helpers/install_windows.ps1 python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -e .
- name: Tests - name: Tests
run: | run: |
@@ -379,7 +351,7 @@ jobs:
mypy-version-check: mypy-version-check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -396,7 +368,7 @@ jobs:
pre-commit: pre-commit:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -408,7 +380,7 @@ jobs:
docs-check: docs-check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -439,7 +411,7 @@ jobs:
# Run pytest with "live" checks # Run pytest with "live" checks
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -449,7 +421,7 @@ jobs:
python-version: "3.12" python-version: "3.12"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -458,25 +430,9 @@ jobs:
cache-suffix: "3.12" cache-suffix: "3.12"
prune-cache: false prune-cache: false
- name: Cache_dependencies
uses: actions/cache@v4
id: cache
with:
path: ~/dependencies/
key: ${{ runner.os }}-dependencies
- name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix - name: Installation - *nix
run: | run: |
uv pip install --upgrade wheel uv pip install --upgrade wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
uv pip install -r requirements-dev.txt uv pip install -r requirements-dev.txt
uv pip install -e ft_client/ uv pip install -e ft_client/
uv pip install -e . uv pip install -e .
@@ -508,15 +464,16 @@ jobs:
- name: Check user permission - name: Check user permission
id: check id: check
uses: scherermichael-oss/action-has-permission@136e061bfe093832d87f090dd768e14e27a740d3 # 1.0.6 continue-on-error: true
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
with: with:
required-permission: write permission: "write"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Discord notification - name: Discord notification
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1 uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
severity: info severity: info
details: Test Completed! details: Test Completed!
@@ -528,7 +485,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -575,12 +532,12 @@ jobs:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Download artifact 📦 - name: Download artifact 📦
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
pattern: freqtrade*-build pattern: freqtrade*-build
path: dist path: dist
@@ -604,12 +561,12 @@ jobs:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Download artifact 📦 - name: Download artifact 📦
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
pattern: freqtrade*-build pattern: freqtrade*-build
path: dist path: dist

View File

@@ -19,7 +19,7 @@ jobs:
name: Deploy Docs through mike name: Deploy Docs through mike
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: true persist-credentials: true

View File

@@ -24,11 +24,11 @@ jobs:
packages: write packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@@ -22,11 +22,12 @@ permissions:
jobs: jobs:
deploy-docker: deploy-docker:
name: "Deploy Docker x64 and armv7l"
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: github.repository == 'freqtrade/freqtrade' if: github.repository == 'freqtrade/freqtrade'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@@ -74,7 +75,7 @@ jobs:
build_helpers/publish_docker_multi.sh build_helpers/publish_docker_multi.sh
deploy-arm: deploy-arm:
name: "Deploy Docker" name: "Deploy Docker ARM64"
permissions: permissions:
packages: write packages: write
needs: [ deploy-docker ] needs: [ deploy-docker ]
@@ -83,7 +84,7 @@ jobs:
if: github.repository == 'freqtrade/freqtrade' if: github.repository == 'freqtrade/freqtrade'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -11,7 +11,7 @@ jobs:
dockerHubDescription: dockerHubDescription:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
auto-update: auto-update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -21,9 +21,9 @@ jobs:
# actions: read # only needed for private repos # actions: read # only needed for private repos
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor 🌈 - name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1 uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2

View File

@@ -21,18 +21,18 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.17.0" rev: "v1.17.1"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==6.1.0.20250717 - types-cachetools==6.1.0.20250717
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.32.4.20250611 - types-requests==2.32.4.20250809
- types-tabulate==0.9.0.20241207 - types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20250708 - types-python-dateutil==2.9.0.20250822
- scipy-stubs==1.16.0.2 - scipy-stubs==1.16.1.1
- SQLAlchemy==2.0.41 - SQLAlchemy==2.0.43
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
@@ -44,13 +44,13 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.12.5' rev: 'v0.12.10'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v6.0.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |
@@ -83,6 +83,6 @@ repos:
# Ensure github actions remain safe # Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit - repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.11.0 rev: v1.12.1
hooks: hooks:
- id: zizmor - id: zizmor

View File

@@ -1,4 +1,4 @@
FROM python:3.13.5-slim-bookworm AS base FROM python:3.13.7-slim-bookworm AS base
# Setup env # Setup env
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
@@ -27,11 +27,6 @@ RUN apt-get update \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade pip wheel && pip install --upgrade pip wheel
# Install TA-lib
COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH=/usr/local/lib
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
USER ftuser USER ftuser
@@ -49,7 +44,7 @@ USER ftuser
# Install and execute # Install and execute
COPY --chown=ftuser:ftuser . /freqtrade/ COPY --chown=ftuser:ftuser . /freqtrade/
RUN pip install -e . --user --no-cache-dir --no-build-isolation \ RUN pip install -e . --user --no-cache-dir \
&& mkdir /freqtrade/user_data/ \ && mkdir /freqtrade/user_data/ \
&& freqtrade install-ui && freqtrade install-ui

View File

@@ -4,7 +4,6 @@
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864) [![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.

View File

@@ -1,35 +0,0 @@
if [ -z "$1" ]; then
INSTALL_LOC=/usr/local
else
INSTALL_LOC=${1}
fi
echo "Installing to ${INSTALL_LOC}"
if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
&& echo "Downloading gcc config.guess and config.sub" \
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
&& ./configure --prefix=${INSTALL_LOC}/ \
&& make
if [ $? -ne 0 ]; then
echo "Failed building ta-lib."
cd .. && rm -rf ./ta-lib/
exit 1
fi
if [ -z "$2" ]; then
which sudo && sudo make install || make install
if [ -x "$(command -v apt-get)" ]; then
echo "Updating library path using ldconfig"
sudo ldconfig
fi
else
# Don't install with sudo
make install
fi
cd .. && rm -rf ./ta-lib/
else
echo "TA-lib already installed, skipping installation"
fi

View File

@@ -1,10 +0,0 @@
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade pip
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
pip install -U wheel "numpy<3.0"
pip install --only-binary ta-lib --find-links=build_helpers\ "ta-lib<0.6.0"
pip install -r requirements-dev.txt
pip install -e .

View File

@@ -180,6 +180,16 @@
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.", "description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
"type": "number" "type": "number"
}, },
"recursive_strategy_search": {
"description": "Enable recursive strategy search.",
"type": "boolean"
},
"user_data_dir": {
"description": "Path to the user data directory."
},
"datadir": {
"description": "Path to the data directory."
},
"fee": { "fee": {
"description": "Trading fee percentage. Can help to simulate slippage in backtesting", "description": "Trading fee percentage. Can help to simulate slippage in backtesting",
"type": "number", "type": "number",
@@ -562,6 +572,7 @@
"pairlists": { "pairlists": {
"description": "Configuration for pairlists.", "description": "Configuration for pairlists.",
"type": "array", "type": "array",
"minItems": 1,
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -34,7 +34,7 @@ COPY build_helpers/* /tmp/
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
USER ftuser USER ftuser
RUN pip install --user --no-cache-dir "numpy<3.0" \ RUN pip install --user --prefer-binary --no-cache-dir "numpy<3.0" build \
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \ && pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
&& pip install --user --no-cache-dir -r requirements.txt && pip install --user --no-cache-dir -r requirements.txt
@@ -49,7 +49,7 @@ USER ftuser
# Install and execute # Install and execute
COPY --chown=ftuser:ftuser . /freqtrade/ COPY --chown=ftuser:ftuser . /freqtrade/
RUN pip install -e . --user --no-cache-dir --no-build-isolation\ RUN pip install -e . --user --no-cache-dir \
&& mkdir /freqtrade/user_data/ \ && mkdir /freqtrade/user_data/ \
&& freqtrade install-ui && freqtrade install-ui

View File

@@ -1,10 +0,0 @@
FROM freqtradeorg/freqtrade:develop
# Install dependencies
COPY requirements-dev.txt /freqtrade/
RUN pip install numpy --user --no-cache-dir \
&& pip install -r requirements-dev.txt --user --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []

View File

@@ -46,29 +46,32 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
More options are available by running with the `-h` option. More options are available by running with the `-h` option.
### Using export-filename ### Using backtest-filename
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go By default, `backtesting-analysis` processes the most recent backtest results in the `user_data/backtest_results` directory.
back to a previous backtest output, you need to supply the `--export-filename` option. If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
output file. This allows you to keep historical versions of backtest results and re-analyse
them at a later date:
``` bash ``` bash
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
``` ```
You should see some output similar to below in the logs with the name of the timestamped You should see some output similar to below in the logs with the name of the timestamped
filename that was exported: filename that was exported:
``` ```
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json" 2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
``` ```
You can then use that filename in `backtesting-analysis`: You can then use that filename in `backtesting-analysis`:
``` ```
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
```
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
``` bash
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
``` ```
### Tuning the buy tags and sell tags to display ### Tuning the buy tags and sell tags to display

View File

@@ -105,12 +105,14 @@ Only use this if you're sure you'll not want to plot or analyze your results fur
--- ---
Exporting trades to file specifying a custom filename Exporting trades to file specifying a custom directory
```bash ```bash
freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json freqtrade backtesting --strategy backtesting --export trades --backtest-directory=user_data/custom-backtest-results
``` ```
---
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
--- ---
@@ -158,117 +160,136 @@ The most important in the backtesting is to understand the result.
A backtesting result will look like that: A backtesting result will look like that:
``` ```
================================================ BACKTESTING REPORT ================================================= BACKTESTING REPORT
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | ┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|----------+--------+----------------+------------------+----------------+--------------+--------------------------| ┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | ┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | │ LTC/USDT:USDT │ 16 │ 1.0 │ 56.176 │ 5.62 │ 16:16:00 │ 16 0 0 100 │
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | │ ETC/USDT:USDT │ 12 0.72 │ 30.936 │ 3.09 │ 9:55:00 │ 11 0 1 91.7 │
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | │ ETH/USDT:USDT │ 8 │ 0.66 │ 17.864 │ 1.79 │ 1 day, 13:55:00 7 0 1 87.5 │
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | │ XLM/USDT:USDT │ 10 │ 0.31 │ 11.054 │ 1.11 │ 12:08:00 │ 9 0 1 90.0 │
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | │ BTC/USDT:USDT │ 8 │ 0.21 │ 7.289 │ 0.73 3 days, 1:24:00 6 0 2 75.0 │
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | │ XRP/USDT:USDT │ 9 │ -0.14 │ -7.261 │ -0.73 │ 21:18:00 8 0 1 88.9 │
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | │ DOT/USDT:USDT │ 6 │ -0.4 │ -9.187 │ -0.92 │ 5:35:00 4 0 2 66.7 │
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | │ ADA/USDT:USDT │ 8 │ -1.76 │ -52.098 │ -5.21 │ 11:38:00 │ 6 0 2 75.0 │
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | └───────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | LEFT OPEN TRADES REPORT
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | ┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | ┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | ┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | │ BTC/USDT:USDT │ 1 │ -4.14 │ -9.930 │ -0.99 │ 17 days, 8:00:00 0 0 1 0 │
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | │ ETC/USDT:USDT │ 1 │ -4.24 │ -15.365 │ -1.54 │ 10:40:00 0 0 1 0 │
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | │ DOT/USDT:USDT │ 1 │ -5.29 │ -19.125 │ -1.91 │ 11:30:00 │ 0 0 1 0 │
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | │ TOTAL │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 0 0 3 0 │
============================================= LEFT OPEN TRADES REPORT ============================================= └───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | ENTER TAG STATS
|----------+---------+----------------+------------------+----------------+----------------+---------------------| ┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | ┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | ┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | │ OTHER │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0
==================== EXIT REASON STATS ==================== │ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
| Exit Reason | Exits | Wins | Draws | Losses | └───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
|--------------------+---------+-------+--------+---------| EXIT REASON STATS
| trailing_stop_loss | 205 | 150 | 0 | 55 | ┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
| stop_loss | 166 | 0 | 0 | 166 | ┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
| exit_signal | 56 | 36 | 0 | 20 | ┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
| force_exit | 2 | 0 | 0 | 2 | │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 67 0 0 100 │
│ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
│ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
│ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
MIXED TAG STATS
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
│ │ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
│ │ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
│ │ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
│ TOTAL │ │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
SUMMARY METRICS
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Backtesting from │ 2025-07-01 00:00:00 │
│ Backtesting to │ 2025-08-01 00:00:00 │
│ Trading Mode │ Isolated Futures │
│ Max open trades │ 3 │
│ │ │
│ Total/Daily Avg Trades │ 77 / 2.48 │
│ Starting balance │ 1000 USDT │
│ Final balance │ 1054.774 USDT │
│ Absolute profit │ 54.774 USDT │
│ Total profit % │ 5.48% │
│ CAGR % │ 87.36% │
│ Sortino │ 2.48 │
│ Sharpe │ 3.75 │
│ Calmar │ 40.99 │
│ SQN │ 0.69 │
│ Profit factor │ 1.29 │
│ Expectancy (Ratio) │ 0.71 (0.04) │
│ Avg. daily profit │ 1.767 USDT │
│ Avg. stake amount │ 345.016 USDT │
│ Total trade volume │ 53316.954 USDT │
│ │ │
│ Long / Short trades │ 67 / 10 │
│ Long / Short profit % │ 8.94% / -3.47% │
│ Long / Short profit USDT │ 89.425 / -34.651 │
│ │ │
│ Best Pair │ LTC/USDT:USDT 5.62% │
│ Worst Pair │ ADA/USDT:USDT -5.21% │
│ Best trade │ ETC/USDT:USDT 2.00% │
│ Worst trade │ ADA/USDT:USDT -10.17% │
│ Best day │ 26.91 USDT │
│ Worst day │ -47.741 USDT │
│ Days win/draw/lose │ 20 / 6 / 5 │
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
│ Max Consecutive Wins / Loss │ 36 / 3 │
│ Rejected Entry signals │ 258 │
│ Entry/Exit Timeouts │ 0 / 0 │
│ │ │
│ Min balance │ 1003.168 USDT │
│ Max balance │ 1149.421 USDT │
│ Max % of account underwater │ 8.23% │
│ Absolute drawdown │ 94.647 USDT (8.23%) │
│ Drawdown duration │ 9 days 08:50:00 │
│ Profit at drawdown start │ 149.421 USDT │
│ Profit at drawdown end │ 54.774 USDT │
│ Drawdown start │ 2025-07-22 15:10:00 │
│ Drawdown end │ 2025-08-01 00:00:00 │
│ Market change │ 30.51% │
└───────────────────────────────┴─────────────────────────────────┘
================== SUMMARY METRICS ================== Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
| Metric | Value | STRATEGY SUMMARY
|-----------------------------+---------------------| ┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
| Backtesting from | 2019-01-01 00:00:00 | ┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
| Backtesting to | 2019-05-01 00:00:00 | ┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
| Trading Mode | Spot | │ SampleStrategy │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │ 94.647 USDT 8.23% │
| Max open trades | 3 | └────────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┴────────────────────┘
| | |
| Total/Daily Avg Trades | 429 / 3.575 |
| Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| CAGR % | 460.87% |
| Sortino | 1.88 |
| Sharpe | 2.97 |
| Calmar | 6.29 |
| SQN | 2.45 |
| Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
| Long / Short | 352 / 77 |
| Total profit Long % | 1250.58% |
| Total profit Short % | -15.02% |
| Absolute profit Long | 0.00838792 BTC |
| Absolute profit Short | -0.00076 BTC |
| | |
| Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% |
| Worst Trade | ZEC/BTC -10.25% |
| Best day | 0.00076 BTC |
| Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Max Consecutive Wins / Loss | 3 / 4 |
| Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| Canceled Trade Entries | 34 |
| Canceled Entry Orders | 123 |
| Replaced Entry Orders | 89 |
| | |
| Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
| Max % of account underwater | 25.19% |
| Absolute Drawdown (Account) | 13.33% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% |
=====================================================
``` ```
### Backtesting report table ### Backtesting report table
The 1st table contains all trades the bot made, including "left open trades". The first table contains all trades the bot made, including "left open trades".
The last line will give you the overall performance of your strategy, The last line will give you the overall performance of your strategy,
here: here:
``` ```
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | TOTAL 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 67 0 10 87.0 │
``` ```
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has The bot has made `77` trades for an average duration of `22:12:00`, with a performance of `5.48%` (profit), that means it has earned a total of `54.774 USDT` starting with a capital of 1000 USDT.
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `Avg Profit %` shows the average profit for all trades made. The column `Avg Profit %` shows the average profit for all trades made.
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
In the above results, we have a starting balance of 1000 USDT and an absolute profit of 54.774 USDT - so the `Tot Profit %` will be `(54.774 / 1000) * 100 ~= 5.48%`.
Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set. Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
@@ -284,86 +305,83 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
(55%), there is almost no chance that the bot will ever reach this profit. (55%), there is almost no chance that the bot will ever reach this profit.
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up. Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
### Exit reasons table
The 2nd table contains a recap of exit reasons.
This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the exit signal, or consider disabling it).
### Left open trades table ### Left open trades table
The 3rd table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture. The second table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
These trades are also included in the first table, but are also shown separately in this table for clarity. These trades are also included in the first table, but are also shown separately in this table for clarity.
### Enter tag stats table
The third table provides a breakdown of trades by their entry tags (e.g., `enter_long`, `enter_short`), showing the number of entries, average profit percentage, total profit in the stake currency, total profit percentage, average duration, and the number of wins, draws, and losses for each tag.
### Exit reason stats table
The fourth table contains a recap of exit reasons (e.g., `exit_signal`, `roi`, `stop_loss`, `force_exit`). This table can tell you which area needs additional work (e.g., if many `exit_signal` trades are losses, you should work on improving the exit signal or consider disabling it).
### Mixed tag stats table
The fifth table combines entry tags and exit reasons, providing a detailed view of how different entry tags performed with specific exit reasons. This can help identify which combinations of entry and exit strategies are most effective.
### Summary metrics ### Summary metrics
The last element of the backtest report is the summary metrics table. The last element of the backtest report is the summary metrics table.
It contains some useful key metrics about performance of your strategy on backtesting data. It contains key metrics about the performance of your strategy on backtesting data.
``` ```
================== SUMMARY METRICS ================== ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
| Metric | Value | Metric Value
|-----------------------------+---------------------| ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
| Backtesting from | 2019-01-01 00:00:00 | Backtesting from 2025-07-01 00:00:00
| Backtesting to | 2019-05-01 00:00:00 | Backtesting to 2025-08-01 00:00:00
| Trading Mode | Spot | Trading Mode │ Isolated Futures
| Max open trades | 3 | Max open trades 3
| | |
| Total/Daily Avg Trades | 429 / 3.575 | Total/Daily Avg Trades │ 72 / 2.32 │
| Starting balance | 0.01000000 BTC | Starting balance │ 1000 USDT │
| Final balance | 0.01762792 BTC | Final balance │ 1106.734 USDT
| Absolute profit | 0.00762792 BTC | Absolute profit │ 106.734 USDT
| Total profit % | 76.2% | Total profit % │ 10.67%
| CAGR % | 460.87% | CAGR % │ 230.04%
| Sortino | 1.88 | Sortino │ 4.99
| Sharpe | 2.97 | Sharpe │ 8.00
| Calmar | 6.29 | Calmar │ 77.76
| SQN | 2.45 | SQN │ 1.52
| Profit factor | 1.11 | Profit factor │ 1.79
| Expectancy (Ratio) | -0.15 (-0.05) | Expectancy (Ratio) │ 1.48 (0.07)
| Avg. daily profit | 0.0001 BTC | Avg. daily profit │ 3.443 USDT
| Avg. stake amount | 0.001 BTC | Avg. stake amount │ 363.133 USDT
| Total trade volume | 0.429 BTC | Total trade volume │ 52466.174 USDT
| | |
| Long / Short | 352 / 77 | │ Best Pair │ LTC/USDT:USDT 4.48%
| Total profit Long % | 1250.58% | │ Worst Pair │ ADA/USDT:USDT -1.78%
| Total profit Short % | -15.02% | │ Best trade │ ETC/USDT:USDT 2.00%
| Absolute profit Long | 0.00838792 BTC | │ Worst trade │ ADA/USDT:USDT -10.17%
| Absolute profit Short | -0.00076 BTC | │ Best day │ 23.535 USDT │
| | | │ Worst day │ -49.813 USDT
| Best Pair | LSK/BTC 26.26% | │ Days win/draw/lose │ 21 / 6 / 4 │
| Worst Pair | ZEC/BTC -10.18% | │ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:30 │
| Best Trade | LSK/BTC 4.25% | │ Min/Max/Avg. Duration Losers │ 0d 12:00 / 17d 08:00 / 3d 23:28 │
| Worst Trade | ZEC/BTC -10.25% | │ Max Consecutive Wins / Loss │ 58 / 4
| Best day | 0.00076 BTC | │ Rejected Entry signals │ 254
| Worst day | -0.00036 BTC | │ Entry/Exit Timeouts │ 0 / 0
| Days win/draw/lose | 12 / 82 / 25 | │ │ │
| Avg. Duration Winners | 4:23:00 | │ Min balance │ 1003.168 USDT
| Avg. Duration Loser | 6:55:00 | │ Max balance │ 1209 USDT
| Max Consecutive Wins / Loss | 3 / 4 | Max % of account underwater │ 8.46%
| Rejected Entry signals | 3089 | │ Absolute drawdown │ 102.266 USDT (8.46%)
| Entry/Exit Timeouts | 0 / 0 | │ Drawdown duration │ 9 days 08:50:00
| Canceled Trade Entries | 34 | │ Profit at drawdown start │ 209 USDT
| Canceled Entry Orders | 123 | │ Profit at drawdown end 106.734 USDT
| Replaced Entry Orders | 89 | │ Drawdown start │ 2025-07-22 15:10:00
| | | │ Drawdown end │ 2025-08-01 00:00:00
| Min balance | 0.00945123 BTC | Market change │ 30.51% │
| Max balance | 0.01846651 BTC | └───────────────────────────────┴─────────────────────────────────┘
| Max % of account underwater | 25.19% |
| Absolute Drawdown (Account) | 13.33% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% |
=====================================================
``` ```
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
- `Trading Mode`: Spot or Futures trading. - `Trading Mode`: Spot or Futures trading.
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
- `Final balance`: Final balance - starting balance + absolute profit. - `Final balance`: Final balance - starting balance + absolute profit.
@@ -374,58 +392,71 @@ It contains some useful key metrics about performance of your strategy on backte
- `Sharpe`: Annualized Sharpe ratio. - `Sharpe`: Annualized Sharpe ratio.
- `Calmar`: Annualized Calmar ratio. - `Calmar`: Annualized Calmar ratio.
- `SQN`: System Quality Number (SQN) - by Van Tharp. - `SQN`: System Quality Number (SQN) - by Van Tharp.
- `Profit factor`: profit / loss. - `Profit factor`: Sum of the profits of all winning trades divided by the sum of the losses of all losing trades.
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable. - `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`. - `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`. - `Long / Short trades`: Split long/short trade counts (only shown when short trades were made).
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Long / Short profit %`: Profit percentage for long and short trades (only shown when short trades were made).
- `Long / Short profit USDT`: Profit in stake currency for long and short trades (only shown when short trades were made).
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on total profit percentage), and its corresponding `Tot Profit %`.
- `Best trade` / `Worst trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trades).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Min/Max/Avg. Duration Winners`: Minimum, maximum, and average durations for winning trades.
- `Min/Max/Avg. Duration Losers`: Minimum, maximum, and average durations for losing trades.
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row. - `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached. - `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
- `Canceled Entry Orders`: Number of entry orders that have been canceled by user request via `adjust_entry_price`.
- `Replaced Entry Orders`: Number of entry orders that have been replaced by user request via `adjust_entry_price`.
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. - `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`. - `Absolute drawdown`: Maximum absolute drawdown experienced, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`..
- `Absolute Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. - `Drawdown duration`: Duration of the largest drawdown period.
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point. - `Profit at drawdown start` / `Profit at drawdown end`: Profit at the beginning and end of the largest drawdown period.
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. - `Drawdown start` / `Drawdown end`: Start and end datetime for the largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as the average of all pairs' changes from the first to the last candle using the "close" column.
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
- `Long / Short`: Split long/short values (Only shown when short trades were made).
- `Total profit Long %` / `Absolute profit Long`: Profit long trades only (Only shown when short trades were made).
- `Total profit Short %` / `Absolute profit Short`: Profit short trades only (Only shown when short trades were made).
### Daily / Weekly / Monthly breakdown ### Daily / Weekly / Monthly / Yearly breakdown
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch. You can get an overview over daily, weekly, monthly, or yearly results by using the `--breakdown <>` switch.
To visualize daily and weekly breakdowns, you can use the following: To visualize monthly and yearly breakdowns, you can use the following:
``` bash ``` bash
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day week freqtrade backtesting --strategy MyAwesomeStrategy --breakdown month year
``` ```
``` output ``` output
======================== DAY BREAKDOWN ========================= MONTH BREAKDOWN
| Day | Tot Profit USDT | Wins | Draws | Losses | ┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|------------+-------------------+--------+---------+----------| ┃ Month ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
| 03/07/2021 | 200.0 | 2 | 0 | 0 | ┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
| 04/07/2021 | -50.31 | 0 | 0 | 2 | │ 31/01/2020 │ 12 │ 44.451 │ 7.28 │ 10 0 2 83.3 │
| 05/07/2021 | 220.611 | 3 | 2 | 0 | │ 29/02/2020 │ 30 │ 45.41 │ 2.36 │ 17 0 13 56.7 │
| 06/07/2021 | 150.974 | 3 | 0 | 2 | │ 31/03/2020 │ 35 │ 142.024 2.42 │ 14 0 21 40.0 │
| 07/07/2021 | -70.193 | 1 | 0 | 2 | │ 30/04/2020 │ 67 │ -23.692 │ 0.81 │ 24 0 43 35.8 │
| 08/07/2021 | 212.413 | 2 | 0 | 3 | ...
...
│ 30/04/2025 │ 203 │ -63.43 │ 0.81 │ 73 0 130 36.0 │
│ 31/05/2025 │ 142 │ 104.675 │ 1.28 │ 59 0 83 41.5 │
│ 30/06/2025 │ 177 │ -1.014 │ 1.0 │ 85 0 92 48.0 │
│ 31/07/2025 │ 155 │ 232.762 │ 1.6 │ 63 0 92 40.6 │
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
YEAR BREAKDOWN
┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Year ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 31/12/2020 │ 896 │ 868.889 │ 1.46 │ 351 0 545 39.2 │
│ 31/12/2021 │ 1778 │ 4487.163 │ 1.93 │ 745 0 1033 41.9 │
│ 31/12/2022 │ 1736 │ 938.27 │ 1.27 │ 698 0 1038 40.2 │
│ 31/12/2023 │ 1712 │ 1677.126 │ 1.68 │ 670 0 1042 39.1 │
│ 31/12/2024 │ 1609 │ 3198.424 │ 2.22 │ 773 0 836 48.0 │
│ 31/12/2025 │ 1042 │ 716.174 │ 1.33 │ 420 0 622 40.3 │
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
``` ```
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day. Below that there will be a second table for the summarized values of weeks indicated by the date of the closing Sunday. The same would apply to a monthly breakdown indicated by the last day of the month. The output will display tables containing the realized absolute profit (in stake currency) for the selected period, along with additional statistics such as number of trades, profit factor, and distribution of wins, draws, and losses that materialized (closed) on this period.
### Backtest result caching ### Backtest result caching
@@ -449,10 +480,10 @@ For this mode - `--notes "<notes>"` can be used to add notes to the backtest res
The output file freqtrade produces is a zip file containing the following files: The output file freqtrade produces is a zip file containing the following files:
- The backtest report in json format - The backtest report in json format
- the market change data in feather format - The market change data in feather format
- a copy of the strategy file - A copy of the strategy file
- a copy of the strategy parameters (if a parameter file was used) - A copy of the strategy parameters (if a parameter file was used)
- a sanitized copy of the config file - A sanitized copy of the config file
This will ensure results are reproducible - under the assumption that the same data is available. This will ensure results are reproducible - under the assumption that the same data is available.
@@ -470,7 +501,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
- ROI - ROI
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) - Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
- Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit - Exits are never "below the candle", so a ROI of 2% may result in an exit at 2.4% if low was at 2.4% profit
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate - ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
@@ -536,7 +567,7 @@ freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-deta
This will load 1h data (the main timeframe) as well as 5m data (detail timeframe) for the selected timerange. This will load 1h data (the main timeframe) as well as 5m data (detail timeframe) for the selected timerange.
The strategy will be analyzed with the 1h timeframe. The strategy will be analyzed with the 1h timeframe.
Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe. Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe.
This will allow for a more accurate simulation of intra-candle movements - and can lead to different results, especially on higher timeframes. This will allow for a more accurate simulation of intra-candle movements - and can lead to different results, especially on higher timeframes.
Entries will generally still happen at the main candle's open, however freed trade slots may be freed earlier (if the exit signal is triggered on the 5m candle), which can then be used for a new trade of a different pair. Entries will generally still happen at the main candle's open, however freed trade slots may be freed earlier (if the exit signal is triggered on the 5m candle), which can then be used for a new trade of a different pair.
@@ -599,5 +630,5 @@ Detailed output for all strategies one after the other will be available, so mak
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy? Great, your strategy is profitable. What if the bot can give you the optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)

View File

@@ -2,7 +2,8 @@
usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE] usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
[-V] [-c PATH] [-d PATH] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [--userdir PATH]
[--export-filename PATH] [--backtest-filename PATH]
[--backtest-directory PATH]
[--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]] [--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]]
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]] [--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]] [--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
@@ -14,10 +15,15 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...] --analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]
grouping output - 0: simple wins/losses by enter tag, grouping output - 0: simple wins/losses by enter tag,
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by 1: by enter_tag, 2: by enter_tag and exit_tag, 3: by

View File

@@ -1,15 +1,22 @@
``` ```
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V] usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH] [-c PATH] [-d PATH] [--userdir PATH]
[--export-filename PATH] [--show-pair-list] [--backtest-filename PATH]
[--backtest-directory PATH]
[--show-pair-list]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]] [--breakdown {day,week,month,year} [{day,week,month,year} ...]]
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--show-pair-list Show backtesting pairlist sorted by profit. --show-pair-list Show backtesting pairlist sorted by profit.
--breakdown {day,week,month,year} [{day,week,month,year} ...] --breakdown {day,week,month,year} [{day,week,month,year} ...]
Show backtesting breakdown per [day, week, month, Show backtesting breakdown per [day, week, month,

View File

@@ -14,7 +14,8 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--timeframe-detail TIMEFRAME_DETAIL] [--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades,signals}] [--export {none,trades,signals}]
[--export-filename PATH] [--backtest-filename PATH]
[--backtest-directory PATH]
[--breakdown {day,week,month,year} [{day,week,month,year} ...]] [--breakdown {day,week,month,year} [{day,week,month,year} ...]]
[--cache {none,day,week,month}] [--cache {none,day,week,month}]
[--freqai-backtest-live-models] [--notes TEXT] [--freqai-backtest-live-models] [--notes TEXT]
@@ -61,10 +62,15 @@ options:
becomes `backtest-data-SampleStrategy.json` becomes `backtest-data-SampleStrategy.json`
--export {none,trades,signals} --export {none,trades,signals}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--breakdown {day,week,month,year} [{day,week,month,year} ...] --breakdown {day,week,month,year} [{day,week,month,year} ...]
Show backtesting breakdown per [day, week, month, Show backtesting breakdown per [day, week, month,
year]. year].

View File

@@ -1,11 +1,16 @@
``` ```
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V] usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a] [-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
[--trading-mode {spot,margin,futures}]
[--dex-exchanges]
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-1, --one-column Print output in one column. -1, --one-column Print output in one column.
-a, --all Print all exchanges known to the ccxt library. -a, --all Print all exchanges known to the ccxt library.
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
Select Trading mode
--dex-exchanges Print only DEX exchanges.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@@ -15,7 +15,8 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE]
[--timeframe-detail TIMEFRAME_DETAIL] [--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades,signals}] [--export {none,trades,signals}]
[--export-filename PATH] [--backtest-filename PATH]
[--backtest-directory PATH]
[--freqai-backtest-live-models] [--freqai-backtest-live-models]
[--minimum-trade-amount INT] [--minimum-trade-amount INT]
[--targeted-trade-amount INT] [--targeted-trade-amount INT]
@@ -60,10 +61,15 @@ options:
becomes `backtest-data-SampleStrategy.json` becomes `backtest-data-SampleStrategy.json`
--export {none,trades,signals} --export {none,trades,signals}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--backtest-directory PATH, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.
--freqai-backtest-live-models --freqai-backtest-live-models
Run backtest with ready models. Run backtest with ready models.
--minimum-trade-amount INT --minimum-trade-amount INT

View File

@@ -10,7 +10,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--plot-limit INT] [--db-url PATH] [--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--trade-source {DB,file}]
[--export {none,trades,signals}] [--export {none,trades,signals}]
[--export-filename PATH] [--backtest-filename PATH]
[--timerange TIMERANGE] [-i TIMEFRAME] [--timerange TIMERANGE] [-i TIMEFRAME]
[--no-trades] [--no-trades]
@@ -38,10 +38,12 @@ options:
(backtest file)) Default: file (backtest file)) Default: file
--export {none,trades,signals} --export {none,trades,signals}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
-i TIMEFRAME, --timeframe TIMEFRAME -i TIMEFRAME, --timeframe TIMEFRAME

View File

@@ -6,7 +6,7 @@ usage: freqtrade plot-profit [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--freqaimodel NAME] [--freqaimodel-path PATH] [--freqaimodel NAME] [--freqaimodel-path PATH]
[-p PAIRS [PAIRS ...]] [--timerange TIMERANGE] [-p PAIRS [PAIRS ...]] [--timerange TIMERANGE]
[--export {none,trades,signals}] [--export {none,trades,signals}]
[--export-filename PATH] [--db-url PATH] [--backtest-filename PATH] [--db-url PATH]
[--trade-source {DB,file}] [-i TIMEFRAME] [--trade-source {DB,file}] [-i TIMEFRAME]
[--auto-open] [--auto-open]
@@ -19,10 +19,12 @@ options:
Specify what timerange of data to use. Specify what timerange of data to use.
--export {none,trades,signals} --export {none,trades,signals}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH, --backtest-filename PATH --backtest-filename PATH, --export-filename PATH
Use this filename for backtest results.Requires Use this filename for backtest results.Example:
`--export` to be set as well. Example: `--export-filen `--backtest-
ame=user_data/backtest_results/backtest_today.json` filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
--db-url PATH Override trades database URL, this is useful in custom --db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for

View File

@@ -566,8 +566,8 @@ Configuration:
### Understand order_time_in_force ### Understand order_time_in_force
The `order_time_in_force` configuration parameter defines the policy by which the order The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange.
is executed on the exchange. Three commonly used time in force are: Commonly used time in force are:
**GTC (Good Till Canceled):** **GTC (Good Till Canceled):**
@@ -589,11 +589,13 @@ is automatically cancelled by the exchange.
Post only order. The order is either placed as a maker order, or it is canceled. Post only order. The order is either placed as a maker order, or it is canceled.
This means the order must be placed on orderbook for at least time in an unfilled state. This means the order must be placed on orderbook for at least time in an unfilled state.
Please check the [Exchange documentation](exchanges.md) for supported time in force values for your exchange.
#### time_in_force config #### time_in_force config
The `order_time_in_force` parameter contains a dict with entry and exit time in force policy values. The `order_time_in_force` parameter contains a dict with entry and exit time in force policy values.
This can be set in the configuration file or in the strategy. This can be set in the configuration file or in the strategy.
Values set in the configuration file overwrites values set in the strategy. Values set in the configuration file overwrite values from in the strategy, following the regular [precedence rules](#configuration-option-prevalence).
The possible values are: `GTC` (default), `FOK` or `IOC`. The possible values are: `GTC` (default), `FOK` or `IOC`.
@@ -605,9 +607,9 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
``` ```
!!! Warning !!! Warning
This is ongoing work. For now, it is supported only for binance, gate and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange. Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### Fiat conversion ### Fiat conversion
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports. Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.

View File

@@ -408,6 +408,22 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/tem
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
``` ```
## Backtest documentation results
To generate backtest outputs, please use the following commands:
``` bash
# Assume a dedicated user directory for this output
freqtrade create-userdir --userdir user_data_bttest/
# set can_short = True
sed -i "s/can_short: bool = False/can_short: bool = True/" user_data_bttest/strategies/sample_strategy.py
freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/config.tests.usdt.json --userdir user_data_bttest/ -t 5m
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
```
## Continuous integration ## Continuous integration
This documents some decisions taken for the CI Pipeline. This documents some decisions taken for the CI Pipeline.
@@ -418,7 +434,6 @@ This documents some decisions taken for the CI Pipeline.
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. * Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
* Full docker image rebuilds are run once a week via schedule. * Full docker image rebuilds are run once a week via schedule.
* Deployments run on ubuntu. * Deployments run on ubuntu.
* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability.
* All tests must pass for a PR to be merged to `stable` or `develop`. * All tests must pass for a PR to be merged to `stable` or `develop`.
## Creating a release ## Creating a release

View File

@@ -227,7 +227,7 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
} }
``` ```
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force). Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel) and "IOC" (immediate-or-cancel) settings.
!!! Tip "Stoploss on Exchange" !!! Tip "Stoploss on Exchange"
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
@@ -271,7 +271,9 @@ Using the wrong exchange will result in the error "OKX Error 50119: API key does
## Gate.io ## Gate.io
!!! Tip "Stoploss on Exchange" !!! Tip "Stoploss on Exchange"
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.. Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
Gate.io supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), and "IOC" (immediate-or-cancel) settings.
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
@@ -286,9 +288,15 @@ Without these permissions, the bot will not start correctly and show errors like
## Bybit ## Bybit
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. !!! Tip "Stoploss on Exchange"
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors. Bybit supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
Futures trading on bybit is currently supported for isolated futures mode.
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that manual changes to this setting may result in exceptions and errors.
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
@@ -305,11 +313,6 @@ We do strongly recommend to limit all API keys to the IP you're going to use it
We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts. We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts.
Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior. Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior.
!!! Tip "Stoploss on Exchange"
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
## Bitmart ## Bitmart
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret. Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
@@ -328,6 +331,26 @@ It's therefore required to pass the UID as well.
!!! Warning "Necessary Verification" !!! Warning "Necessary Verification"
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification. Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
## Bitget
Bitget requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
```json
"exchange": {
"name": "bitget",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
// ...
}
```
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
!!! Tip "Stoploss on Exchange"
Bitget supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
## Hyperliquid ## Hyperliquid
!!! Tip "Stoploss on Exchange" !!! Tip "Stoploss on Exchange"

View File

@@ -389,6 +389,8 @@ The `refresh_period` setting defines the interval (in seconds) at which the mark
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied. The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories. If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis, with the prefixes `1000` and `K` being used to identify them.
!!! Warning "Many categories" !!! Warning "Many categories"
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues. Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.

View File

@@ -3,7 +3,6 @@
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864) [![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
<!-- GitHub action buttons --> <!-- GitHub action buttons -->
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm } [:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }

View File

@@ -46,7 +46,6 @@ These requirements apply to both [Script Installation](#script-installation) and
* [pip](https://pip.pypa.io/en/stable/installing/) * [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib))
### Install code ### Install code
@@ -201,35 +200,6 @@ This option will hard reset your branch (only if you are on either `stable` or `
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository). Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
### Install TA-Lib
#### TA-Lib script installation
```bash
sudo ./build_helpers/install_ta-lib.sh
```
!!! Note
This will use the ta-lib tar.gz included in this repository.
##### TA-Lib manual installation
[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html)
```bash
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz
cd ta-lib
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
./configure --prefix=/usr/local
make
sudo make install
# On debian based systems (debian, ubuntu, ...) - updating ldconfig might be necessary.
sudo ldconfig
cd ..
rm -rf ./ta-lib*
```
### Setup Python virtual environment (virtualenv) ### Setup Python virtual environment (virtualenv)
You will run freqtrade in separated `virtual environment` You will run freqtrade in separated `virtual environment`
@@ -332,16 +302,6 @@ python3 -m pip install -r requirements.txt
python3 -m pip install -e . python3 -m pip install -e .
``` ```
Patch conda libta-lib (Linux only)
```bash
# Ensure that the environment is active!
conda activate freqtrade
cd build_helpers
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
```
[You are now ready](#you-are-ready) to run the bot. [You are now ready](#you-are-ready) to run the bot.
### Important shortcuts ### Important shortcuts

View File

@@ -38,7 +38,7 @@ Many strategies, without the programmer knowing, have fallen prey to lookahead b
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes. This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset. The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
If the programmer is not careful or oblivious how things work internally If the programmer is not careful or oblivious how things work internally
(which sometimes can be really hard to find out) then the strategy will look into the future. (which sometimes can be really hard to find out) then the strategy will look into the future.
This command is made to try to verify the validity in the form of the aforementioned lookahead bias. This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
@@ -50,8 +50,7 @@ After this initial backtest runs, it will look if the `minimum-trade-amount` is
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur. If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
After setting the baseline it will then do additional backtest runs for every entry and exit separately. After setting the baseline it will then do additional backtest runs for every entry and exit separately.
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit) When these verification backtests complete, it will compare both dataframes (baseline and sliced) for any difference in columns' value and report the bias.
and report the bias.
After all signals have been verified or falsified a result table will be generated for the user to see. After all signals have been verified or falsified a result table will be generated for the user to see.
### How to find and remove bias? How can I salvage a biased strategy? ### How to find and remove bias? How can I salvage a biased strategy?
@@ -98,8 +97,8 @@ If the strategy has many different signals / signal types, it's up to you to sel
This would lead to a false-negative, i.e. the strategy will be reported as non-biased. This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems. - `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
Please don't use any options like enabling position stacking as this will distort the number of checked signals. Please don't use any options like enabling position stacking as this will distort the number of checked signals.
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots, If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
and that you have enough capital in the backtest wallet configuration. and that you have enough capital in the backtest wallet configuration.
- In the results table, the `biased_indicators` column - In the results table, the `biased_indicators` column
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased. will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
**These are not biased and can safely be ignored.** **These are not biased and can safely be ignored.**

View File

@@ -1,7 +1,7 @@
markdown==3.8.2 markdown==3.8.2
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.6.16 mkdocs-material==9.6.18
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.16 pymdown-extensions==10.16.1
jinja2==3.1.6 jinja2==3.1.6
mike==2.1.3 mike==2.1.3

View File

@@ -31,6 +31,7 @@ The Order-type will be ignored if only one mode is available.
| Binance | limit | | Binance | limit |
| Binance Futures | market, limit | | Binance Futures | market, limit |
| Bingx | market, limit | | Bingx | market, limit |
| Bitget | market, limit |
| HTX | limit | | HTX | limit |
| kraken | market, limit | | kraken | market, limit |
| Gate | limit | | Gate | limit |

View File

@@ -47,3 +47,8 @@
border-color: #afb8c1; border-color: #afb8c1;
box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2); box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2);
} }
.md-grid {
/* default is max-width: 61rem; */
max-width: 75rem;
}

View File

@@ -42,7 +42,3 @@ freqtrade install-ui
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib). Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
Please refer to the corresponding installation sections (common problems linked below) Please refer to the corresponding installation sections (common problems linked below)
Common problems and their solutions:
* [ta-lib update on windows](windows_installation.md#install-ta-lib)

View File

@@ -38,30 +38,6 @@ cd freqtrade
!!! Hint !!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information. Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
### Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.11, 3.12 and 3.13) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
Other versions must be downloaded from the above link.
``` powershell
cd \path\freqtrade
python -m venv .venv
.venv\Scripts\activate.ps1
# optionally install ta-lib from wheel
# Eventually adjust the below filename to match the downloaded wheel
pip install --find-links build_helpers\ TA-Lib -U
pip install -r requirements.txt
pip install -e .
freqtrade
```
!!! Note "Use Powershell"
The above installation script assumes you're using powershell on a 64bit windows.
Commands for the legacy CMD windows console may differ.
### Error during installation on Windows ### Error during installation on Windows

View File

@@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2025.7" __version__ = "2025.8"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path

View File

@@ -17,7 +17,7 @@ def start_analysis_entries_exits(args: dict[str, Any]) -> None:
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
# Initialize configuration # Initialize configuration
config = setup_utils_configuration(args, RunMode.BACKTEST) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
logger.info("Starting freqtrade in analysis mode") logger.info("Starting freqtrade in analysis mode")

View File

@@ -54,6 +54,7 @@ ARGS_BACKTEST = [
"strategy_list", "strategy_list",
"export", "export",
"exportfilename", "exportfilename",
"exportdirectory",
"backtest_breakdown", "backtest_breakdown",
"backtest_cache", "backtest_cache",
"freqai_backtest_live_models", "freqai_backtest_live_models",
@@ -94,9 +95,14 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column"]
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"] ARGS_BACKTEST_SHOW = [
"exportfilename",
"exportdirectory",
"backtest_show_pair_list",
"backtest_breakdown",
]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
@@ -233,6 +239,7 @@ ARGS_HYPEROPT_SHOW = [
ARGS_ANALYZE_ENTRIES_EXITS = [ ARGS_ANALYZE_ENTRIES_EXITS = [
"exportfilename", "exportfilename",
"exportdirectory",
"analysis_groups", "analysis_groups",
"enter_reason_list", "enter_reason_list",
"exit_reason_list", "exit_reason_list",

View File

@@ -199,22 +199,29 @@ AVAILABLE_CLI_OPTIONS = {
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`", "(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
nargs="+", nargs="+",
), ),
"export": Arg(
"--export",
help="Export backtest results (default: trades).",
choices=constants.EXPORT_OPTIONS,
),
"backtest_notes": Arg( "backtest_notes": Arg(
"--notes", "--notes",
help="Add notes to the backtest results.", help="Add notes to the backtest results.",
metavar="TEXT", metavar="TEXT",
), ),
"export": Arg(
"--export",
help="Export backtest results (default: trades).",
choices=constants.EXPORT_OPTIONS,
),
"exportdirectory": Arg(
"--backtest-directory",
"--export-directory",
help="Directory to use for backtest results. "
"Example: `--export-directory=user_data/backtest_results/`. ",
metavar="PATH",
),
"exportfilename": Arg( "exportfilename": Arg(
"--export-filename",
"--backtest-filename", "--backtest-filename",
"--export-filename",
help="Use this filename for backtest results." help="Use this filename for backtest results."
"Requires `--export` to be set as well. " "Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`", "Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
metavar="PATH", metavar="PATH",
), ),
"disableparamexport": Arg( "disableparamexport": Arg(
@@ -369,6 +376,11 @@ AVAILABLE_CLI_OPTIONS = {
help="Print all exchanges known to the ccxt library.", help="Print all exchanges known to the ccxt library.",
action="store_true", action="store_true",
), ),
"dex_exchanges": Arg(
"--dex-exchanges",
help="Print only DEX exchanges.",
action="store_true",
),
# List pairs / markets # List pairs / markets
"list_pairs_all": Arg( "list_pairs_all": Arg(
"-a", "-a",

View File

@@ -6,7 +6,7 @@ from typing import Any
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
from freqtrade.enums import CandleType, RunMode, TradingMode from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import ConfigurationError from freqtrade.exceptions import ConfigurationError
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -134,7 +134,8 @@ def start_list_data(args: dict[str, Any]) -> None:
config["datadir"], config.get("trading_mode", TradingMode.SPOT) config["datadir"], config.get("trading_mode", TradingMode.SPOT)
) )
if args["pairs"]: if args["pairs"]:
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]] pl = expand_pairlist(args["pairs"], [p[0] for p in paircombs], keep_invalid=True)
paircombs = [comb for comb in paircombs if comb[0] in pl]
title = f"Found {len(paircombs)} pair / timeframe combinations." title = f"Found {len(paircombs)} pair / timeframe combinations."
if not config.get("show_timerange"): if not config.get("show_timerange"):
groupedpair = defaultdict(list) groupedpair = defaultdict(list)
@@ -197,7 +198,8 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
) )
if args["pairs"]: if args["pairs"]:
paircombs = [comb for comb in paircombs if comb in args["pairs"]] pl = expand_pairlist(args["pairs"], [p for p in paircombs], keep_invalid=True)
paircombs = [comb for comb in paircombs if comb in pl]
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}." title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
if not config.get("show_timerange"): if not config.get("show_timerange"):

View File

@@ -46,7 +46,18 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
table.add_column("Markets") table.add_column("Markets")
table.add_column("Reason") table.add_column("Reason")
trading_mode = args.get("trading_mode", None)
dex_only = args.get("dex_exchanges", False)
for exchange in available_exchanges: for exchange in available_exchanges:
if trading_mode and not any(
a["trading_mode"] == trading_mode for a in exchange["trade_modes"]
):
# If trading_mode is specified, only show exchanges that support it
continue
if dex_only and not exchange.get("dex", False):
# If dex_only is specified, only show DEX exchanges
continue
name = Text(exchange["name"]) name = Text(exchange["name"])
if exchange["supported"]: if exchange["supported"]:
name.append(" (Supported)", style="italic") name.append(" (Supported)", style="italic")
@@ -135,6 +146,9 @@ def start_list_strategies(args: dict[str, Any]) -> None:
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, not args["print_one_column"], config.get("recursive_strategy_search", False) config, not args["print_one_column"], config.get("recursive_strategy_search", False)
) )
if not strategy_objs:
logger.warning("No strategies found.")
return
# Sort alphabetically # Sort alphabetically
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"]) strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
for obj in strategy_objs: for obj in strategy_objs:

View File

@@ -72,7 +72,7 @@ def start_backtesting_show(args: dict[str, Any]) -> None:
from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.data.btanalysis import load_backtest_stats
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
results = load_backtest_stats(config["exportfilename"]) results = load_backtest_stats(config["exportdirectory"], config["exportfilename"])
show_backtest_results(config, results) show_backtest_results(config, results)
show_sorted_pairlist(config, results) show_sorted_pairlist(config, results)

View File

@@ -157,6 +157,16 @@ CONF_SCHEMA = {
"description": f"Offset for profit exit. {__IN_STRATEGY}", "description": f"Offset for profit exit. {__IN_STRATEGY}",
"type": "number", "type": "number",
}, },
"recursive_strategy_search": {
"description": "Enable recursive strategy search.",
"type": "boolean",
},
"user_data_dir": {
"description": "Path to the user data directory.",
},
"datadir": {
"description": "Path to the data directory.",
},
"fee": { "fee": {
"description": "Trading fee percentage. Can help to simulate slippage in backtesting", "description": "Trading fee percentage. Can help to simulate slippage in backtesting",
"type": "number", "type": "number",
@@ -443,6 +453,7 @@ CONF_SCHEMA = {
"pairlists": { "pairlists": {
"description": "Configuration for pairlists.", "description": "Configuration for pairlists.",
"type": "array", "type": "array",
"minItems": 1,
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1371,6 +1382,7 @@ SCHEMA_TRADE_REQUIRED = [
"entry_pricing", "entry_pricing",
"stoploss", "stoploss",
"minimal_roi", "minimal_roi",
"pairlists",
"internals", "internals",
"dataformat_ohlcv", "dataformat_ohlcv",
"dataformat_trades", "dataformat_trades",
@@ -1380,6 +1392,7 @@ SCHEMA_BACKTEST_REQUIRED = [
"exchange", "exchange",
"stake_currency", "stake_currency",
"stake_amount", "stake_amount",
"pairlists",
"dry_run_wallet", "dry_run_wallet",
"dataformat_ohlcv", "dataformat_ohlcv",
"dataformat_trades", "dataformat_trades",

View File

@@ -66,7 +66,8 @@ def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> d
return conf return conf
except ValidationError as e: except ValidationError as e:
logger.critical(f"Invalid configuration. Reason: {e}") logger.critical(f"Invalid configuration. Reason: {e}")
raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message) result = best_match(FreqtradeValidator(conf_schema).iter_errors(conf))
raise ConfigurationError(result.message)
def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None: def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None:

View File

@@ -84,9 +84,6 @@ class Configuration:
if "internals" not in config: if "internals" not in config:
config["internals"] = {} config["internals"] = {}
if "pairlists" not in config:
config["pairlists"] = []
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config["original_config"] = deepcopy(config) config["original_config"] = deepcopy(config)
@@ -212,13 +209,28 @@ class Configuration:
config.update({"datadir": create_datadir(config, self.args.get("datadir"))}) config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
logger.info("Using data directory: %s ...", config.get("datadir")) logger.info("Using data directory: %s ...", config.get("datadir"))
self._args_to_config(
config, argname="exportdirectory", logstring="Using {} as backtest directory ..."
)
if self.args.get("exportfilename"): if self.args.get("exportfilename"):
self._args_to_config( self._args_to_config(
config, argname="exportfilename", logstring="Storing backtest results to {} ..." config, argname="exportfilename", logstring="Storing backtest results to {} ..."
) )
config["exportfilename"] = Path(config["exportfilename"]) config["exportfilename"] = Path(config["exportfilename"])
else: if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
config["exportfilename"] = config["user_data_dir"] / "backtest_results" logger.warning(
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
"use `--backtest-directory` instead."
)
if config.get("exportdirectory") is None:
# Fallback - assign export-directory directly.
config["exportdirectory"] = config["exportfilename"]
if not config.get("exportdirectory"):
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
if not config.get("exportfilename"):
config["exportfilename"] = None
config["exportdirectory"] = Path(config["exportdirectory"])
if self.args.get("show_sensitive"): if self.args.get("show_sensitive"):
logger.warning( logger.warning(

View File

@@ -16,10 +16,7 @@ from .bt_fileutils import (
load_backtest_data, load_backtest_data,
load_backtest_metadata, load_backtest_metadata,
load_backtest_stats, load_backtest_stats,
load_exit_signal_candles,
load_file_from_zip, load_file_from_zip,
load_rejected_signals,
load_signal_candles,
load_trades, load_trades,
load_trades_from_db, load_trades_from_db,
trade_list_to_dataframe, trade_list_to_dataframe,

View File

@@ -155,33 +155,55 @@ def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
raise OperationalException("Unexpected error while loading backtest metadata.") from e raise OperationalException("Unexpected error while loading backtest metadata.") from e
def load_backtest_stats(filename: Path | str) -> BacktestResultType: def _normalize_filename(file_or_directory: Path | str, filename: Path | str | None) -> Path:
"""
Normalize the filename by ensuring it is a Path object.
:param file_or_directory: The directory or file to normalize.
:param filename: The filename to normalize.
:return: A Path object representing the normalized filename.
"""
if isinstance(file_or_directory, str):
file_or_directory = Path(file_or_directory)
if file_or_directory.is_dir():
if not filename:
filename = get_latest_backtest_filename(file_or_directory)
if Path(filename).is_file():
fn = Path(filename)
else:
fn = file_or_directory / filename
else:
fn = file_or_directory
return fn
def load_backtest_stats(
file_or_directory: Path | str, filename: Path | str | None = None
) -> BacktestResultType:
""" """
Load backtest statistics file. Load backtest statistics file.
:param filename: pathlib.Path object, or string pointing to the file. :param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: a dictionary containing the resulting file. :return: a dictionary containing the resulting file.
""" """
if isinstance(filename, str): fn = _normalize_filename(file_or_directory, filename)
filename = Path(filename)
if filename.is_dir():
filename = filename / get_latest_backtest_filename(filename)
if not filename.is_file():
raise ValueError(f"File {filename} does not exist.")
logger.info(f"Loading backtest result from {filename}")
if filename.suffix == ".zip": if not fn.is_file():
raise ValueError(f"File or directory {fn} does not exist.")
logger.info(f"Loading backtest result from {fn}")
if fn.suffix == ".zip":
data = json_load( data = json_load(
StringIO( StringIO(load_file_from_zip(fn, fn.with_suffix(".json").name).decode("utf-8"))
load_file_from_zip(filename, filename.with_suffix(".json").name).decode("utf-8")
)
) )
else: else:
with filename.open() as file: with fn.open() as file:
data = json_load(file) data = json_load(file)
# Legacy list format does not contain metadata. # Legacy list format does not contain metadata.
if isinstance(data, dict): if isinstance(data, dict):
data["metadata"] = load_backtest_metadata(filename) data["metadata"] = load_backtest_metadata(fn)
return data return data
@@ -362,16 +384,21 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
return df return df
def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame: def load_backtest_data(
file_or_directory: Path | str, strategy: str | None = None, filename: Path | str | None = None
) -> pd.DataFrame:
""" """
Load backtest data file. Load backtest data file, returns a dataframe with the individual trades.
:param filename: pathlib.Path object, or string pointing to a file or directory :param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param strategy: Strategy to load - mainly relevant for multi-strategy backtests :param strategy: Strategy to load - mainly relevant for multi-strategy backtests
Can also serve as protection to load the correct result. Can also serve as protection to load the correct result.
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: a dataframe with the analysis results :return: a dataframe with the analysis results
:raise: ValueError if loading goes wrong. :raise: ValueError if loading goes wrong.
""" """
data = load_backtest_stats(filename) data = load_backtest_stats(file_or_directory, filename)
if not isinstance(data, list): if not isinstance(data, list):
# new, nested format # new, nested format
if "strategy" not in data: if "strategy" not in data:
@@ -430,20 +457,23 @@ def load_file_from_zip(zip_path: Path, filename: str) -> bytes:
raise ValueError(f"Bad zip file: {zip_path}.") from None raise ValueError(f"Bad zip file: {zip_path}.") from None
def load_backtest_analysis_data(backtest_dir: Path, name: str): def load_backtest_analysis_data(
file_or_directory: Path,
name: Literal["signals", "rejected", "exited"],
filename: Path | str | None = None,
):
""" """
Load backtest analysis data either from a pickle file or from within a zip file Load backtest analysis data either from a pickle file or from within a zip file
:param backtest_dir: Directory containing backtest results :param file_or_directory: pathlib.Path object, or string pointing to the directory,
or absolute/relative path to the backtest results file.
:param name: Name of the analysis data to load (signals, rejected, exited) :param name: Name of the analysis data to load (signals, rejected, exited)
:param filename: Optional filename to load from (if different from the main filename).
Only valid when loading from a directory.
:return: Analysis data :return: Analysis data
""" """
import joblib import joblib
if backtest_dir.is_dir(): zip_path = _normalize_filename(file_or_directory, filename)
lbf = Path(get_latest_backtest_filename(backtest_dir))
zip_path = backtest_dir / lbf
else:
zip_path = backtest_dir
if zip_path.suffix == ".zip": if zip_path.suffix == ".zip":
# Load from zip file # Load from zip file
@@ -458,10 +488,10 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
else: else:
# Load from separate pickle file # Load from separate pickle file
if backtest_dir.is_dir(): if file_or_directory.is_dir():
scpf = Path(backtest_dir, f"{zip_path.stem}_{name}.pkl") scpf = Path(file_or_directory, f"{zip_path.stem}_{name}.pkl")
else: else:
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl") scpf = Path(file_or_directory.parent / f"{file_or_directory.stem}_{name}.pkl")
try: try:
with scpf.open("rb") as scp: with scpf.open("rb") as scp:
@@ -473,27 +503,6 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
return None return None
def load_rejected_signals(backtest_dir: Path):
"""
Load rejected signals from backtest directory
"""
return load_backtest_analysis_data(backtest_dir, "rejected")
def load_signal_candles(backtest_dir: Path):
"""
Load signal candles from backtest directory
"""
return load_backtest_analysis_data(backtest_dir, "signals")
def load_exit_signal_candles(backtest_dir: Path) -> dict[str, dict[str, pd.DataFrame]]:
"""
Load exit signal candles from backtest directory
"""
return load_backtest_analysis_data(backtest_dir, "exited")
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame: def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
""" """
Convert list of Trade objects to pandas Dataframe Convert list of Trade objects to pandas Dataframe

View File

@@ -11,7 +11,7 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
# count the number of significant digits for the open and close prices # count the number of significant digits for the open and close prices
for col in ["open", "high", "low", "close"]: for col in ["open", "high", "low", "close"]:
candles[f"{col}_count"] = ( candles[f"{col}_count"] = (
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len() candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
) )
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max( candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
axis=1 axis=1

View File

@@ -7,11 +7,9 @@ from freqtrade.configuration import TimeRange
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.data.btanalysis import ( from freqtrade.data.btanalysis import (
BT_DATA_COLUMNS, BT_DATA_COLUMNS,
load_backtest_analysis_data,
load_backtest_data, load_backtest_data,
load_backtest_stats, load_backtest_stats,
load_exit_signal_candles,
load_rejected_signals,
load_signal_candles,
) )
from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.util import print_df_rich_table from freqtrade.util import print_df_rich_table
@@ -332,7 +330,7 @@ def process_entry_exit_reasons(config: Config):
do_rejected = config.get("analysis_rejected", False) do_rejected = config.get("analysis_rejected", False)
to_csv = config.get("analysis_to_csv", False) to_csv = config.get("analysis_to_csv", False)
csv_path = Path( csv_path = Path(
config.get("analysis_csv_path", config["exportfilename"]), # type: ignore[arg-type] config.get("analysis_csv_path", config["exportdirectory"]), # type: ignore[arg-type]
) )
if entry_only is True and exit_only is True: if entry_only is True and exit_only is True:
@@ -346,20 +344,30 @@ def process_entry_exit_reasons(config: Config):
None if config.get("timerange") is None else str(config.get("timerange")) None if config.get("timerange") is None else str(config.get("timerange"))
) )
try: try:
backtest_stats = load_backtest_stats(config["exportfilename"]) backtest_stats = load_backtest_stats(
config["exportdirectory"], config["exportfilename"]
)
except ValueError as e: except ValueError as e:
raise ConfigurationError(e) from e raise ConfigurationError(e) from e
for strategy_name, results in backtest_stats["strategy"].items(): for strategy_name, results in backtest_stats["strategy"].items():
trades = load_backtest_data(config["exportfilename"], strategy_name) trades = load_backtest_data(
config["exportdirectory"], strategy_name, config["exportfilename"]
)
if trades is not None and not trades.empty: if trades is not None and not trades.empty:
signal_candles = load_signal_candles(config["exportfilename"]) signal_candles = load_backtest_analysis_data(
exit_signals = load_exit_signal_candles(config["exportfilename"]) config["exportdirectory"], "signals", config["exportfilename"]
)
exit_signals = load_backtest_analysis_data(
config["exportdirectory"], "exited", config["exportfilename"]
)
rej_df = None rej_df = None
if do_rejected: if do_rejected:
rejected_signals_dict = load_rejected_signals(config["exportfilename"]) rejected_signals_dict = load_backtest_analysis_data(
config["exportdirectory"], "rejected", config["exportfilename"]
)
rej_df = prepare_results( rej_df = prepare_results(
rejected_signals_dict, rejected_signals_dict,
strategy_name, strategy_name,

View File

@@ -97,7 +97,7 @@ def load_data(
""" """
result: dict[str, DataFrame] = {} result: dict[str, DataFrame] = {}
if startup_candles > 0 and timerange: if startup_candles > 0 and timerange:
logger.info(f"Using indicator startup period: {startup_candles} ...") logger.debug(f"Using indicator startup period: {startup_candles} ...")
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)

View File

@@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange
# isort: on # isort: on
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bingx import Bingx from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitget import Bitget
from freqtrade.exchange.bitmart import Bitmart from freqtrade.exchange.bitmart import Bitmart
from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bitvavo import Bitvavo
@@ -45,4 +46,4 @@ from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno from freqtrade.exchange.luno import Luno
from freqtrade.exchange.modetrade import Modetrade from freqtrade.exchange.modetrade import Modetrade
from freqtrade.exchange.okx import Okx from freqtrade.exchange.okx import MyOkx, Okx

View File

@@ -45,7 +45,6 @@ class Binance(Exchange):
"funding_fee_candle_limit": 1000, "funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"stoploss_blocks_assets": False, # Stoploss orders do not block assets "stoploss_blocks_assets": False, # Stoploss orders do not block assets
"order_time_in_force": ["GTC", "FOK", "IOC"],
"tickers_have_price": False, "tickers_have_price": False,
"floor_leverage": True, "floor_leverage": True,
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days "fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
import logging
from datetime import timedelta
import ccxt
from freqtrade.enums import CandleType
from freqtrade.exceptions import (
DDosProtection,
OperationalException,
RetryableOrderError,
TemporaryError,
)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts
logger = logging.getLogger(__name__)
class Bitget(Exchange):
"""
Bitget exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: FtHas = {
"stoploss_on_exchange": True,
"stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice",
"stoploss_order_types": {"limit": "limit", "market": "market"},
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
}
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int:
"""
Exchange ohlcv candle limit
bitget has the following behaviour:
* 1000 candles for up-to-date data
* 200 candles for historic data (prior to a certain date)
:param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer
"""
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
days = timeframe_map.get(timeframe, 30)
if candle_type in (CandleType.FUTURES, CandleType.SPOT, CandleType.MARK) and (
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
):
return 1000
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
if order.get("status", "open") == "closed":
# Use orderID as cliendOrderId filter to fetch the regular followup order.
# Could be done with "fetch_order" - but clientOid as filter doesn't seem to work
# https://www.bitget.com/api-doc/spot/trade/Get-Order-Info
for method in (
self._api.fetch_canceled_and_closed_orders,
self._api.fetch_open_orders,
):
orders = method(pair)
orders_f = [order for order in orders if order["clientOrderId"] == order_id]
if orders_f:
order_reg = orders_f[0]
self._log_exchange_response("fetch_stoploss_order1", order_reg)
order_reg["id_stop"] = order_reg["id"]
order_reg["id"] = order_id
order_reg["type"] = "stoploss"
order_reg["status_stop"] = "triggered"
return order_reg
order = self._order_contracts_to_amount(order)
order["type"] = "stoploss"
return order
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
params2 = {
"stop": True,
}
for method in (
self._api.fetch_open_orders,
self._api.fetch_canceled_and_closed_orders,
):
try:
orders = method(pair, params=params2)
orders_f = [order for order in orders if order["id"] == order_id]
if orders_f:
order = orders_f[0]
self._log_exchange_response("get_stop_order_fallback", order)
return self._convert_stop_order(pair, order_id, order)
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
@retrier(retries=API_RETRY_COUNT)
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self._config["dry_run"]:
return self.fetch_dry_run_order(order_id)
return self._fetch_stop_order_fallback(order_id, pair)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})

View File

@@ -46,10 +46,9 @@ BAD_EXCHANGES = {
MAP_EXCHANGE_CHILDCLASS = { MAP_EXCHANGE_CHILDCLASS = {
"binanceus": "binance", "binanceus": "binance",
"binanceje": "binance",
"binanceusdm": "binance", "binanceusdm": "binance",
"okex": "okx", "okex": "okx",
"myokx": "okx", "okxus": "okx",
"gateio": "gate", "gateio": "gate",
"huboi": "htx", "huboi": "htx",
} }
@@ -64,6 +63,7 @@ SUPPORTED_EXCHANGES = [
"hyperliquid", "hyperliquid",
"kraken", "kraken",
"okx", "okx",
"myokx",
] ]
# either the main, or replacement methods (array) is required # either the main, or replacement methods (array) is required

View File

@@ -213,9 +213,9 @@ def amount_to_precision(
amount = float( amount = float(
decimal_to_precision( decimal_to_precision(
amount, amount,
rounding_mode=TRUNCATE, TRUNCATE, # rounding_mode
precision=precision, precision, # numPrecisionDigits
counting_mode=precisionMode, precisionMode, # counting_mode
) )
) )
@@ -311,11 +311,11 @@ def price_to_precision(
return float( return float(
decimal_to_precision( decimal_to_precision(
price, price,
rounding_mode=rounding_mode, rounding_mode, # rounding mode
precision=int(price_precision) int(price_precision)
if precisionMode != TICK_SIZE if precisionMode != TICK_SIZE
else price_precision, else price_precision, # numPrecisionDigits
counting_mode=precisionMode, precisionMode, # counting_mode
) )
) )

View File

@@ -287,3 +287,14 @@ class Okx(Exchange):
orders_open = self._api.fetch_open_orders(pair, since=since_ms) orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open) orders.extend(orders_open)
return orders return orders
class MyOkx(Okx):
"""
MyOkx exchange class.
Minimal adjustment to disable futures trading for the EU subsidiary of Okx
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
]

View File

@@ -493,7 +493,7 @@ class FreqaiDataDrawer:
dk.data["data_path"] = str(dk.data_path) dk.data["data_path"] = str(dk.data_path)
dk.data["model_filename"] = str(dk.model_filename) dk.data["model_filename"] = str(dk.model_filename)
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns) dk.data["training_features_list"] = dk.training_features_list
dk.data["label_list"] = dk.label_list dk.data["label_list"] = dk.label_list
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp: with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:

View File

@@ -514,12 +514,7 @@ class IFreqaiModel(ABC):
current coin/bot loop current coin/bot loop
""" """
if "training_features_list_raw" in dk.data: if dk.training_features_list != dk.data["training_features_list"]:
feature_list = dk.data["training_features_list_raw"]
else:
feature_list = dk.data["training_features_list"]
if dk.training_features_list != feature_list:
raise OperationalException( raise OperationalException(
"Trying to access pretrained model with `identifier` " "Trying to access pretrained model with `identifier` "
"but found different features furnished by current strategy. " "but found different features furnished by current strategy. "

View File

@@ -13,8 +13,8 @@ from freqtrade.loggers.set_log_levels import (
reduce_verbosity_for_bias_tester, reduce_verbosity_for_bias_tester,
restore_verbosity_for_bias_tester, restore_verbosity_for_bias_tester,
) )
from freqtrade.optimize.analysis.base_analysis import BaseAnalysis, VarHolder
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.optimize.base_analysis import BaseAnalysis, VarHolder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -70,34 +70,29 @@ class LookaheadAnalysis(BaseAnalysis):
cut_df: DataFrame = cut_vars.indicators[current_pair] cut_df: DataFrame = cut_vars.indicators[current_pair]
full_df: DataFrame = full_vars.indicators[current_pair] full_df: DataFrame = full_vars.indicators[current_pair]
# cut longer dataframe to length of the shorter # trim full_df to the same index and length as cut_df
full_df_cut = full_df[(full_df.date == cut_vars.compared_dt)].reset_index(drop=True) cut_full_df = full_df.loc[cut_df.index]
cut_df_cut = cut_df[(cut_df.date == cut_vars.compared_dt)].reset_index(drop=True) compare_df = cut_full_df.compare(cut_df)
# check if dataframes are not empty if compare_df.shape[0] > 0:
if full_df_cut.shape[0] != 0 and cut_df_cut.shape[0] != 0: for col_name in compare_df:
# compare dataframes col_idx = compare_df.columns.get_loc(col_name)
compare_df = full_df_cut.compare(cut_df_cut) compare_df_row = compare_df.iloc[0]
# compare_df now comprises tuples with [1] having either 'self' or 'other'
if "other" in col_name[1]:
continue
self_value = compare_df_row.iloc[col_idx]
other_value = compare_df_row.iloc[col_idx + 1]
if compare_df.shape[0] > 0: # output differences
for col_name, values in compare_df.items(): if self_value != other_value:
col_idx = compare_df.columns.get_loc(col_name) if not self.current_analysis.false_indicators.__contains__(col_name[0]):
compare_df_row = compare_df.iloc[0] self.current_analysis.false_indicators.append(col_name[0])
# compare_df now comprises tuples with [1] having either 'self' or 'other' logger.info(
if "other" in col_name[1]: f"=> found look ahead bias in column "
continue f"{col_name[0]}. "
self_value = compare_df_row.iloc[col_idx] f"{str(self_value)} != {str(other_value)}"
other_value = compare_df_row.iloc[col_idx + 1] )
# output differences
if self_value != other_value:
if not self.current_analysis.false_indicators.__contains__(col_name[0]):
self.current_analysis.false_indicators.append(col_name[0])
logger.info(
f"=> found look ahead bias in indicator "
f"{col_name[0]}. "
f"{str(self_value)} != {str(other_value)}"
)
def prepare_data(self, varholder: VarHolder, pairs_to_load: list[DataFrame]): def prepare_data(self, varholder: VarHolder, pairs_to_load: list[DataFrame]):
if "freqai" in self.local_config and "identifier" in self.local_config["freqai"]: if "freqai" in self.local_config and "identifier" in self.local_config["freqai"]:
@@ -132,7 +127,13 @@ class LookaheadAnalysis(BaseAnalysis):
varholder.data, varholder.timerange = backtesting.load_bt_data() varholder.data, varholder.timerange = backtesting.load_bt_data()
varholder.timeframe = backtesting.timeframe varholder.timeframe = backtesting.timeframe
varholder.indicators = backtesting.strategy.advise_all_indicators(varholder.data) temp_indicators = backtesting.strategy.advise_all_indicators(varholder.data)
filled_indicators = dict()
for pair, dataframe in temp_indicators.items():
filled_indicators[pair] = backtesting.strategy.ft_advise_signals(
dataframe, {"pair": pair}
)
varholder.indicators = filled_indicators
varholder.result = self.get_result(backtesting, varholder.indicators) varholder.result = self.get_result(backtesting, varholder.indicators)
def fill_entry_and_exit_varHolders(self, result_row): def fill_entry_and_exit_varHolders(self, result_row):
@@ -171,23 +172,23 @@ class LookaheadAnalysis(BaseAnalysis):
self.fill_entry_and_exit_varHolders(result_row) self.fill_entry_and_exit_varHolders(result_row)
# this will trigger a logger-message # this will trigger a logger-message
buy_or_sell_biased: bool = False entry_or_exit_biased: bool = False
# register if buy signal is broken # register if buy signal is broken
if not self.report_signal( if not self.report_signal(
self.entry_varHolders[idx].result, "open_date", self.entry_varHolders[idx].compared_dt self.entry_varHolders[idx].result, "open_date", self.entry_varHolders[idx].compared_dt
): ):
self.current_analysis.false_entry_signals += 1 self.current_analysis.false_entry_signals += 1
buy_or_sell_biased = True entry_or_exit_biased = True
# register if buy or sell signal is broken # register if buy or sell signal is broken
if not self.report_signal( if not self.report_signal(
self.exit_varHolders[idx].result, "close_date", self.exit_varHolders[idx].compared_dt self.exit_varHolders[idx].result, "close_date", self.exit_varHolders[idx].compared_dt
): ):
self.current_analysis.false_exit_signals += 1 self.current_analysis.false_exit_signals += 1
buy_or_sell_biased = True entry_or_exit_biased = True
if buy_or_sell_biased: if entry_or_exit_biased:
logger.info( logger.info(
f"found lookahead-bias in trade " f"found lookahead-bias in trade "
f"pair: {result_row['pair']}, " f"pair: {result_row['pair']}, "

View File

@@ -13,9 +13,8 @@ from freqtrade.loggers.set_log_levels import (
reduce_verbosity_for_bias_tester, reduce_verbosity_for_bias_tester,
restore_verbosity_for_bias_tester, restore_verbosity_for_bias_tester,
) )
from freqtrade.optimize.analysis.base_analysis import BaseAnalysis, VarHolder
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.optimize.base_analysis import BaseAnalysis, VarHolder
from freqtrade.resolvers import StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,13 +32,6 @@ class RecursiveAnalysis(BaseAnalysis):
super().__init__(config, strategy_obj) super().__init__(config, strategy_obj)
strat = StrategyResolver.load_strategy(config)
self._strat_scc = strat.startup_candle_count
if self._strat_scc not in self._startup_candle:
self._startup_candle.append(self._strat_scc)
self._startup_candle.sort()
self.partial_varHolder_array: list[VarHolder] = [] self.partial_varHolder_array: list[VarHolder] = []
self.partial_varHolder_lookahead_array: list[VarHolder] = [] self.partial_varHolder_lookahead_array: list[VarHolder] = []
@@ -146,8 +138,16 @@ class RecursiveAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange) backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
strat = backtesting.strategy
self._strat_scc = strat.startup_candle_count
if self._strat_scc not in self._startup_candle:
self._startup_candle.append(self._strat_scc)
self._startup_candle.sort()
varholder.data, varholder.timerange = backtesting.load_bt_data() varholder.data, varholder.timerange = backtesting.load_bt_data()
varholder.timeframe = backtesting.timeframe varholder.timeframe = backtesting.timeframe

View File

@@ -17,7 +17,7 @@ class RecursiveAnalysisSubFunctions:
@staticmethod @staticmethod
def text_table_recursive_analysis_instances(recursive_instances: list[RecursiveAnalysis]): def text_table_recursive_analysis_instances(recursive_instances: list[RecursiveAnalysis]):
startups = recursive_instances[0]._startup_candle startups = recursive_instances[0]._startup_candle
strat_scc = recursive_instances[0]._strat_scc strat_scc = getattr(recursive_instances[0], "_strat_scc", 0) or 0
headers = ["Indicators"] headers = ["Indicators"]
for candle in startups: for candle in startups:
if candle == strat_scc: if candle == strat_scc:

View File

@@ -124,7 +124,7 @@ class Backtesting:
self.trade_id_counter: int = 0 self.trade_id_counter: int = 0
self.order_id_counter: int = 0 self.order_id_counter: int = 0
config["dry_run"] = True self.config["dry_run"] = True
self.price_pair_prec: dict[str, Series] = {} self.price_pair_prec: dict[str, Series] = {}
self.run_ids: dict[str, str] = {} self.run_ids: dict[str, str] = {}
self.strategylist: list[IStrategy] = [] self.strategylist: list[IStrategy] = []
@@ -137,6 +137,7 @@ class Backtesting:
self.rejected_dict: dict[str, list] = {} self.rejected_dict: dict[str, list] = {}
self._exchange_name = self.config["exchange"]["name"] self._exchange_name = self.config["exchange"]["name"]
self.__initial_backtest = exchange is None
if not exchange: if not exchange:
exchange = ExchangeResolver.load_exchange(self.config, load_leverage_tiers=True) exchange = ExchangeResolver.load_exchange(self.config, load_leverage_tiers=True)
self.exchange = exchange self.exchange = exchange
@@ -179,20 +180,7 @@ class Backtesting:
if len(self.pairlists.whitelist) == 0: if len(self.pairlists.whitelist) == 0:
raise OperationalException("No pair in whitelist.") raise OperationalException("No pair in whitelist.")
self.set_fee()
if config.get("fee", None) is not None:
self.fee = config["fee"]
logger.info(f"Using fee {self.fee:.4%} from config.")
else:
fees = [
self.exchange.get_fee(
symbol=self.pairlists.whitelist[0],
taker_or_maker=mt, # type: ignore
)
for mt in ("taker", "maker")
]
self.fee = max(fee for fee in fees if fee is not None)
logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).")
self.precision_mode = self.exchange.precisionMode self.precision_mode = self.exchange.precisionMode
self.precision_mode_price = self.exchange.precision_mode_price self.precision_mode_price = self.exchange.precision_mode_price
@@ -217,8 +205,8 @@ class Backtesting:
# This value should NOT be written to startup_candle_count # This value should NOT be written to startup_candle_count
self.required_startup = self.dataprovider.get_required_startup(self.timeframe) self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT) self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT)
self.margin_mode: MarginMode = config.get("margin_mode", MarginMode.ISOLATED) self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.ISOLATED)
# strategies which define "can_short=True" will fail to load in Spot mode. # strategies which define "can_short=True" will fail to load in Spot mode.
self._can_short = self.trading_mode != TradingMode.SPOT self._can_short = self.trading_mode != TradingMode.SPOT
self._position_stacking: bool = self.config.get("position_stacking", False) self._position_stacking: bool = self.config.get("position_stacking", False)
@@ -238,6 +226,30 @@ class Backtesting:
"PrecisionFilter not allowed for backtesting multiple strategies." "PrecisionFilter not allowed for backtesting multiple strategies."
) )
def log_once(self, msg: str) -> None:
"""
Partial reimplementation of log_once from the Login mixin.
only used by recursive, as __initial_backtest is false in all other cases.
"""
if self.__initial_backtest:
logger.info(msg)
def set_fee(self):
if self.config.get("fee", None) is not None:
self.fee = self.config["fee"]
self.log_once(f"Using fee {self.fee:.4%} from config.")
else:
fees = [
self.exchange.get_fee(
symbol=self.pairlists.whitelist[0],
taker_or_maker=mt,
)
for mt in ("taker", "maker")
]
self.fee = max(fee for fee in fees if fee is not None)
self.log_once(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).")
@staticmethod @staticmethod
def cleanup(): def cleanup():
LoggingMixin.show_output = True LoggingMixin.show_output = True
@@ -1649,7 +1661,7 @@ class Backtesting:
pair_detail = self.get_detail_data(pair, row) pair_detail = self.get_detail_data(pair, row)
if pair_detail is not None: if pair_detail is not None:
pair_detail_cache[pair] = pair_detail pair_detail_cache[pair] = pair_detail
row = pair_detail_cache[pair][idx] row = pair_detail_cache[pair][idx]
is_last_row = current_time_det == end_date is_last_row = current_time_det == end_date

View File

@@ -194,6 +194,7 @@ def text_table_strategy(strategy_results, stake_currency: str, title: str):
def text_table_add_metrics(strat_results: dict) -> None: def text_table_add_metrics(strat_results: dict) -> None:
stake = strat_results["stake_currency"]
if len(strat_results["trades"]) > 0: if len(strat_results["trades"]) > 0:
best_trade = max(strat_results["trades"], key=lambda x: x["profit_ratio"]) best_trade = max(strat_results["trades"], key=lambda x: x["profit_ratio"])
worst_trade = min(strat_results["trades"], key=lambda x: x["profit_ratio"]) worst_trade = min(strat_results["trades"], key=lambda x: x["profit_ratio"])
@@ -202,23 +203,19 @@ def text_table_add_metrics(strat_results: dict) -> None:
[ [
("", ""), # Empty line to improve readability ("", ""), # Empty line to improve readability
( (
"Long / Short", "Long / Short trades",
f"{strat_results.get('trade_count_long', 'total_trades')} / " f"{strat_results.get('trade_count_long', 'total_trades')} / "
f"{strat_results.get('trade_count_short', 0)}", f"{strat_results.get('trade_count_short', 0)}",
), ),
("Total profit Long %", f"{strat_results['profit_total_long']:.2%}"),
("Total profit Short %", f"{strat_results['profit_total_short']:.2%}"),
( (
"Absolute profit Long", "Long / Short profit %",
fmt_coin( f"{strat_results['profit_total_long']:.2%} / "
strat_results["profit_total_long_abs"], strat_results["stake_currency"] f"{strat_results['profit_total_short']:.2%}",
),
), ),
( (
"Absolute profit Short", f"Long / Short profit {stake}",
fmt_coin( f"{strat_results['profit_total_long_abs']:.{decimals_per_coin(stake)}f} / "
strat_results["profit_total_short_abs"], strat_results["stake_currency"] f"{strat_results['profit_total_short_abs']:.{decimals_per_coin(stake)}f}",
),
), ),
] ]
if strat_results.get("trade_count_short", 0) > 0 if strat_results.get("trade_count_short", 0) > 0
@@ -231,27 +228,34 @@ def text_table_add_metrics(strat_results: dict) -> None:
drawdown_metrics.append( drawdown_metrics.append(
("Max % of account underwater", f"{strat_results['max_relative_drawdown']:.2%}") ("Max % of account underwater", f"{strat_results['max_relative_drawdown']:.2%}")
) )
drawdown_account = (
strat_results["max_drawdown_account"]
if "max_drawdown_account" in strat_results
else strat_results["max_drawdown"]
)
drawdown_metrics.extend( drawdown_metrics.extend(
[ [
( (
("Absolute Drawdown (Account)", f"{strat_results['max_drawdown_account']:.2%}") "Absolute drawdown",
if "max_drawdown_account" in strat_results f"{fmt_coin(strat_results['max_drawdown_abs'], stake)} "
else ("Drawdown", f"{strat_results['max_drawdown']:.2%}") f"({drawdown_account:.2%})",
), ),
( (
"Absolute Drawdown", "Drawdown duration",
fmt_coin(strat_results["max_drawdown_abs"], strat_results["stake_currency"]), strat_results["drawdown_duration"]
if "drawdown_duration" in strat_results
else "N/A",
), ),
( (
"Drawdown high", "Profit at drawdown start",
fmt_coin(strat_results["max_drawdown_high"], strat_results["stake_currency"]), fmt_coin(strat_results["max_drawdown_high"], stake),
), ),
( (
"Drawdown low", "Profit at drawdown end",
fmt_coin(strat_results["max_drawdown_low"], strat_results["stake_currency"]), fmt_coin(strat_results["max_drawdown_low"], stake),
), ),
("Drawdown Start", strat_results["drawdown_start"]), ("Drawdown start", strat_results["drawdown_start"]),
("Drawdown End", strat_results["drawdown_end"]), ("Drawdown end", strat_results["drawdown_end"]),
] ]
) )
@@ -299,15 +303,15 @@ def text_table_add_metrics(strat_results: dict) -> None:
), ),
( (
"Starting balance", "Starting balance",
fmt_coin(strat_results["starting_balance"], strat_results["stake_currency"]), fmt_coin(strat_results["starting_balance"], stake),
), ),
( (
"Final balance", "Final balance",
fmt_coin(strat_results["final_balance"], strat_results["stake_currency"]), fmt_coin(strat_results["final_balance"], stake),
), ),
( (
"Absolute profit ", "Absolute profit ",
fmt_coin(strat_results["profit_total_abs"], strat_results["stake_currency"]), fmt_coin(strat_results["profit_total_abs"], stake),
), ),
("Total profit %", f"{strat_results['profit_total']:.2%}"), ("Total profit %", f"{strat_results['profit_total']:.2%}"),
("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"), ("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"),
@@ -335,16 +339,16 @@ def text_table_add_metrics(strat_results: dict) -> None:
"Avg. daily profit", "Avg. daily profit",
fmt_coin( fmt_coin(
(strat_results["profit_total_abs"] / strat_results["backtest_days"]), (strat_results["profit_total_abs"] / strat_results["backtest_days"]),
strat_results["stake_currency"], stake,
), ),
), ),
( (
"Avg. stake amount", "Avg. stake amount",
fmt_coin(strat_results["avg_stake_amount"], strat_results["stake_currency"]), fmt_coin(strat_results["avg_stake_amount"], stake),
), ),
( (
"Total trade volume", "Total trade volume",
fmt_coin(strat_results["total_volume"], strat_results["stake_currency"]), fmt_coin(strat_results["total_volume"], stake),
), ),
*short_metrics, *short_metrics,
("", ""), # Empty line to improve readability ("", ""), # Empty line to improve readability
@@ -362,11 +366,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
("Worst trade", f"{worst_trade['pair']} {worst_trade['profit_ratio']:.2%}"), ("Worst trade", f"{worst_trade['pair']} {worst_trade['profit_ratio']:.2%}"),
( (
"Best day", "Best day",
fmt_coin(strat_results["backtest_best_day_abs"], strat_results["stake_currency"]), fmt_coin(strat_results["backtest_best_day_abs"], stake),
), ),
( (
"Worst day", "Worst day",
fmt_coin(strat_results["backtest_worst_day_abs"], strat_results["stake_currency"]), fmt_coin(strat_results["backtest_worst_day_abs"], stake),
), ),
( (
"Days win/draw/lose", "Days win/draw/lose",
@@ -404,17 +408,17 @@ def text_table_add_metrics(strat_results: dict) -> None:
), ),
*entry_adjustment_metrics, *entry_adjustment_metrics,
("", ""), # Empty line to improve readability ("", ""), # Empty line to improve readability
("Min balance", fmt_coin(strat_results["csum_min"], strat_results["stake_currency"])), ("Min balance", fmt_coin(strat_results["csum_min"], stake)),
("Max balance", fmt_coin(strat_results["csum_max"], strat_results["stake_currency"])), ("Max balance", fmt_coin(strat_results["csum_max"], stake)),
*drawdown_metrics, *drawdown_metrics,
("Market change", f"{strat_results['market_change']:.2%}"), ("Market change", f"{strat_results['market_change']:.2%}"),
] ]
print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left") print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left")
else: else:
start_balance = fmt_coin(strat_results["starting_balance"], strat_results["stake_currency"]) start_balance = fmt_coin(strat_results["starting_balance"], stake)
stake_amount = ( stake_amount = (
fmt_coin(strat_results["stake_amount"], strat_results["stake_currency"]) fmt_coin(strat_results["stake_amount"], stake)
if strat_results["stake_amount"] != UNLIMITED_STAKE_AMOUNT if strat_results["stake_amount"] != UNLIMITED_STAKE_AMOUNT
else "unlimited" else "unlimited"
) )

View File

@@ -64,7 +64,7 @@ def store_backtest_results(
:param market_change_data: Dataframe containing market change data :param market_change_data: Dataframe containing market change data
:param analysis_results: Dictionary containing analysis results :param analysis_results: Dictionary containing analysis results
""" """
recordfilename: Path = config["exportfilename"] recordfilename: Path = config["exportdirectory"]
zip_filename = _generate_filename(recordfilename, dtappendix, ".zip") zip_filename = _generate_filename(recordfilename, dtappendix, ".zip")
base_filename = _generate_filename(recordfilename, dtappendix, "") base_filename = _generate_filename(recordfilename, dtappendix, "")
json_filename = _generate_filename(recordfilename, dtappendix, ".json") json_filename = _generate_filename(recordfilename, dtappendix, ".json")

View File

@@ -598,6 +598,8 @@ def generate_strategy_stats(
"timerange": config.get("timerange", ""), "timerange": config.get("timerange", ""),
"enable_protections": config.get("enable_protections", False), "enable_protections": config.get("enable_protections", False),
"strategy_name": strategy, "strategy_name": strategy,
"freqaimodel": config.get("freqaimodel", None),
"freqai_identifier": config.get("freqai", {}).get("identifier", None),
# Parameters relevant for backtesting # Parameters relevant for backtesting
"stoploss": config["stoploss"], "stoploss": config["stoploss"],
"trailing_stop": config.get("trailing_stop", False), "trailing_stop": config.get("trailing_stop", False),
@@ -625,6 +627,7 @@ def generate_strategy_stats(
underwater = calculate_max_drawdown( underwater = calculate_max_drawdown(
results, value_col="profit_abs", starting_balance=start_balance, relative=True results, value_col="profit_abs", starting_balance=start_balance, relative=True
) )
drawdown_duration = drawdown.low_date - drawdown.high_date
strat_stats.update( strat_stats.update(
{ {
@@ -635,6 +638,8 @@ def generate_strategy_stats(
"drawdown_start_ts": drawdown.high_date.timestamp() * 1000, "drawdown_start_ts": drawdown.high_date.timestamp() * 1000,
"drawdown_end": drawdown.low_date.strftime(DATETIME_PRINT_FORMAT), "drawdown_end": drawdown.low_date.strftime(DATETIME_PRINT_FORMAT),
"drawdown_end_ts": drawdown.low_date.timestamp() * 1000, "drawdown_end_ts": drawdown.low_date.timestamp() * 1000,
"drawdown_duration": drawdown_duration,
"drawdown_duration_s": drawdown_duration.total_seconds(),
"max_drawdown_low": drawdown.low_value, "max_drawdown_low": drawdown.low_value,
"max_drawdown_high": drawdown.high_value, "max_drawdown_high": drawdown.high_value,
} }

View File

@@ -1187,10 +1187,13 @@ class LocalTrade:
""" """
close_trade_value = self.calc_close_trade_value(rate, amount) close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None: if (amount is None) and (open_rate is None):
open_trade_value = self.open_trade_value open_trade_value = self.open_trade_value
else: else:
open_trade_value = self._calc_open_trade_value(amount, open_rate) # Fall back to trade.amount and self.open_rate if necessary
open_trade_value = self._calc_open_trade_value(
amount or self.amount, open_rate or self.open_rate
)
if open_trade_value == 0.0: if open_trade_value == 0.0:
return 0.0 return 0.0

View File

@@ -31,8 +31,9 @@ class AgeFilter(IPairList):
self._min_days_listed = self._pairlistconfig.get("min_days_listed", 10) self._min_days_listed = self._pairlistconfig.get("min_days_listed", 10)
self._max_days_listed = self._pairlistconfig.get("max_days_listed") self._max_days_listed = self._pairlistconfig.get("max_days_listed")
self._def_candletype = self._config["candle_type_def"]
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._def_candletype)
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > candle_limit: if self._min_days_listed > candle_limit:
@@ -100,7 +101,7 @@ class AgeFilter(IPairList):
:return: new allowlist :return: new allowlist
""" """
needed_pairs: ListPairsWithTimeframes = [ needed_pairs: ListPairsWithTimeframes = [
(p, "1d", self._config["candle_type_def"]) (p, "1d", self._def_candletype)
for p in pairlist for p in pairlist
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed if p not in self._symbolsChecked and p not in self._symbolsCheckFailed
] ]
@@ -116,8 +117,8 @@ class AgeFilter(IPairList):
if self._enabled: if self._enabled:
for p in deepcopy(pairlist): for p in deepcopy(pairlist):
daily_candles = ( daily_candles = (
candles[(p, "1d", self._config["candle_type_def"])] candles[(p, "1d", self._def_candletype)]
if (p, "1d", self._config["candle_type_def"]) in candles if (p, "1d", self._def_candletype) in candles
else None else None
) )
if not self._validate_pair_loc(p, daily_candles): if not self._validate_pair_loc(p, daily_candles):

View File

@@ -37,7 +37,6 @@ class MarketCapPairList(IPairList):
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._categories = self._pairlistconfig.get("categories", []) self._categories = self._pairlistconfig.get("categories", [])
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._def_candletype = self._config["candle_type_def"]
_coingecko_config = self._config.get("coingecko", {}) _coingecko_config = self._config.get("coingecko", {})
@@ -118,6 +117,16 @@ class MarketCapPairList(IPairList):
}, },
} }
def get_markets_exchange(self):
markets = [
k
for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
return markets
def gen_pairlist(self, tickers: Tickers) -> list[str]: def gen_pairlist(self, tickers: Tickers) -> list[str]:
""" """
Generate the pairlist Generate the pairlist
@@ -133,12 +142,8 @@ class MarketCapPairList(IPairList):
else: else:
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
_pairlist = [ _pairlist = self.get_markets_exchange()
k
for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
# No point in testing for blacklisted pairs... # No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info) _pairlist = self.verify_blacklist(_pairlist, logger.info)
@@ -147,6 +152,31 @@ class MarketCapPairList(IPairList):
return pairlist return pairlist
# Prefixes to test to discover coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
prefixes = ("1000", "K")
def resolve_marketcap_pair(
self,
pair: str,
pairlist: list[str],
markets: list[str],
filtered_pairlist: list[str],
) -> str | None:
if pair in filtered_pairlist:
return None
if pair in pairlist:
return pair
if pair not in markets:
for prefix in self.prefixes:
test_prefix = f"{prefix}{pair}"
if test_prefix in pairlist:
return test_prefix
return None
def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]: def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]:
""" """
Filters and sorts pairlist and returns the whitelist again. Filters and sorts pairlist and returns the whitelist again.
@@ -189,21 +219,25 @@ class MarketCapPairList(IPairList):
self._marketcap_cache["marketcap"] = marketcap_list self._marketcap_cache["marketcap"] = marketcap_list
if marketcap_list: if marketcap_list:
filtered_pairlist = [] filtered_pairlist: list[str] = []
market = self._config["trading_mode"] market = self._exchange._config["trading_mode"]
pair_format = f"{self._stake_currency.upper()}" pair_format = f"{self._stake_currency.upper()}"
if market == "futures": if market == "futures":
pair_format += f":{self._stake_currency.upper()}" pair_format += f":{self._stake_currency.upper()}"
top_marketcap = marketcap_list[: self._max_rank :] top_marketcap = marketcap_list[: self._max_rank :]
markets = self.get_markets_exchange()
for mc_pair in top_marketcap: for mc_pair in top_marketcap:
test_pair = f"{mc_pair.upper()}/{pair_format}" pair = f"{mc_pair.upper()}/{pair_format}"
if test_pair in pairlist and test_pair not in filtered_pairlist: resolved = self.resolve_marketcap_pair(pair, pairlist, markets, filtered_pairlist)
filtered_pairlist.append(test_pair)
if len(filtered_pairlist) == self._number_assets: if resolved:
break filtered_pairlist.append(resolved)
if len(filtered_pairlist) == self._number_assets:
break
if len(filtered_pairlist) > 0: if len(filtered_pairlist) > 0:
return filtered_pairlist return filtered_pairlist

View File

@@ -91,7 +91,7 @@ class PercentChangePairList(IPairList):
) )
candle_limit = self._exchange.ohlcv_candle_limit( candle_limit = self._exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config["candle_type_def"] self._lookback_timeframe, self._def_candletype
) )
if self._lookback_period > candle_limit: if self._lookback_period > candle_limit:

View File

@@ -40,7 +40,7 @@ class VolatilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._def_candletype)
if self._days < 1: if self._days < 1:
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:

View File

@@ -89,7 +89,7 @@ class VolumePairList(IPairList):
raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}") raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
candle_limit = self._exchange.ohlcv_candle_limit( candle_limit = self._exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config["candle_type_def"] self._lookback_timeframe, self._def_candletype
) )
if self._lookback_period < 0: if self._lookback_period < 0:
raise OperationalException("VolumeFilter requires lookback_period to be >= 0") raise OperationalException("VolumeFilter requires lookback_period to be >= 0")

View File

@@ -34,7 +34,7 @@ class RangeStabilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._def_candletype)
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:

View File

@@ -54,6 +54,7 @@ def __run_pairlist(job_id: str, config_loc: Config):
with FtNoDBContext(): with FtNoDBContext():
exchange = get_exchange(config_loc) exchange = get_exchange(config_loc)
config_loc["candle_type_def"] = exchange._config["candle_type_def"]
pairlists = PairListManager(exchange, config_loc) pairlists = PairListManager(exchange, config_loc)
pairlists.refresh_pairlist() pairlists.refresh_pairlist()
ApiBG.jobs[job_id]["result"] = { ApiBG.jobs[job_id]["result"] = {

View File

@@ -342,8 +342,22 @@ class Telegram(RPCHandler):
self._loop.run_until_complete(self._startup_telegram()) self._loop.run_until_complete(self._startup_telegram())
async def _startup_telegram(self) -> None: async def _startup_telegram(self) -> None:
await self._app.initialize() retries = 3
await self._app.start() attempt = 0
while attempt < retries:
try:
await self._app.initialize()
await self._app.start()
break
except Exception as ex:
logger.error(
"Error starting Telegram bot (attempt %d/%d): %s", attempt + 1, retries, ex
)
attempt += 1
if attempt == retries:
logger.warning("Telegram init failed.")
return
await asyncio.sleep(2)
if self._app.updater: if self._app.updater:
await self._app.updater.start_polling( await self._app.updater.start_polling(
bootstrap_retries=-1, bootstrap_retries=-1,

View File

@@ -225,7 +225,6 @@ class RealParameter(NumericParameter):
class DecimalParameter(NumericParameter): class DecimalParameter(NumericParameter):
default: float default: float
value: float
def __init__( def __init__(
self, self,
@@ -259,6 +258,14 @@ class DecimalParameter(NumericParameter):
low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs
) )
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, new_value: float):
self._value = round(new_value, self._decimals)
def get_space(self, name: str) -> "SKDecimal": def get_space(self, name: str) -> "SKDecimal":
""" """
Create optimization space. Create optimization space.

View File

@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.7" __version__ = "2025.8"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path

View File

@@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.32.4 requests==2.32.5
python-rapidjson==1.21 python-rapidjson==1.21

View File

@@ -40,7 +40,7 @@ dependencies = [
"jsonschema", "jsonschema",
"numpy>2.0,<3.0", "numpy>2.0,<3.0",
"pandas>=2.2.0,<3.0", "pandas>=2.2.0,<3.0",
"TA-Lib<0.6", "TA-Lib<0.7",
"ft-pandas-ta", "ft-pandas-ta",
"technical", "technical",
"tabulate", "tabulate",

View File

@@ -6,9 +6,9 @@
-r requirements-freqai-rl.txt -r requirements-freqai-rl.txt
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
ruff==0.12.5 ruff==0.12.10
mypy==1.17.0 mypy==1.17.1
pre-commit==4.2.0 pre-commit==4.3.0
pytest==8.4.1 pytest==8.4.1
pytest-asyncio==1.1.0 pytest-asyncio==1.1.0
pytest-cov==6.2.1 pytest-cov==6.2.1
@@ -18,15 +18,15 @@ pytest-timeout==2.4.0
pytest-xdist==3.8.0 pytest-xdist==3.8.0
isort==6.0.1 isort==6.0.1
# For datetime mocking # For datetime mocking
time-machine==2.16.0 time-machine==2.19.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.16.6 nbconvert==7.16.6
# mypy types # mypy types
scipy-stubs==1.16.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt` scipy-stubs==1.16.1.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.1.0.20250717 types-cachetools==6.1.0.20250717
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.32.4.20250611 types-requests==2.32.4.20250809
types-tabulate==0.9.0.20241207 types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20250708 types-python-dateutil==2.9.0.20250822

View File

@@ -2,7 +2,7 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64' torch==2.8.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos # SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64' stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'

View File

@@ -4,9 +4,9 @@
# Required for freqai # Required for freqai
scikit-learn==1.7.1 scikit-learn==1.7.1
joblib==1.5.1 joblib==1.5.2
catboost==1.2.8; 'arm' not in platform_machine catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0 lightgbm==4.6.0
xgboost==3.0.2 xgboost==3.0.4
tensorboard==2.20.0 tensorboard==2.20.0
datasieve==0.1.9 datasieve==0.1.9

View File

@@ -4,6 +4,6 @@
# Required for hyperopt # Required for hyperopt
scipy==1.16.1 scipy==1.16.1
scikit-learn==1.7.1 scikit-learn==1.7.1
filelock==3.18.0 filelock==3.19.1
optuna==4.4.0 optuna==4.5.0
cmaes==0.12.0 cmaes==0.12.0

View File

@@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==6.2.0 plotly==6.3.0

View File

@@ -1,36 +1,40 @@
numpy==2.3.2 numpy==2.3.2; platform_machine != 'armv7l'
pandas==2.3.1 numpy==2.2.4; platform_machine == 'armv7l'
pandas==2.3.2; platform_machine != 'armv7l'
pandas==2.2.3; platform_machine == 'armv7l'
bottleneck==1.5.0 bottleneck==1.5.0
numexpr==2.11.0 numexpr==2.11.0
# Indicator libraries # Indicator libraries
ft-pandas-ta==0.3.15 ft-pandas-ta==0.3.15
ta-lib==0.5.5 ta-lib==0.6.5
technical==1.5.2 technical==1.5.3
ccxt==4.4.96 ccxt==4.5.2
cryptography==45.0.5 cryptography==45.0.6
aiohttp==3.12.14 aiohttp==3.12.15
SQLAlchemy==2.0.41 SQLAlchemy==2.0.43
python-telegram-bot==22.3 python-telegram-bot==22.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
humanize==4.12.3 humanize==4.12.3
cachetools==6.1.0 cachetools==6.1.0
requests==2.32.4 requests==2.32.5
urllib3==2.5.0 urllib3==2.5.0
certifi==2025.7.14 certifi==2025.8.3
jsonschema==4.25.0 jsonschema==4.25.1
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.2.0 pycoingecko==3.2.0
jinja2==3.1.6 jinja2==3.1.6
joblib==1.5.1 joblib==1.5.2
rich==14.1.0 rich==14.1.0
pyarrow==21.0.0; platform_machine != 'armv7l' pyarrow==21.0.0; platform_machine != 'armv7l' and platform_machine != "aarch64"
# TODO: downgrade for aarch64 until https://github.com/apache/arrow/issues/47229 is resolved
pyarrow==20.0.0; platform_machine == "aarch64"
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.21 python-rapidjson==1.21
# Properly format api responses # Properly format api responses
orjson==3.11.1 orjson==3.11.2
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2

View File

@@ -228,16 +228,6 @@ function Main {
} }
} }
if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) {
# Install TA-Lib using the virtual environment's pip
Write-Log "Installing TA-Lib using virtual environment's pip..."
python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install TA-Lib." -Level 'ERROR'
Exit-Script -exitCode 1
}
}
# Present options for requirement files # Present options for requirement files
$SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A' $SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A'

View File

@@ -91,7 +91,6 @@ function updateenv() {
fi fi
fi fi
fi fi
install_talib
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL} ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL}
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -118,25 +117,6 @@ function updateenv() {
fi fi
} }
# Install tab lib
function install_talib() {
if [ -f /usr/local/lib/libta_lib.a ] || [ -f /usr/local/lib/libta_lib.so ] || [ -f /usr/lib/libta_lib.so ]; then
echo "ta-lib already installed, skipping"
return
fi
cd build_helpers && ./install_ta-lib.sh
if [ $? -ne 0 ]; then
echo "Quitting. Please fix the above error before continuing."
cd ..
exit 1
fi;
cd ..
}
# Install bot MacOS # Install bot MacOS
function install_macos() { function install_macos() {
if [ ! -x "$(command -v brew)" ] if [ ! -x "$(command -v brew)" ]
@@ -257,7 +237,7 @@ function install() {
install_redhat install_redhat
else else
echo "This script does not support your OS." echo "This script does not support your OS."
echo "If you have Python version 3.11 - 3.13, pip, virtualenv, ta-lib you can continue." echo "If you have Python version 3.11 - 3.13, pip, virtualenv installed you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10 sleep 10
fi fi

View File

@@ -133,6 +133,8 @@ def test_list_exchanges(capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bybit$", captured.out, re.MULTILINE) assert re.search(r"^bybit$", captured.out, re.MULTILINE)
# An exchange not supporting futures
assert re.search(r"^kraken$", captured.out, re.MULTILINE)
# Test with --all # Test with --all
args = [ args = [
@@ -160,6 +162,32 @@ def test_list_exchanges(capsys):
assert re.search(r"^bingx$", captured.out, re.MULTILINE) assert re.search(r"^bingx$", captured.out, re.MULTILINE)
assert re.search(r"^bitmex$", captured.out, re.MULTILINE) assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
# Only dex
args = [
"list-exchanges",
"--dex",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
assert not re.search(r".*binance.*", captured.out)
assert not re.search(r".*bingx.*", captured.out)
assert re.search(r".*hyperliquid.*", captured.out)
# Only futures
args = [
"list-exchanges",
"--trading-mode",
"futures",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
assert re.search(r".*binance.*", captured.out)
assert not re.search(r".*kraken.*", captured.out)
def test_list_timeframes(mocker, capsys): def test_list_timeframes(mocker, capsys):
api_mock = MagicMock() api_mock = MagicMock()
@@ -1748,6 +1776,27 @@ def test_start_list_data(testdatadir, capsys):
captured.out, captured.out,
) )
# Test with regex
args = [
"list-data",
"--pairs",
"XMR/.*",
"--datadir",
str(testdatadir),
"--show-timerange",
]
pargs = get_args(args)
pargs["config"] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 1 pair / timeframe combinations." in captured.out
assert re.search(r".*Pair.*Timeframe.*Type.*From .* To .* Candles .*\n", captured.out)
assert "UNITTEST/BTC" not in captured.out
assert re.search(
r"\n.* XMR/USDT .* 5m .* spot .* 2019-10-11 00:00:00 .* 2019-10-13 11:19:00 .* 2469 |\n",
captured.out,
)
def test_start_list_trades_data(testdatadir, capsys): def test_start_list_trades_data(testdatadir, capsys):
args = [ args = [
@@ -1834,8 +1883,10 @@ def test_backtesting_show(mocker, testdatadir, capsys):
sbr = mocker.patch("freqtrade.optimize.optimize_reports.show_backtest_results") sbr = mocker.patch("freqtrade.optimize.optimize_reports.show_backtest_results")
args = [ args = [
"backtesting-show", "backtesting-show",
"--export-directory",
f"{testdatadir / 'backtest_results'}",
"--export-filename", "--export-filename",
f"{testdatadir / 'backtest_results/backtest-result.json'}", "backtest-result.json",
"--show-pair-list", "--show-pair-list",
] ]
pargs = get_args(args) pargs = get_args(args)

View File

@@ -521,7 +521,11 @@ def patch_torch_initlogs(mocker) -> None:
mocked_module = types.ModuleType(module_name) mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module sys.modules[module_name] = mocked_module
else: else:
mocker.patch("torch._logging._init_logs") try:
mocker.patch("torch._logging._init_logs")
except ModuleNotFoundError:
# Allow running limited tests to run without freqAI dependencies
pass
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

Some files were not shown because too many files have changed in this diff Show More