Merge pull request #12875 from freqtrade/new_release

New release 2026.2
This commit is contained in:
Matthias
2026-02-28 12:25:06 +01:00
committed by GitHub
115 changed files with 38506 additions and 35695 deletions

View File

@@ -2,7 +2,7 @@ name: Binance Leverage tiers update
on:
schedule:
- cron: "0 3 * * 4"
- cron: "25 3 * * 4"
# on demand
workflow_dispatch:
@@ -24,12 +24,19 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
activate-environment: true
enable-cache: false
python-version: "3.14"
- name: Install ccxt
run: pip install ccxt
run: uv pip install ccxt orjson
- name: Run leverage tier update
env:
@@ -39,7 +46,7 @@ jobs:
run: python build_helpers/binance_update_lev_tiers.py
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: freqtrade/exchange/binance_leverage_tiers.json

View File

@@ -33,12 +33,12 @@ jobs:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
activate-environment: true
enable-cache: true
@@ -161,7 +161,7 @@ jobs:
$PSVersionTable
Get-PSRepository | Format-List *
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
$Error.clear()
Invoke-Pester -Path "tests" -CI
if ($Error.Length -gt 0) {exit 1}
@@ -183,7 +183,7 @@ jobs:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 #v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
with:
python-version: "3.12"
@@ -200,7 +200,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
@@ -218,7 +218,7 @@ jobs:
./tests/test_docs.sh
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -246,12 +246,12 @@ jobs:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
activate-environment: true
enable-cache: true
@@ -325,7 +325,7 @@ jobs:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

View File

@@ -27,7 +27,7 @@ jobs:
persist-credentials: true
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'

View File

@@ -31,7 +31,7 @@ jobs:
with:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -57,7 +57,7 @@ jobs:
uses: ./.github/actions/docker-tags
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -179,13 +179,13 @@ jobs:
uses: ./.github/actions/docker-tags
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to github
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -22,7 +22,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -33,7 +33,7 @@ jobs:
- name: Run auto-update
run: pre-commit autoupdate
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml

View File

@@ -31,4 +31,4 @@ jobs:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0

View File

@@ -30,13 +30,13 @@ repos:
- types-filelock==3.2.7
- types-requests==2.32.4.20260107
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20251115
- scipy-stubs==1.17.0.1
- SQLAlchemy==2.0.45
- types-python-dateutil==2.9.0.20260124
- scipy-stubs==1.17.0.2
- SQLAlchemy==2.0.46
# stages: [push]
- repo: https://github.com/pycqa/isort
rev: "7.0.0"
rev: "8.0.0"
hooks:
- id: isort
name: isort (python)
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.14'
rev: 'v0.15.2'
hooks:
- id: ruff
- id: ruff-format

View File

@@ -12,7 +12,8 @@ Few pointers for contributions:
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code. The final responsibility for the code with the PR author, not with the AI.
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code yourself.
The final responsibility for the code with the PR author, not with the AI, which also means that commits must be linked to your (human) account, not some generic AI account.
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
@@ -24,8 +25,7 @@ Best start by reading the [documentation](https://www.freqtrade.io/) to get a fe
### 1. Run unit tests
All unit tests must pass. If a unit test is broken, change your code to
make it pass. It means you have introduced a regression.
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression.
#### Test the whole project

View File

@@ -1,4 +1,4 @@
FROM python:3.13.11-slim-trixie AS base
FROM python:3.13.12-slim-trixie AS base
# Setup env
ENV LANG=C.UTF-8

View File

@@ -2,8 +2,9 @@
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
[![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)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
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.
@@ -24,7 +25,7 @@ hesitate to read the source code and understand the mechanism of this bot.
## Supported Exchange marketplaces
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exchanges/) to learn about special configurations that maybe needed for each exchange.
### Supported Spot Exchanges
@@ -50,7 +51,7 @@ Please read the [exchange-specific notes](docs/exchanges.md) to learn about spec
- [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
### Community tested
@@ -142,7 +143,7 @@ options:
### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/stable/telegram-usage/)
- `/start`: Starts the trader.
- `/stop`: Stops the trader.

View File

@@ -1057,7 +1057,8 @@
},
"jwt_secret_key": {
"description": "Secret key for JWT authentication.",
"type": "string"
"type": "string",
"default": "somethingRandomSomethingRandom123"
},
"CORS_origins": {
"description": "List of allowed CORS origins.",
@@ -1080,7 +1081,8 @@
"listen_ip_address",
"listen_port",
"username",
"password"
"password",
"jwt_secret_key"
]
},
"db_url": {

View File

@@ -70,7 +70,7 @@
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"verbosity": "error",
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": [],
"username": "freqtrader",
"password": "SuperSecurePassword"

View File

@@ -177,7 +177,7 @@
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": [],
"username": "freqtrader",
"password": "SuperSecurePassword",
@@ -215,4 +215,4 @@
"reduce_df_footprint": false,
"dataformat_ohlcv": "feather",
"dataformat_trades": "feather"
}
}

View File

@@ -75,7 +75,7 @@
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"verbosity": "error",
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": [],
"username": "freqtrader",
"password": "SuperSecurePassword"

View File

@@ -73,7 +73,7 @@ services:
volumes:
- "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
# before enabling this.
ports:
- "127.0.0.1:8080:8080"
@@ -100,7 +100,7 @@ services:
volumes:
- "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
# before enabling this.
ports:
- "127.0.0.1:8081:8080"

View File

@@ -64,18 +64,15 @@ options:
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that timeframe needs to be set
either in config or via command line. When using this
together with `--export trades`, the strategy-name is
injected into the filename (so `backtest-data.json`
becomes `backtest-data-SampleStrategy.json`
either in config or via command line.
--export {none,trades,signals}
Export backtest results (default: trades).
--backtest-filename, --export-filename PATH
Use this filename for backtest results.Example:
`--backtest-
filename=backtest_results_2020-09-27_16-20-48.json`.
Assumes either `user_data/backtest_results/` or
`--export-directory` as base directory.
DEPRECATED: This option is deprecated for backtesting
and will be removed in a future release. Using a
custom filename for backtest results is no longer
supported. Use `--backtest-directory` to specify the
directory.
--backtest-directory, --export-directory PATH
Directory to use for backtest results. Example:
`--export-directory=user_data/backtest_results/`.

View File

@@ -62,10 +62,7 @@ options:
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that timeframe needs to be set
either in config or via command line. When using this
together with `--export trades`, the strategy-name is
injected into the filename (so `backtest-data.json`
becomes `backtest-data-SampleStrategy.json`
either in config or via command line.
--export {none,trades,signals}
Export backtest results (default: trades).
--backtest-filename, --export-filename PATH

View File

@@ -10,10 +10,7 @@ options:
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that timeframe needs to be set
either in config or via command line. When using this
together with `--export trades`, the strategy-name is
injected into the filename (so `backtest-data.json`
becomes `backtest-data-SampleStrategy.json`
either in config or via command line.
--strategy-path PATH Specify additional strategy lookup path.
--recursive-strategy-search
Recursively search for a strategy in the strategies

View File

@@ -239,7 +239,7 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
### Kucoin Blacklists
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
## HTX
@@ -319,7 +319,6 @@ API Keys for live futures trading must have the following permissions:
We do strongly recommend to limit all API keys to the IP you're going to use it from.
## Bitmart
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
@@ -458,6 +457,8 @@ Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEX
!!! Note
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
The pair name for HIP-3 pairs will be slightly different than non HIP-3 pairs. Please use `list-pairs` subcommand to get the correct pair naming for all pairs for the specified dexes.
## Bitvavo
If your account is required to use an operatorId, you can set it in the configuration file as follows:
@@ -521,5 +522,5 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t
!!! Warning
Please make sure to fully understand the impacts of these settings before modifying them.
Using `_ft_has_params` overrides may lead to unexpected behavior, and may even break your bot.
Using `_ft_has_params` overrides may lead to unexpected behavior, and may even break your bot.
We will not be able to provide support for issues caused by custom settings in `_ft_has_params`.

View File

@@ -45,7 +45,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]

View File

@@ -7,7 +7,7 @@
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
!!! Note
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/stable/freqai/).
Features include:

View File

@@ -15,7 +15,7 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": ["https://frequi.freqtrade.io"],
//...
}
@@ -29,7 +29,7 @@ The correct configuration for this case is `http://localhost:8080` - the main pa
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": ["http://localhost:8080"],
//...
}

View File

@@ -20,15 +20,15 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
### Common settings to all Protections
| Parameter| Description |
|------------|-------------|
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections)
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles)
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes)
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
| Parameter | Description |
| --------- | ---------- |
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections) |
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles) |
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes) |
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles). |
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes) |
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer |
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours) |
!!! Note "Durations"
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
@@ -69,7 +69,17 @@ def protections(self):
#### MaxDrawdown
`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
The `MaxDrawdown` protection evaluates trades that closed within the current `lookback_period` (or `lookback_period_candles`).
It supports 2 calculation modes:
- `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios.
- `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit.
With `calculation_mode: "ratios"`, drawdown is derived from cumulative trade profit ratios, not from the account equity curve. This is kept for backward compatibility and can differ from account-level drawdown when position sizing changes over time.
For new setups, `calculation_mode: "equity"` is recommended. Prefer `calculation_mode: "ratios"` only when you intentionally rely on legacy behavior, especially with fixed stake amount configurations where ratio-based behavior is easier to reason about.
If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover.
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
@@ -79,6 +89,7 @@ def protections(self):
return [
{
"method": "MaxDrawdown",
"calculation_mode": "equity",
"lookback_period_candles": 48,
"trade_limit": 20,
"stop_duration_candles": 12,
@@ -160,6 +171,7 @@ class AwesomeStrategy(IStrategy)
},
{
"method": "MaxDrawdown",
"calculation_mode": "equity",
"lookback_period_candles": 48,
"trade_limit": 20,
"stop_duration_candles": 4,

View File

@@ -2,7 +2,9 @@
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
[![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)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
<!-- GitHub action buttons -->
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }

View File

@@ -1,7 +1,7 @@
markdown==3.10
markdown==3.10.2
mkdocs==1.6.1
mkdocs-material==9.7.1
mdx_truly_sane_lists==1.3
pymdown-extensions==10.20
pymdown-extensions==10.21
jinja2==3.1.6
mike==2.1.3

View File

@@ -17,7 +17,7 @@ Sample configuration:
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": [],
"username": "Freqtrader",
"password": "SuperSecret1!",
@@ -56,7 +56,7 @@ secrets.token_hex()
!!! Danger "Password selection"
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!).
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). This value should also be 32 characters or longer to be safe.
### Configuration with docker
@@ -245,7 +245,7 @@ You would then add that token under `ws_token` in your `api_server` config. Like
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom",
"jwt_secret_key": "somethingRandomSomethingRandom123",
"CORS_origins": [],
"username": "Freqtrader",
"password": "SuperSecret1!",

View File

@@ -225,7 +225,7 @@ class AwesomeStrategy(IStrategy):
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
@@ -805,7 +805,7 @@ class AwesomeStrategy(IStrategy):
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -853,7 +853,7 @@ class AwesomeStrategy(IStrategy):
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -991,7 +991,7 @@ class DigDeeperStrategy(IStrategy):
This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None
@@ -1118,7 +1118,7 @@ class AwesomeStrategy(IStrategy):
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
@@ -1303,7 +1303,8 @@ Currently two types of annotations are supported, `area` and `line`.
"z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
"label": "some label",
"size": 2, // Optional, line width in pixels. Defaults to 10
"symbol": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
"shape": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
"rotate": 0, // Optional, rotation of the shape/symbol in degrees. Defaults to 0
}
```
@@ -1385,7 +1386,7 @@ Entries will be validated, and won't be passed to the UI if they don't correspon
}
)
elif (start_dt.hour % 2) == 0:
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
price = dataframe.loc[dataframe["date"] == start_dt, "close"].mean()
annotations.append(
{
"type": "area",

View File

@@ -594,9 +594,9 @@ Features will now expand automatically. As such, the expansion loops, as well as
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
@@ -657,9 +657,9 @@ Basic features. Make sure to remove the `{pair}` part from your features.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
@@ -690,7 +690,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
@@ -713,7 +713,7 @@ Targets now get their own, dedicated method.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]

View File

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

View File

@@ -215,9 +215,7 @@ AVAILABLE_CLI_OPTIONS = {
"--strategy-list",
help="Provide a space-separated list of strategies to backtest. "
"Please note that timeframe needs to be set either in config "
"or via command line. When using this together with `--export trades`, "
"the strategy-name is injected into the filename "
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
"or via command line. ",
nargs="+",
),
"backtest_notes": Arg(
@@ -240,6 +238,14 @@ AVAILABLE_CLI_OPTIONS = {
"exportfilename": Arg(
"--backtest-filename",
"--export-filename",
fthelp={
"freqtrade backtesting": (
"DEPRECATED: This option is deprecated for backtesting and will be removed "
"in a future release. "
"Using a custom filename for backtest results is no longer supported. "
"Use `--backtest-directory` to specify the directory."
),
},
help="Use this filename for backtest results."
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",

View File

@@ -223,7 +223,7 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
end.strftime(DATETIME_PRINT_FORMAT),
str(length),
)
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
for pair, start, end, length in sorted(paircombs1, key=lambda x: x[0])
],
("Pair", "Type", "From", "To", "Trades"),
summary=title,

View File

@@ -13,6 +13,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.persistence import Order, Trade, init_db
from freqtrade.persistence.custom_data import _CustomData
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
from freqtrade.persistence.migrations import set_sequence_ids
from freqtrade.persistence.pairlock import PairLock
@@ -25,6 +27,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
trade_count = 0
pairlock_count = 0
kv_count = 0
custom_data_count = 0
for trade in Trade.get_trades():
trade_count += 1
make_transient(trade)
@@ -41,16 +45,35 @@ def start_convert_db(args: dict[str, Any]) -> None:
session_target.add(pairlock)
session_target.commit()
for kv in _KeyValueStoreModel.session.scalars(select(_KeyValueStoreModel)):
kv_count += 1
make_transient(kv)
session_target.add(kv)
session_target.commit()
for cd in _CustomData.session.scalars(select(_CustomData)):
custom_data_count += 1
make_transient(cd)
session_target.add(cd)
session_target.commit()
# Update sequences
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
max_order_id = session_target.scalar(select(func.max(Order.id)))
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
max_kv_id = session_target.scalar(select(func.max(_KeyValueStoreModel.id)))
max_custom_data_id = session_target.scalar(select(func.max(_CustomData.id)))
set_sequence_ids(
session_target.get_bind(),
trade_id=max_trade_id,
order_id=max_order_id,
pairlock_id=max_pairlock_id,
trade_id=(max_trade_id or 0) + 1,
order_id=(max_order_id or 0) + 1,
pairlock_id=(max_pairlock_id or 0) + 1,
kv_id=(max_kv_id or 0) + 1,
custom_data_id=(max_custom_data_id or 0) + 1,
)
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")
logger.info(
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
f"{kv_count} Key-Value pairs, and {custom_data_count} Custom Data entries."
)

View File

@@ -4,7 +4,7 @@ import sys
from typing import Any
from freqtrade.enums import RunMode
from freqtrade.exceptions import ConfigurationError, OperationalException
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
logger = logging.getLogger(__name__)
@@ -166,7 +166,14 @@ def start_list_strategies(args: dict[str, Any]) -> None:
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
for obj in strategy_objs:
if obj["class"]:
obj["hyperoptable"] = detect_all_parameters(obj["class"])
try:
obj["hyperoptable"] = detect_all_parameters(obj["class"])
except DependencyException as e:
logger.warning(
f"Cannot detect hyperoptable parameters for strategy {obj['name']}. Reason: {e}"
)
obj["hyperoptable"] = {}
else:
obj["hyperoptable"] = {}

View File

@@ -752,6 +752,7 @@ CONF_SCHEMA = {
"jwt_secret_key": {
"description": "Secret key for JWT authentication.",
"type": "string",
"default": "somethingRandomSomethingRandom123",
},
"CORS_origins": {
"description": "List of allowed CORS origins.",
@@ -764,7 +765,14 @@ CONF_SCHEMA = {
"enum": ["error", "info"],
},
},
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
"required": [
"enabled",
"listen_ip_address",
"listen_port",
"username",
"password",
"jwt_secret_key",
],
},
# end of RPC section
"db_url": {

View File

@@ -221,30 +221,30 @@ class Configuration:
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
)
config["exportfilename"] = Path(config["exportfilename"])
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
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 config.get("exportfilename"):
if Path(config["exportfilename"]).is_dir():
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"]
elif config.get("runmode") == RunMode.BACKTEST:
logger.warning(
"DEPRECATED: Using `--export-filename` has no impact when backtesting. "
"Please use `--notes` to annotate backtest results and "
"`--backtest-directory` to specify the output directory. "
)
if not config.get("exportdirectory"):
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
if not config.get("exportfilename"):
config["exportfilename"] = None
config["exportfilename"] = config.get("exportfilename", None)
if config.get("exportfilename"):
# ensure exportfilename is a Path object
config["exportfilename"] = Path(config["exportfilename"])
config["exportdirectory"] = Path(config["exportdirectory"])
if self.args.get("show_sensitive"):
logger.warning(
"Sensitive information will be shown in the upcoming output. "
"Please make sure to never share this output without redacting "
"the information yourself."
)
def _process_optimize_options(self, config: Config) -> None:
# This will override the strategy configuration
self._args_to_config(
@@ -312,6 +312,13 @@ class Configuration:
self._process_datadir_options(config)
if self.args.get("show_sensitive"):
logger.warning(
"Sensitive information will be shown in the upcoming output. "
"Please make sure to never share this output without redacting "
"the information yourself."
)
self._args_to_config(
config,
argname="strategy_list",

View File

@@ -239,3 +239,6 @@ IntOrInf = float
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
# Prefixes for low-priced coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
PairPrefixes = ["1000", "1000000", "1M", "K"]

View File

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

View File

@@ -70,28 +70,6 @@ class IDataHandler(ABC):
if match and len(match.groups()) > 1
]
@classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
"""
Returns a list of all pairs with ohlcv data available in this datadir
for the specified timeframe
:param datadir: Directory to search for ohlcv files
:param timeframe: Timeframe to search pairs for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: List of Pairs
"""
candle = ""
if candle_type != CandleType.SPOT:
datadir = datadir.joinpath("futures")
candle = f"-{candle_type}"
ext = cls._get_file_extension()
_tmp = [
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
]
# Check if regex found something and only return these results
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
@abstractmethod
def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class CandleType(str, Enum):
class CandleType(StrEnum):
"""Enum to distinguish candle types"""
SPOT = "spot"
@@ -14,9 +14,6 @@ class CandleType(str, Enum):
FUNDING_RATE = "funding_rate"
# BORROW_RATE = "borrow_rate" # * unimplemented
def __str__(self):
return f"{self.name.lower()}"
@staticmethod
def from_string(value: str) -> "CandleType":
if not value:

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class MarginMode(str, Enum):
class MarginMode(StrEnum):
"""
Enum to distinguish between
cross margin/futures margin_mode and
@@ -11,6 +11,3 @@ class MarginMode(str, Enum):
CROSS = "cross"
ISOLATED = "isolated"
NONE = ""
def __str__(self):
return f"{self.value.lower()}"

View File

@@ -1,6 +1,6 @@
from enum import Enum
from enum import StrEnum
class OrderTypeValues(str, Enum):
class OrderTypeValues(StrEnum):
limit = "limit"
market = "market"

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class PriceType(str, Enum):
class PriceType(StrEnum):
"""Enum to distinguish possible trigger prices for stoplosses"""
LAST = "last"

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class RPCMessageType(str, Enum):
class RPCMessageType(StrEnum):
STATUS = "status"
WARNING = "warning"
EXCEPTION = "exception"
@@ -25,21 +25,16 @@ class RPCMessageType(str, Enum):
NEW_CANDLE = "new_candle"
def __repr__(self):
return self.value
def __str__(self):
# TODO: do we still need to overwrite __repr__? Impact needs to be looked at in detail
return self.value
# Enum for parsing requests from ws consumers
class RPCRequestType(str, Enum):
class RPCRequestType(StrEnum):
SUBSCRIBE = "subscribe"
WHITELIST = "whitelist"
ANALYZED_DF = "analyzed_df"
def __str__(self):
return self.value
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class RunMode(str, Enum):
class RunMode(StrEnum):
"""
Bot running mode (backtest, hyperopt, ...)
can be "live", "dry-run", "backtest", "hyperopt".

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class SignalType(Enum):
class SignalType(StrEnum):
"""
Enum to distinguish between enter and exit signals
"""
@@ -11,11 +11,8 @@ class SignalType(Enum):
ENTER_SHORT = "enter_short"
EXIT_SHORT = "exit_short"
def __str__(self):
return f"{self.name.lower()}"
class SignalTagType(Enum):
class SignalTagType(StrEnum):
"""
Enum for signal columns
"""
@@ -23,13 +20,7 @@ class SignalTagType(Enum):
ENTER_TAG = "enter_tag"
EXIT_TAG = "exit_tag"
def __str__(self):
return f"{self.name.lower()}"
class SignalDirection(str, Enum):
class SignalDirection(StrEnum):
LONG = "long"
SHORT = "short"
def __str__(self):
return f"{self.name.lower()}"

View File

@@ -1,7 +1,7 @@
from enum import Enum
from enum import StrEnum
class TradingMode(str, Enum):
class TradingMode(StrEnum):
"""
Enum to distinguish between
spot, margin, futures or any other trading method
@@ -10,6 +10,3 @@ class TradingMode(str, Enum):
SPOT = "spot"
MARGIN = "margin"
FUTURES = "futures"
def __str__(self):
return f"{self.name.lower()}"

View File

@@ -48,6 +48,7 @@ class Binance(Exchange):
"has_delisting": True,
}
_ft_has_futures: FtHas = {
"ohlcv_candle_limit": 499,
"funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"stoploss_blocks_assets": False, # Stoploss orders do not block assets

File diff suppressed because it is too large Load Diff

View File

@@ -106,6 +106,7 @@ from freqtrade.misc import (
file_dump_json,
file_load_json,
safe_value_fallback,
safe_value_nested,
)
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
@@ -207,7 +208,7 @@ class Exchange:
self._config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
)
self.margin_mode: MarginMode = MarginMode(
MarginMode(self._config.get("margin_mode"))
self._config["margin_mode"]
if self._config.get("margin_mode")
else self._supported_trading_mode_margin_pairs[0][1]
)
@@ -313,10 +314,19 @@ class Exchange:
if self._exchange_ws:
self._exchange_ws.cleanup()
logger.debug("Exchange object destroyed, closing async loop")
try:
generic_loop = asyncio.get_running_loop()
except RuntimeError:
generic_loop = None
loop_running = (getattr(self, "loop", None) and self.loop.is_running()) or (
generic_loop is not None and generic_loop.is_running()
)
if (
getattr(self, "_api_async", None)
and inspect.iscoroutinefunction(self._api_async.close)
and self._api_async.session
and not loop_running
):
logger.debug("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close())
@@ -324,6 +334,7 @@ class Exchange:
self._ws_async
and inspect.iscoroutinefunction(self._ws_async.close)
and self._ws_async.session
and not loop_running
):
logger.debug("Closing ws ccxt session.")
self.loop.run_until_complete(self._ws_async.close())
@@ -982,12 +993,12 @@ class Exchange:
swap.linear.fetchOHLCV.limit
"""
feat = (
self._api_async.features.get("spot", {})
safe_value_nested(self._api_async.features, "spot", {})
if market_type == "spot"
else self._api_async.features.get("swap", {}).get("linear", {})
else safe_value_nested(self._api_async.features, "swap.linear", {})
)
return feat.get(endpoint, {}).get(attribute, default)
return safe_value_nested(feat, f"{endpoint}.{attribute}", default)
def get_precision_amount(self, pair: str) -> float | None:
"""
@@ -1156,7 +1167,7 @@ class Exchange:
orderbook: OrderBook | None = None
if self.exchange_has("fetchL2OrderBook"):
orderbook = self.fetch_l2_order_book(pair, 20)
if ordertype == "limit" and orderbook:
if not stop_loss and ordertype == "limit" and orderbook:
# Allow a 1% price difference
allowed_diff = 0.01
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
@@ -1293,6 +1304,7 @@ class Exchange:
Check dry-run limit order fill and update fee (if it filled).
"""
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
# Stoploss branch
pair = order["symbol"]
if not orderbook and self.exchange_has("fetchL2OrderBook"):
orderbook = self.fetch_l2_order_book(pair, 20)
@@ -1300,6 +1312,11 @@ class Exchange:
crossed = self._dry_is_price_crossed(
pair, order["side"], price, orderbook, is_stop=True
)
if crossed and immediate:
raise InvalidOrderException(
"Could not create dry stoploss order. Stoploss would trigger immediately."
)
if crossed:
average = self.get_dry_market_fill_price(
pair,
@@ -2896,8 +2913,11 @@ class Exchange:
}
pairs_to_download = [p for p in pairs if p not in candles]
if pairs_to_download:
candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
for c, val in candles.items():
candles_new = self.refresh_latest_ohlcv(
pairs_to_download, since_ms=since_ms, cache=False
)
for c, val in candles_new.items():
candles[c] = val
self._expiring_candle_cache[(c[1], since_ms)][c] = val
return candles

View File

@@ -446,7 +446,7 @@ class FreqaiDataDrawer:
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
pattern = re.compile(r"^sub-train-(.+)_(\d{10})$")
delete_dict: dict[str, Any] = {}

View File

@@ -2421,7 +2421,10 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate re-entries
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
prot_trig = self.protections.stop_per_pair(pair, side=side)
starting_balance = self.wallets.get_starting_balance()
prot_trig = self.protections.stop_per_pair(
pair, side=side, starting_balance=starting_balance
)
if prot_trig:
msg: RPCProtectionMsg = {
"type": RPCMessageType.PROTECTION_TRIGGER,
@@ -2430,7 +2433,7 @@ class FreqtradeBot(LoggingMixin):
}
self.rpc.send_msg(msg)
prot_trig_glb = self.protections.global_stop(side=side)
prot_trig_glb = self.protections.global_stop(side=side, starting_balance=starting_balance)
if prot_trig_glb:
msg = {
"type": RPCMessageType.PROTECTION_TRIGGER_GLOBAL,

View File

@@ -34,6 +34,7 @@ class PointAnnotationType(_BaseAnnotationType, total=False):
y: float
size: int
shape: Literal["circle", "rect", "roundRect", "triangle", "pin", "arrow", "none"]
rotate: int
AnnotationType = AreaAnnotationType | LineAnnotationType | PointAnnotationType

View File

@@ -84,7 +84,12 @@ def file_load_json(file: Path):
def is_file_in_dir(file: Path, directory: Path) -> bool:
"""
Helper function to check if file is in directory.
Helper function to check if file is directly within a directory.
:param file: File to check
:param directory: Directory to check against
When used in the API, this parameter cannot be user controlled (outside of the config)
to avoid security issues.
:return: True if file is directly within directory, False otherwise
"""
return file.is_file() and file.parent.samefile(directory)
@@ -125,6 +130,27 @@ def round_dict(d, n):
DictMap = dict[str, Any] | Mapping[str, Any]
def safe_value_nested(obj: DictMap, keys: str, default_value=None):
"""
Search a nested dict for a value.
:param obj: dict to search in
:param keys: dot separated keys to search for
:param default_value: value to return if the key is not found or value is None
:return: value found in dict or default_value
Sample:
>>> d = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
>>> safe_value_nested(d, "first.rows.pass") == "dog"
True
"""
nested_obj = obj
for key in keys.split("."):
if isinstance(nested_obj, Mapping) and key in nested_obj and nested_obj[key] is not None:
nested_obj = nested_obj[key]
else:
return default_value
return nested_obj
def safe_value_fallback(obj: DictMap, key1: str, key2: str | None = None, default_value=None):
"""
Search a value in obj, return this if it's not None.
@@ -210,12 +236,12 @@ def remove_entry_exit_signals(dataframe: pd.DataFrame):
:param dataframe: The DataFrame to remove signals from
"""
dataframe[SignalType.ENTER_LONG.value] = 0
dataframe[SignalType.EXIT_LONG.value] = 0
dataframe[SignalType.ENTER_SHORT.value] = 0
dataframe[SignalType.EXIT_SHORT.value] = 0
dataframe[SignalTagType.ENTER_TAG.value] = None
dataframe[SignalTagType.EXIT_TAG.value] = None
dataframe[SignalType.ENTER_LONG] = 0
dataframe[SignalType.EXIT_LONG] = 0
dataframe[SignalType.ENTER_SHORT] = 0
dataframe[SignalType.EXIT_SHORT] = 0
dataframe[SignalTagType.ENTER_TAG] = None
dataframe[SignalTagType.EXIT_TAG] = None
return dataframe

View File

@@ -136,6 +136,7 @@ class Backtesting:
"exited": {},
}
self.rejected_dict: dict[str, list] = {}
self.starting_balance: float = 0.0
self._exchange_name = self.config["exchange"]["name"]
self.__initial_backtest = exchange is None
@@ -277,6 +278,7 @@ class Backtesting:
self.reset_backtest(False)
self.wallets = Wallets(self.config, self.exchange, is_backtest=True)
self.starting_balance = self.wallets.get_starting_balance()
self.progress = BTProgress()
self.abort = False
@@ -605,8 +607,6 @@ class Backtesting:
trade_dur: int,
) -> float:
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
roi_entry, roi = self.strategy.min_roi_reached_entry(
trade, # type: ignore[arg-type]
trade_dur,
@@ -619,10 +619,7 @@ class Backtesting:
# - we'll use open instead of close
return row[OPEN_IDX]
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
roi_rate = trade.open_rate * roi / leverage
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1)
close_rate = trade.calc_close_rate_for_roi(roi)
if is_short:
is_new_roi = row[OPEN_IDX] < close_rate
else:
@@ -1276,8 +1273,8 @@ class Backtesting:
def run_protections(self, pair: str, current_time: datetime, side: LongShort):
if self.enable_protections:
self.protections.stop_per_pair(pair, current_time, side)
self.protections.global_stop(current_time, side)
self.protections.stop_per_pair(pair, current_time, side, self.starting_balance)
self.protections.global_stop(current_time, side, self.starting_balance)
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: tuple) -> bool:
"""

View File

@@ -1,5 +1,5 @@
from datetime import UTC, datetime
from enum import Enum
from enum import StrEnum
from typing import ClassVar, Literal
from sqlalchemy import String
@@ -11,7 +11,7 @@ from freqtrade.persistence.base import ModelBase, SessionType
ValueTypes = str | datetime | float | int
class ValueTypesEnum(str, Enum):
class ValueTypesEnum(StrEnum):
STRING = "str"
DATETIME = "datetime"
FLOAT = "float"

View File

@@ -30,25 +30,39 @@ def get_backup_name(tabs: list[str], backup_prefix: str):
return table_back_name
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
order_id: int | None = None
trade_id: int | None = None
def get_last_sequence_ids(engine, sequence_name: str, table_back_name: str) -> int | None:
last_id: int | None = None
if engine.name == "postgresql":
with engine.begin() as connection:
trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
last_id = connection.execute(text(f"select nextval('{sequence_name}')")).fetchone()[0]
with engine.begin() as connection:
connection.execute(
text(f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak")
text(f"ALTER SEQUENCE {sequence_name} rename to {table_back_name}_id_seq_bak")
)
connection.execute(
text(f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak")
)
return order_id, trade_id
return last_id
def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
def set_sequence_ids(
engine,
order_id: int | None = None,
trade_id: int | None = None,
pairlock_id: int | None = None,
kv_id: int | None = None,
custom_data_id: int | None = None,
):
"""
Set sequence ids to the given values.
The id's given should be the next id to use, so the current max id + 1 - or current id
if using nextval before migration.
:param engine: SQLAlchemy engine
:param order_id: value to set for orders_id_seq (optional)
:param trade_id: value to set for trades_id_seq (optional)
:param pairlock_id: value to set for pairlocks_id_seq (optional)
:param kv_id: value to set for KeyValueStore_id_seq (optional)
:param custom_data_id: value to set for trade_custom_data_id_seq (optional)
"""
if engine.name == "postgresql":
with engine.begin() as connection:
if order_id:
@@ -59,6 +73,14 @@ def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
connection.execute(
text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}")
)
if kv_id:
connection.execute(
text(f'ALTER SEQUENCE "KeyValueStore_id_seq" RESTART WITH {kv_id}')
)
if custom_data_id:
connection.execute(
text(f"ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH {custom_data_id}")
)
def drop_index_on_table(engine, inspector, table_bak_name):
@@ -157,7 +179,8 @@ def migrate_trades_and_orders_table(
drop_index_on_table(engine, inspector, trade_back_name)
order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
order_id = get_last_sequence_ids(engine, "order_id_seq", order_back_name)
trade_id = get_last_sequence_ids(engine, "trades_id_seq", trade_back_name)
drop_orders_table(engine, order_back_name)
@@ -269,6 +292,7 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}"))
drop_index_on_table(engine, inspector, pairlock_back_name)
pairlock_id = get_last_sequence_ids(engine, "pairlocks_id_seq", pairlock_back_name)
side = get_column_def(cols, "side", "'*'")
@@ -288,6 +312,8 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
)
)
set_sequence_ids(engine, pairlock_id=pairlock_id)
def set_sqlite_to_wal(engine):
if engine.name == "sqlite" and str(engine.url) != "sqlite://":

View File

@@ -86,7 +86,7 @@ class PairLocks:
lock
for lock in PairLocks.locks
if (
lock.lock_end_time >= now
lock.lock_end_time > now
and lock.active is True
and (pair is None or lock.pair == pair)
and (side is None or lock.side == "*" or lock.side == side)

View File

@@ -1208,6 +1208,35 @@ class LocalTrade:
return float(f"{profit_ratio:.8f}")
def calc_close_rate_for_roi(self, target_roi: float) -> float:
"""
Calculate the required close price to reach a target ROI.
Must match the logic used in `calc_profit_ratio()`.
:param target_roi: The desired return on investment (as a decimal, e.g., 0.05 for 5%)
:return: Close price (rate) required to achieve the target ROI
"""
leverage = float(self.leverage or 1.0)
deleveraged_roi = float(target_roi) / leverage
open_value = self._calc_open_trade_value(self.amount, self.open_rate)
# The ROI formula uses close_value(rate), which depends on trading mode:
# - SPOT: linear in rate, adjusted by close fee
# - MARGIN: same, but long subtracts interest, short increases amount
# - FUTURES: adds/subtracts funding to/from close value
# All cases are affine in rate:
# close_value(rate) = a * rate + b
# We extract a and b by probing close_value at rate = 0 and 1.
value_at_0 = self.calc_close_trade_value(0.0)
value_at_1 = self.calc_close_trade_value(1.0)
alpha = value_at_1 - value_at_0
beta = value_at_0
s = -1.0 if self.is_short else 1.0
adj = 1.0 + (deleveraged_roi / s)
return (adj * open_value - beta) / alpha
def recalc_trade_from_orders(self, *, is_closing: bool = False):
ZERO = FtPrecise(0.0)
current_amount = FtPrecise(0.0)

View File

@@ -261,10 +261,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
if trades is not None and len(trades) > 0:
# Create description for exit summarizing the trade
trades["desc"] = trades.apply(
lambda row: f"{row['profit_ratio']:.2%}, "
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
+ f"{row['exit_reason']}, "
+ f"{row['trade_duration']} min",
lambda row: (
f"{row['profit_ratio']:.2%}, "
+ (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
+ f"{row['exit_reason']}, "
+ f"{row['trade_duration']} min"
),
axis=1,
)
trade_entries = go.Scatter(

View File

@@ -5,7 +5,7 @@ PairList Handler base class
import logging
from abc import ABC, abstractmethod
from copy import deepcopy
from enum import Enum
from enum import StrEnum
from typing import Any, Literal, TypedDict
from freqtrade.constants import Config
@@ -58,7 +58,7 @@ PairlistParameter = (
)
class SupportsBacktesting(str, Enum):
class SupportsBacktesting(StrEnum):
"""
Enum to indicate if a Pairlist Handler supports backtesting.
"""

View File

@@ -7,6 +7,7 @@ Provides dynamic pair list based on Market Cap
import logging
import math
from freqtrade.constants import PairPrefixes
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
@@ -162,9 +163,6 @@ class MarketCapPairList(IPairList):
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,
@@ -179,7 +177,7 @@ class MarketCapPairList(IPairList):
return pair
if pair not in markets:
for prefix in self.prefixes:
for prefix in PairPrefixes:
test_prefix = f"{prefix}{pair}"
if test_prefix in pairlist:

View File

@@ -47,13 +47,17 @@ class ProtectionManager:
"""
return [{p.name: p.short_desc()} for p in self._protection_handlers]
def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None:
def global_stop(
self, now: datetime | None = None, side: LongShort = "long", starting_balance: float = 0.0
) -> PairLock | None:
if not now:
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop:
lock = protection_handler.global_stop(date_now=now, side=side)
lock = protection_handler.global_stop(
date_now=now, side=side, starting_balance=starting_balance
)
if lock and lock.until:
if not PairLocks.is_global_lock(lock.until, side=lock.lock_side):
result = PairLocks.lock_pair(
@@ -62,14 +66,20 @@ class ProtectionManager:
return result
def stop_per_pair(
self, pair, now: datetime | None = None, side: LongShort = "long"
self,
pair,
now: datetime | None = None,
side: LongShort = "long",
starting_balance: float = 0.0,
) -> PairLock | None:
if not now:
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop:
lock = protection_handler.stop_per_pair(pair=pair, date_now=now, side=side)
lock = protection_handler.stop_per_pair(
pair=pair, date_now=now, side=side, starting_balance=starting_balance
)
if lock and lock.until:
if not PairLocks.is_pair_locked(pair, lock.until, lock.lock_side):
result = PairLocks.lock_pair(

View File

@@ -52,7 +52,9 @@ class CooldownPeriod(IProtection):
return None
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
def global_stop(
self, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@@ -63,7 +65,7 @@ class CooldownPeriod(IProtection):
return None
def stop_per_pair(
self, pair: str, date_now: datetime, side: LongShort
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for this pair

View File

@@ -102,7 +102,9 @@ class IProtection(LoggingMixin, ABC):
"""
@abstractmethod
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
def global_stop(
self, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@@ -110,7 +112,7 @@ class IProtection(LoggingMixin, ABC):
@abstractmethod
def stop_per_pair(
self, pair: str, date_now: datetime, side: LongShort
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for this pair

View File

@@ -81,7 +81,9 @@ class LowProfitPairs(IProtection):
return None
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
def global_stop(
self, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@@ -91,7 +93,7 @@ class LowProfitPairs(IProtection):
return None
def stop_per_pair(
self, pair: str, date_now: datetime, side: LongShort
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for this pair

View File

@@ -22,6 +22,7 @@ class MaxDrawdown(IProtection):
self._trade_limit = protection_config.get("trade_limit", 1)
self._max_allowed_drawdown = protection_config.get("max_allowed_drawdown", 0.0)
self._calculation_mode = protection_config.get("calculation_mode", "ratios")
# TODO: Implement checks to limit max_drawdown to sensible values
def short_desc(self) -> str:
@@ -42,25 +43,53 @@ class MaxDrawdown(IProtection):
f"locking {self.unlock_reason_time_element}."
)
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn | None:
def _max_drawdown(self, date_now: datetime, starting_balance: float) -> ProtectionReturn | None:
"""
Evaluate recent trades for drawdown ...
"""
look_back_until = date_now - timedelta(minutes=self._lookback_period)
trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until)
trades_in_window = Trade.get_trades_proxy(is_open=False, close_date=look_back_until)
trades_df = pd.DataFrame([trade.to_json() for trade in trades])
if len(trades) < self._trade_limit:
# Not enough trades in the relevant period
if len(trades_in_window) < self._trade_limit:
return None
# Drawdown is always positive
try:
# TODO: This should use absolute profit calculation, considering account balance.
drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit")
drawdown = drawdown_obj.drawdown_abs
if self._calculation_mode == "equity":
# Standard equity-based drawdown
# Get all trades to calculate cumulative profit before the window
all_closed_trades = Trade.get_trades_proxy(is_open=False)
profit_before_window = sum(
trade.close_profit_abs or 0.0
for trade in all_closed_trades
if trade.close_date_utc <= look_back_until
)
trades_df = pd.DataFrame(
[
{"close_date": t.close_date_utc, "profit_abs": t.close_profit_abs}
for t in trades_in_window
]
)
actual_starting_balance = starting_balance + profit_before_window
drawdown_obj = calculate_max_drawdown(
trades_df,
value_col="profit_abs",
starting_balance=actual_starting_balance,
relative=True,
)
drawdown = drawdown_obj.relative_account_drawdown
else:
# Legacy ratios-based calculation (default)
trades_df = pd.DataFrame(
[
{"close_date": t.close_date_utc, "close_profit": t.close_profit}
for t in trades_in_window
]
)
drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit")
# In ratios mode, drawdown_abs is the cumulative ratio drop
drawdown = drawdown_obj.drawdown_abs
except ValueError:
return None
@@ -71,7 +100,7 @@ class MaxDrawdown(IProtection):
logger.info,
)
until = self.calculate_lock_end(trades)
until = self.calculate_lock_end(trades_in_window)
return ProtectionReturn(
lock=True,
@@ -81,17 +110,19 @@ class MaxDrawdown(IProtection):
return None
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
def global_stop(
self, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
:return: Tuple of [bool, until, reason].
If true, all pairs will be locked with <reason> until <until>
"""
return self._max_drawdown(date_now)
return self._max_drawdown(date_now, starting_balance)
def stop_per_pair(
self, pair: str, date_now: datetime, side: LongShort
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for this pair

View File

@@ -86,7 +86,9 @@ class StoplossGuard(IProtection):
lock_side=(side if self._only_per_side else "*"),
)
def global_stop(self, date_now: datetime, side: LongShort) -> ProtectionReturn | None:
def global_stop(
self, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@@ -98,7 +100,7 @@ class StoplossGuard(IProtection):
return self._stoploss_guard(date_now, None, side)
def stop_per_pair(
self, pair: str, date_now: datetime, side: LongShort
self, pair: str, date_now: datetime, side: LongShort, starting_balance: float
) -> ProtectionReturn | None:
"""
Stops trading (position entering) for this pair

View File

@@ -15,6 +15,7 @@ from freqtrade.rpc.api_server.deps import get_api_config
logger = logging.getLogger(__name__)
ALGORITHM = "HS256"
__DEFAULT_JWT = "somethingRandomSomethingRandom123"
router_login = APIRouter()
@@ -59,7 +60,7 @@ async def validate_ws_token(
api_config: dict[str, Any] = Depends(get_api_config),
):
secret_ws_token = api_config.get("ws_token", None)
secret_jwt_key = api_config.get("jwt_secret_key", "super-secret")
secret_jwt_key = api_config["jwt_secret_key"]
# Check if ws_token is/in secret_ws_token
if ws_token and secret_ws_token:
@@ -111,7 +112,7 @@ def http_basic_or_jwt_token(
api_config=Depends(get_api_config),
):
if token:
return get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"))
return get_user_from_token(token, api_config["jwt_secret_key"])
elif form_data and verify_auth(api_config, form_data.username, form_data.password):
return form_data.username
@@ -129,12 +130,12 @@ def token_login(
token_data = {"identity": {"u": form_data.username}}
access_token = create_token(
token_data,
api_config.get("jwt_secret_key", "super-secret"),
api_config["jwt_secret_key"],
token_type="access", # noqa: S106
)
refresh_token = create_token(
token_data,
api_config.get("jwt_secret_key", "super-secret"),
api_config["jwt_secret_key"],
token_type="refresh", # noqa: S106
)
return {
@@ -151,11 +152,11 @@ def token_login(
@router_login.post("/token/refresh", response_model=AccessToken)
def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_api_config)):
# Refresh token
u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh")
u = get_user_from_token(token, api_config["jwt_secret_key"], "refresh")
token_data = {"identity": {"u": u}}
access_token = create_token(
token_data,
api_config.get("jwt_secret_key", "super-secret"),
api_config["jwt_secret_key"],
token_type="access", # noqa: S106
)
return {"access_token": access_token}

View File

@@ -30,7 +30,7 @@ from freqtrade.rpc.api_server.api_schemas import (
BacktestRequest,
BacktestResponse,
)
from freqtrade.rpc.api_server.deps import get_config
from freqtrade.rpc.api_server.deps import get_config, verify_strategy
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from freqtrade.rpc.rpc import RPCException
@@ -134,8 +134,7 @@ async def api_start_backtest(
if ApiBG.bgtask_running:
raise RPCException("Bot Background task already running")
if ":" in bt_settings.strategy:
raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
verify_strategy(bt_settings.strategy)
btconfig = deepcopy(config)
remove_exchange_credentials(btconfig["exchange"], True)

View File

@@ -63,6 +63,7 @@ def pairlists_evaluate(
config_loc["timeframes"] = payload.timeframes
config_loc["erase"] = payload.erase
config_loc["download_trades"] = payload.download_trades
config_loc["prepend_data"] = payload.prepend_data
if payload.candle_types is not None:
config_loc["candle_types"] = payload.candle_types

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException
from freqtrade.configuration import validate_config_consistency
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
from freqtrade.rpc.api_server.api_schemas import PairHistory, PairHistoryRequest
from freqtrade.rpc.api_server.deps import get_config, get_exchange
from freqtrade.rpc.api_server.deps import get_config, get_exchange, verify_strategy
from freqtrade.rpc.rpc import RPC
@@ -25,6 +25,7 @@ def pair_history(
config=Depends(get_config),
exchange=Depends(get_exchange),
):
verify_strategy(strategy)
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config_loc = deepcopy(config)
@@ -45,6 +46,7 @@ def pair_history(
@router.post("/pair_history", response_model=PairHistory, tags=["Candle data"])
def pair_history_filtered(payload: PairHistoryRequest, config=Depends(get_config)):
verify_strategy(payload.strategy)
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config_loc = deepcopy(config)

View File

@@ -1,7 +1,7 @@
from datetime import date, datetime
from typing import Any
from typing import Annotated, Any, Literal
from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny, model_validator
from pydantic import AwareDatetime, BaseModel, Field, RootModel, SerializeAsAny, model_validator
from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
@@ -513,6 +513,7 @@ class DownloadDataPayload(ExchangeModePayloadMixin, BaseModel):
erase: bool = False
download_trades: bool = False
candle_types: list[str] | None = None
prepend_data: bool = False
@model_validator(mode="before")
def check_mutually_exclusive(cls, values):
@@ -526,10 +527,59 @@ class FreqAIModelListResponse(BaseModel):
freqaimodels: list[str]
class __StrategyParameter(BaseModel):
param_type: str
name: str
space: str
load: bool
optimize: bool
class IntParameter(__StrategyParameter):
param_type: Literal["IntParameter"]
value: int
low: int
high: int
class RealParameter(__StrategyParameter):
param_type: Literal["RealParameter"]
value: float
low: float
high: float
class DecimalParameter(__StrategyParameter):
param_type: Literal["DecimalParameter"]
value: float
low: float
high: float
decimals: int
class BooleanParameter(__StrategyParameter):
param_type: Literal["BooleanParameter"]
value: bool | None
opt_range: list[bool]
class CategoricalParameter(__StrategyParameter):
param_type: Literal["CategoricalParameter"]
value: Any
opt_range: list[Any]
AllParameters = Annotated[
BooleanParameter | CategoricalParameter | DecimalParameter | IntParameter | RealParameter,
Field(discriminator="param_type"),
]
class StrategyResponse(BaseModel):
strategy: str
code: str
timeframe: str | None
params: list[AllParameters] = Field(default_factory=list)
code: str
class AvailablePairs(BaseModel):

View File

@@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException
from freqtrade import __version__
from freqtrade.enums import RunMode, State
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
from freqtrade.rpc.api_server.api_schemas import (
@@ -17,10 +18,17 @@ from freqtrade.rpc.api_server.api_schemas import (
Ping,
PlotConfig,
ShowConfig,
StrategyResponse,
SysInfo,
Version,
)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.api_server.deps import (
get_config,
get_exchange,
get_rpc,
get_rpc_optional,
verify_strategy,
)
from freqtrade.rpc.rpc import RPCException
@@ -59,7 +67,9 @@ logger = logging.getLogger(__name__)
# 2.43: Add /profit_all endpoint
# 2.44: Add candle_types parameter to download-data endpoint
# 2.45: Add price to forceexit endpoint
API_VERSION = 2.45
# 2.46: Add prepend_data to download-data endpoint
# 2.47: Add Strategy parameters
API_VERSION = 2.47
# Public API, requires no auth.
router_public = APIRouter()
@@ -67,7 +77,7 @@ router_public = APIRouter()
router = APIRouter()
@router_public.get("/ping", response_model=Ping, tags=["Info"])
@router_public.api_route("/ping", methods=["GET", "HEAD"], response_model=Ping, tags=["Info"])
def ping():
"""simple ping"""
return {"status": "pong"}
@@ -138,6 +148,46 @@ def markets(
}
@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["Strategy"])
def get_strategy(
strategy: str, config=Depends(get_config), rpc: RPC | None = Depends(get_rpc_optional)
):
verify_strategy(strategy)
if not rpc or config["runmode"] == RunMode.WEBSERVER:
# webserver mode
config_ = deepcopy(config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(
strategy, config_, extra_dir=config_.get("strategy_path")
)
strategy_obj.ft_load_hyper_params()
except OperationalException:
raise HTTPException(status_code=404, detail="Strategy not found")
except Exception:
logger.exception("Unexpected error while loading strategy '%s'.", strategy)
raise HTTPException(
status_code=502,
detail="Unexpected error while loading strategy.",
)
else:
# trade mode
strategy_obj = rpc._freqtrade.strategy
if strategy_obj.get_strategy_name() != strategy:
raise HTTPException(
status_code=404,
detail="Only the currently active strategy is available in trade mode",
)
return {
"strategy": strategy_obj.get_strategy_name(),
"timeframe": getattr(strategy_obj, "timeframe", None),
"code": strategy_obj.__source__,
"params": [p for _, p in strategy_obj.enumerate_parameters()],
}
@router.get("/sysinfo", response_model=SysInfo, tags=["Info"])
def sysinfo():
return RPC._rpc_sysinfo()

View File

@@ -1,19 +1,15 @@
import logging
from copy import deepcopy
from fastapi import APIRouter, Depends
from fastapi.exceptions import HTTPException
from freqtrade.data.history.datahandlers import get_datahandler
from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.rpc.api_server.api_schemas import (
AvailablePairs,
ExchangeListResponse,
FreqAIModelListResponse,
HyperoptLossListResponse,
StrategyListResponse,
StrategyResponse,
)
from freqtrade.rpc.api_server.deps import get_config
@@ -36,29 +32,6 @@ def list_strategies(config=Depends(get_config)):
return {"strategies": [x["name"] for x in strategies]}
@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["Strategy"])
def get_strategy(strategy: str, config=Depends(get_config)):
if ":" in strategy:
raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
config_ = deepcopy(config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(
strategy, config_, extra_dir=config_.get("strategy_path")
)
except OperationalException:
raise HTTPException(status_code=404, detail="Strategy not found")
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
return {
"strategy": strategy_obj.get_strategy_name(),
"code": strategy_obj.__source__,
"timeframe": getattr(strategy_obj, "timeframe", None),
}
@router.get("/exchanges", response_model=ExchangeListResponse, tags=[])
def list_exchanges(config=Depends(get_config)):
from freqtrade.exchange import list_available_exchanges

View File

@@ -75,3 +75,12 @@ def is_trading_mode(config=Depends(get_config)):
if config["runmode"] not in TRADE_MODES:
raise HTTPException(status_code=503, detail="Bot is not in the correct state.")
return None
def verify_strategy(strategy: str | None):
"""Verify that the strategy name is valid (not base64 encoded).
This is a security measure to prevent potential attacks using base64 encoded strategies.
This should be called for every endpoint that accepts a strategy name as a parameter.
"""
if strategy is not None and ":" in strategy:
raise HTTPException(status_code=422, detail="base64 encoded strategies are not allowed.")

View File

@@ -5,20 +5,20 @@ from fastapi.exceptions import HTTPException
from starlette.responses import FileResponse
router_ui = APIRouter()
router_ui = APIRouter(include_in_schema=False, tags=["Web UI"])
@router_ui.get("/favicon.ico", include_in_schema=False)
@router_ui.get("/favicon.ico")
async def favicon():
return FileResponse(str(Path(__file__).parent / "ui/favicon.ico"))
@router_ui.get("/fallback_file.html", include_in_schema=False)
@router_ui.get("/fallback_file.html")
async def fallback():
return FileResponse(str(Path(__file__).parent / "ui/fallback_file.html"))
@router_ui.get("/ui_version", include_in_schema=False)
@router_ui.get("/ui_version")
async def ui_version():
from freqtrade.commands.deploy_ui import read_ui_version
@@ -30,15 +30,15 @@ async def ui_version():
}
@router_ui.get("/{rest_of_path:path}", include_in_schema=False)
@router_ui.get("/{rest_of_path:path}")
async def index_html(rest_of_path: str):
"""
Emulate path fallback to index.html.
"""
if rest_of_path.startswith("api") or rest_of_path.startswith("."):
raise HTTPException(status_code=404, detail="Not Found")
uibase = Path(__file__).parent / "ui/installed/"
filename = uibase / rest_of_path
uibase = (Path(__file__).parent / "ui/installed/").resolve()
filename = (uibase / rest_of_path).resolve()
# It's security relevant to check "relative_to".
# Without this, Directory-traversal is possible.
media_type: str | None = None

View File

@@ -302,7 +302,9 @@ class ApiServer(RPCHandler):
)
if self._config["api_server"].get("jwt_secret_key", "super-secret") in (
"super-secret, somethingrandom"
"super-secret",
"somethingrandom",
"somethingRandomSomethingRandom123",
):
logger.warning(
"SECURITY WARNING - `jwt_secret_key` seems to be default."

View File

@@ -37,7 +37,8 @@ class ApiBG:
# Generic background jobs
# TODO: Change this to FtTTLCache
# TODO: Change this to FtTTLCache -> must be more intelligent than FtTTLCache - as we can't
# evict still running jobs.
jobs: dict[str, JobsContainer] = {}
# Pairlist evaluate things
pairlist_running: bool = False

View File

@@ -43,7 +43,7 @@ class WebSocketChannel:
self._channel_tasks: list[asyncio.Task] = []
# Deque for average send times
self._send_times: deque[float] = deque([], maxlen=10)
self._send_times: deque[float] = deque(maxlen=10)
# High limit defaults to 3 to start
self._send_high_limit = 3
self._send_throttle = send_throttle

View File

@@ -127,26 +127,29 @@ class ExternalMessageConsumer:
self._channel_streams = {}
if self._sub_tasks:
# Cancel sub tasks
for task in self._sub_tasks:
task.cancel()
asyncio.run_coroutine_threadsafe(self._shutdown_async(), loop=self._loop)
if self._main_task:
# Cancel the main task
self._main_task.cancel()
self._thread.join()
self._thread.join(timeout=5)
self._thread = None
self._loop = None
self._sub_tasks = None
self._main_task = None
async def _shutdown_async(self):
"""Cancel all tasks, let them finish, then stop the loop."""
if self._sub_tasks:
for task in self._sub_tasks:
task.cancel()
await asyncio.gather(*self._sub_tasks, return_exceptions=True)
if self._main_task:
self._main_task.cancel()
self._loop.stop()
async def _main(self):
"""
The main task coroutine
"""
"""The main task coroutine"""
lock = asyncio.Lock()
try:
@@ -161,7 +164,8 @@ class ExternalMessageConsumer:
pass
finally:
# Stop the loop once we are done
self._loop.stop()
if self._loop:
self._loop.stop()
async def _handle_producer_connection(self, producer: Producer, lock: asyncio.Lock):
"""

View File

@@ -9,7 +9,7 @@ from collections.abc import Iterator
from pathlib import Path
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import deep_merge_dicts
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.strategy.parameters import BaseParameter
@@ -179,10 +179,10 @@ def detect_all_parameters(
attr.space = space
break
if attr.space is None:
raise OperationalException(f"Cannot determine parameter space for {attr_name}.")
raise DependencyException(f"Cannot determine parameter space for {attr_name}.")
if attr.space in ("all", "default") or attr.space.isidentifier() is False:
raise OperationalException(
raise DependencyException(
f"'{attr.space}' is not a valid space. Parameter: {attr_name}."
)
attr.name = attr_name

View File

@@ -367,7 +367,7 @@ class IStrategy(ABC, HyperStrategyMixin):
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -403,7 +403,7 @@ class IStrategy(ABC, HyperStrategyMixin):
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -453,7 +453,7 @@ class IStrategy(ABC, HyperStrategyMixin):
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
@@ -511,7 +511,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
Custom entry price logic, returning the new entry price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set entry price
@@ -539,7 +539,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
Custom exit price logic, returning the new exit price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set exit price
@@ -666,7 +666,7 @@ class IStrategy(ABC, HyperStrategyMixin):
This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None
@@ -706,7 +706,7 @@ class IStrategy(ABC, HyperStrategyMixin):
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
@@ -743,7 +743,7 @@ class IStrategy(ABC, HyperStrategyMixin):
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
@@ -781,7 +781,7 @@ class IStrategy(ABC, HyperStrategyMixin):
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
@@ -925,9 +925,9 @@ class IStrategy(ABC, HyperStrategyMixin):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
@@ -956,9 +956,9 @@ class IStrategy(ABC, HyperStrategyMixin):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -985,7 +985,7 @@ class IStrategy(ABC, HyperStrategyMixin):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -1001,7 +1001,7 @@ class IStrategy(ABC, HyperStrategyMixin):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
@@ -1329,13 +1329,13 @@ class IStrategy(ABC, HyperStrategyMixin):
return False, False, None
if is_short:
enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
enter = latest.get(SignalType.ENTER_SHORT, 0) == 1
exit_ = latest.get(SignalType.EXIT_SHORT, 0) == 1
else:
enter = latest.get(SignalType.ENTER_LONG.value, 0) == 1
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
enter = latest.get(SignalType.ENTER_LONG, 0) == 1
exit_ = latest.get(SignalType.EXIT_LONG, 0) == 1
exit_tag = latest.get(SignalTagType.EXIT_TAG, None)
# Tags can be None, which does not resolve to False.
exit_tag = exit_tag if isinstance(exit_tag, str) and exit_tag != "nan" else None
@@ -1362,16 +1362,16 @@ class IStrategy(ABC, HyperStrategyMixin):
if latest is None or latest_date is None:
return None, None
enter_long = latest.get(SignalType.ENTER_LONG.value, 0) == 1
exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1
enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
enter_long = latest.get(SignalType.ENTER_LONG, 0) == 1
exit_long = latest.get(SignalType.EXIT_LONG, 0) == 1
enter_short = latest.get(SignalType.ENTER_SHORT, 0) == 1
exit_short = latest.get(SignalType.EXIT_SHORT, 0) == 1
enter_signal: SignalDirection | None = None
enter_tag: str | None = None
if enter_long == 1 and not any([exit_long, enter_short]):
enter_signal = SignalDirection.LONG
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
enter_tag = latest.get(SignalTagType.ENTER_TAG, None)
if (
self.config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT
and self.can_short
@@ -1379,7 +1379,7 @@ class IStrategy(ABC, HyperStrategyMixin):
and not any([exit_short, enter_long])
):
enter_signal = SignalDirection.SHORT
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
enter_tag = latest.get(SignalTagType.ENTER_TAG, None)
enter_tag = enter_tag if isinstance(enter_tag, str) and enter_tag != "nan" else None
@@ -1871,7 +1871,9 @@ class IStrategy(ABC, HyperStrategyMixin):
if isinstance(annotation, dict):
# Convert to AnnotationType
try:
AnnotationTypeTA.validate_python(annotation)
# "forbid" extra fields to catch user errors
# Can be questioned if this creates many problems
AnnotationTypeTA.validate_python(annotation, extra="forbid")
annotations_new.append(annotation)
except ValidationError as e:
logger.error(f"Invalid annotation data: {annotation}. Error: {e}")

View File

@@ -70,6 +70,10 @@ class BaseParameter(ABC):
def __repr__(self):
return f"{self.__class__.__name__}({self.value})"
@property
def param_type(self) -> str:
return self.__class__.__name__
@abstractmethod
def get_space(self, name: str) -> Union["Integer", "Real", "SKDecimal", "Categorical"]:
"""
@@ -255,8 +259,8 @@ class DecimalParameter(NumericParameter):
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to optuna's NumericParameter.
"""
self._decimals = decimals
default = round(default, self._decimals)
self.decimals = decimals
default = round(default, self.decimals)
super().__init__(
low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs
@@ -268,7 +272,7 @@ class DecimalParameter(NumericParameter):
@value.setter
def value(self, new_value: float):
self._value = round(new_value, self._decimals)
self._value = round(new_value, self.decimals)
def get_space(self, name: str) -> "SKDecimal":
"""
@@ -276,7 +280,7 @@ class DecimalParameter(NumericParameter):
:param name: A name of parameter field.
"""
return SKDecimal(
low=self.low, high=self.high, decimals=self._decimals, name=name, **self._space_params
low=self.low, high=self.high, decimals=self.decimals, name=name, **self._space_params
)
@property
@@ -288,9 +292,9 @@ class DecimalParameter(NumericParameter):
calculating 100ds of indicators.
"""
if self.can_optimize():
low = int(self.low * pow(10, self._decimals))
high = int(self.high * pow(10, self._decimals)) + 1
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
low = int(self.low * pow(10, self.decimals))
high = int(self.high * pow(10, self.decimals)) + 1
return [round(n * pow(0.1, self.decimals), self.decimals) for n in range(low, high)]
else:
return [self.value]

View File

@@ -182,5 +182,4 @@ def stoploss_from_absolute(
# negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price
# shorts can yield stoploss values higher than 1, so limit that as well
return max(min(stoploss, 1.0), 0.0) * leverage
return max(stoploss, 0.0) * leverage

View File

@@ -113,9 +113,9 @@ class FreqaiExampleHybridStrategy(IStrategy):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
@@ -169,9 +169,9 @@ class FreqaiExampleHybridStrategy(IStrategy):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -201,7 +201,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -219,7 +219,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair

View File

@@ -65,9 +65,9 @@ class FreqaiExampleStrategy(IStrategy):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
@@ -125,9 +125,9 @@ class FreqaiExampleStrategy(IStrategy):
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -161,7 +161,7 @@ class FreqaiExampleStrategy(IStrategy):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
@@ -183,7 +183,7 @@ class FreqaiExampleStrategy(IStrategy):
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
https://www.freqtrade.io/en/stable/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair

View File

@@ -40,7 +40,7 @@ from technical import qtpylib
class {{ strategy }}(IStrategy):
"""
This is a strategy template to get you started.
More information in https://www.freqtrade.io/en/latest/strategy-customization/
More information in https://www.freqtrade.io/en/stable/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies

View File

@@ -40,7 +40,7 @@ from technical import qtpylib
class SampleStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
More information in https://www.freqtrade.io/en/latest/strategy-customization/
More information in https://www.freqtrade.io/en/stable/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies

View File

@@ -5,7 +5,7 @@ def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, this simply does nothing.
:param current_time: datetime object, containing the current datetime
@@ -26,7 +26,7 @@ def custom_entry_price(
"""
Custom entry price logic, returning the new entry price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set entry price
@@ -58,7 +58,7 @@ def adjust_order_price(
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
@@ -91,7 +91,7 @@ def custom_exit_price(
"""
Custom exit price logic, returning the new exit price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set exit price
@@ -183,7 +183,7 @@ def custom_stoploss(
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
@@ -246,7 +246,7 @@ def confirm_trade_entry(
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -282,7 +282,7 @@ def confirm_trade_exit(
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
@@ -314,7 +314,7 @@ def check_entry_timeout(
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
@@ -337,7 +337,7 @@ def check_exit_timeout(
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for
@@ -369,7 +369,7 @@ def adjust_trade_position(
This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
When not implemented by a strategy, returns None

View File

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

View File

@@ -6,7 +6,7 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.13
ruff==0.15.1
mypy==1.19.1
pre-commit==4.5.1
pytest==9.0.2
@@ -21,13 +21,13 @@ isort==7.0.0
time-machine==3.2.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.16.6
nbconvert==7.17.0
# mypy types
scipy-stubs==1.17.0.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.17.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.2.0.20251022
types-filelock==3.2.7
types-requests==2.32.4.20260107
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20251115
types-python-dateutil==2.9.0.20260124
pip-audit==2.10.0

View File

@@ -2,10 +2,10 @@
-r requirements-freqai.txt
# Required for freqai-rl
torch==2.9.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
torch==2.10.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==1.2.3
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1
tqdm==4.67.3

View File

@@ -6,6 +6,6 @@
scikit-learn==1.8.0
joblib==1.5.3
lightgbm==4.6.0
xgboost==3.1.3
xgboost==3.2.0
tensorboard==2.20.0
datasieve==0.1.9

View File

@@ -4,6 +4,6 @@
# Required for hyperopt
scipy==1.17.0
scikit-learn==1.8.0
filelock==3.20.3
optuna==4.6.0
filelock==3.24.2
optuna==4.7.0
cmaes==0.12.0

View File

@@ -1,4 +1,4 @@
numpy==2.4.1
numpy==2.4.2
pandas==2.3.3
bottleneck==1.6.0
numexpr==2.14.1
@@ -7,15 +7,15 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.4
ccxt==4.5.34
cryptography==46.0.3
ccxt==4.5.39
cryptography==46.0.5
aiohttp==3.13.3
SQLAlchemy==2.0.45
python-telegram-bot==22.5
SQLAlchemy==2.0.46
python-telegram-bot==22.6
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.15.0
cachetools==6.2.4
cachetools==7.0.1
requests==2.32.5
urllib3==2.6.3
certifi==2026.1.4
@@ -24,25 +24,25 @@ tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.3
rich==14.2.0
rich==14.3.2
pyarrow==23.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster
python-rapidjson==1.23
# Properly format api responses
orjson==3.11.5
orjson==3.11.7
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.128.0
fastapi==0.129.0
pydantic==2.12.5
uvicorn==0.40.0
pyjwt==2.10.1
pyjwt==2.11.0
aiofiles==25.1.0
psutil==7.2.1
psutil==7.2.2
# Building config files interactively
questionary==2.1.1
@@ -59,4 +59,4 @@ websockets==16.0
janus==2.0.0
ast-comments==1.2.3
packaging==25.0
packaging==26.0

View File

@@ -1,12 +1,29 @@
Clear-Host
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt"
$Global:LogFilePath = Join-Path ([System.IO.Path]::GetTempPath()) "script_log_$Timestamp.txt"
$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt")
$VenvName = ".venv"
$VenvDir = Join-Path $PSScriptRoot $VenvName
# Supported Python minor versions (detection order: prefer newest first)
$SupportedMinorVersions = @(13,12,11)
# Build a human-readable supported versions string like "3.11, 3.12 and 3.13"
$asc = $SupportedMinorVersions | Sort-Object
if ($asc.Count -eq 1) {
$SupportedPythonVersions = "3.$($asc[0])"
}
elseif ($asc.Count -eq 2) {
$SupportedPythonVersions = "3.$($asc[0]) and 3.$($asc[1])"
}
else {
$allButLast = ($asc[0..($asc.Count - 2)] | ForEach-Object { "3.$_" }) -join ", "
$last = "3.$($asc[-1])"
$SupportedPythonVersions = "$allButLast and $last"
}
function Write-Log {
param (
[string]$Message,
@@ -148,20 +165,21 @@ function Test-PythonExecutable {
}
function Find-PythonExecutable {
$PythonExecutables = @(
"python",
"python3.13",
"python3.12",
"python3.11",
"python3",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe"
)
# Build a list of candidate executables dynamically from supported versions
$PythonExecutables = @()
$PythonExecutables += "python"
foreach ($v in $SupportedMinorVersions) {
$PythonExecutables += "python3.$v"
}
$PythonExecutables += "python3"
# Add common Windows installation paths for each supported minor version
foreach ($v in $SupportedMinorVersions) {
$PythonExecutables += "C:\\Users\\$env:USERNAME\\AppData\\Local\\Programs\\Python\\Python3$v\\python.exe"
}
foreach ($v in $SupportedMinorVersions) {
$PythonExecutables += "C:\\Python3$v\\python.exe"
}
foreach ($Executable in $PythonExecutables) {
if (Test-PythonExecutable -PythonExecutable $Executable) {
@@ -178,7 +196,7 @@ function Main {
# Exit on lower versions than Python 3.11 or when Python executable not found
$PythonExecutable = Find-PythonExecutable
if ($null -eq $PythonExecutable) {
Write-Log "No suitable Python executable found. Please ensure that Python 3.11 or higher is installed and available in the system PATH." -Level 'ERROR'
Write-Log "No suitable Python executable found. Supported versions are: $SupportedPythonVersions. Please install one of these and ensure it's available in the system PATH." -Level 'ERROR'
Exit 1
}

View File

@@ -7,6 +7,9 @@ function echo_block() {
echo "----------------------------"
}
UV=false
# Supported Python minor versions (order matters for detection)
SUPPORTED_MINOR_VERS=(13 12 11)
SUPPORTED_PY_VERSIONS="3.11, 3.12 and 3.13"
function check_installed_pip() {
${PYTHON} -m pip > /dev/null
@@ -33,7 +36,7 @@ function check_installed_python() {
return
fi
for v in 13 12 11
for v in "${SUPPORTED_MINOR_VERS[@]}"
do
PYTHON="python3.${v}"
which $PYTHON
@@ -45,7 +48,7 @@ function check_installed_python() {
fi
done
echo "No usable python found. Please make sure to have python3.11 or newer installed."
echo "No usable python found. Supported versions are: ${SUPPORTED_PY_VERSIONS}. Please install one of these."
exit 1
}

View File

@@ -604,6 +604,7 @@ def get_default_conf(testdatadir):
"cancel_open_orders_on_exit": False,
"minimal_roi": {"40": 0.0, "30": 0.01, "20": 0.02, "0": 0.04},
"dry_run_wallet": 1000,
"tradable_balance_ratio": 0.99,
"stoploss": -0.10,
"unfilledtimeout": {"entry": 10, "exit": 30},
"entry_pricing": {

View File

@@ -24,36 +24,6 @@ from freqtrade.exceptions import OperationalException
from tests.conftest import log_has, log_has_re
def test_datahandler_ohlcv_get_pairs(testdatadir):
pairs = FeatherDataHandler.ohlcv_get_pairs(testdatadir, "5m", candle_type=CandleType.SPOT)
# Convert to set to avoid failures due to sorting
assert set(pairs) == {
"UNITTEST/BTC",
"XLM/BTC",
"ETH/BTC",
"TRX/BTC",
"LTC/BTC",
"XMR/BTC",
"ZEC/BTC",
"ADA/BTC",
"ETC/BTC",
"NXT/BTC",
"DASH/BTC",
"XRP/ETH",
"BTC/USDT",
"XRP/USDT",
}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "8m", candle_type=CandleType.SPOT)
assert set(pairs) == {"UNITTEST/BTC"}
pairs = FeatherDataHandler.ohlcv_get_pairs(testdatadir, "1h", candle_type=CandleType.MARK)
assert set(pairs) == {"UNITTEST/USDT:USDT", "XRP/USDT:USDT"}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "1h", candle_type=CandleType.FUTURES)
assert set(pairs) == {"XRP/USDT:USDT"}
@pytest.mark.parametrize(
"filename,pair,timeframe,candletype",
[

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