Compare commits

..

15 Commits

Author SHA1 Message Date
Matthias 281ba605da Merge pull request #13106 from freqtrade/new_release
New release 2026.4
2026-04-30 19:49:02 +02:00
Matthias d460eef576 chore: bump version to 2026.4 2026-04-30 07:08:05 +02:00
Matthias 6c2cd96622 Merge branch 'stable' into new_release 2026-04-30 07:07:49 +02:00
Matthias dc2abe32a3 Merge pull request #13003 from freqtrade/new_release
New release 2026.3
2026-03-30 19:11:38 +02:00
Matthias 43bb0f7cb1 chore: bump version to 2026.3 2026-03-30 06:38:56 +02:00
Matthias 9ee87cb6ba Merge branch 'stable' into new_release 2026-03-30 06:38:42 +02:00
Matthias 0e2313be7b Merge pull request #12875 from freqtrade/new_release
New release 2026.2
2026-02-28 12:25:06 +01:00
Matthias f535c4cff4 chore: bump version to 2026.2 2026-02-28 08:02:58 +01:00
Matthias 5f901d837c Merge branch 'stable' into new_release 2026-02-28 08:02:41 +01:00
Matthias c86484b152 Merge pull request #12758 from freqtrade/new_release
New release 2026.1
2026-01-31 13:06:44 +01:00
Matthias a33eb51f36 chore: bump version to 2026.1 2026-01-31 08:23:44 +01:00
Matthias 373cd8141c Merge branch 'stable' into new_release 2026-01-31 08:23:11 +01:00
Matthias 9f00a1d0d2 Merge pull request #12673 from freqtrade/new_release
New release 2025.12
2025-12-30 08:19:19 +01:00
Matthias 9a37d7bfbb chore: bump version to 2025.12 2025-12-29 13:19:31 +01:00
Matthias c9c08906e5 Merge branch 'stable' into new_release 2025-12-29 13:17:12 +01:00
37 changed files with 3932 additions and 7371 deletions
+1 -5
View File
@@ -49,11 +49,7 @@ updates:
patterns: patterns:
- "scipy" - "scipy"
- "scipy-stubs" - "scipy-stubs"
gymnasium:
patterns:
- "gymnasium"
- "stable-baselines3"
- "sb3-contrib"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
cooldown: cooldown:
+2 -5
View File
@@ -68,7 +68,7 @@ jobs:
run: | run: |
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04') if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
with: with:
fail_ci_if_error: true fail_ci_if_error: true
@@ -150,10 +150,7 @@ jobs:
run: | run: |
$PSVersionTable $PSVersionTable
Get-PSRepository | Format-List * Get-PSRepository | Format-List *
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { Set-PSRepository psgallery -InstallationPolicy trusted
Register-PSRepository -Default
}
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
$Error.clear() $Error.clear()
Invoke-Pester -Path "tests" -CI Invoke-Pester -Path "tests" -CI
@@ -1,53 +0,0 @@
name: Pre-commit Types update
on:
pull_request:
branches:
- "develop"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
cancel-in-progress: true
permissions: {}
jobs:
mypy-version-update:
name: "Pre-commit mypy type versions update"
runs-on: ubuntu-24.04
# Only run this job for pull requests created by dependabot[bot]
if: >
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.repository == github.event.pull_request.head.repo.full_name &&
github.event_name == 'pull_request' &&
startsWith(github.head_ref, 'dependabot/')
environment:
name: dependabot-pulls
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true
token: ${{ secrets.REPO_SCOPED_TOKEN_DEP }}
ref: ${{ github.head_ref || github.ref }}
- name: Install uv and Python 🐍
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
activate-environment: true
python-version: "3.13"
- name: Install PyYAML
run: |
- name: pre-commit dependencies
run: |
uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
python build_helpers/pre_commit_update.py --update
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7
with:
commit_message: "chore(deps): Apply pre-commit types update"
commit_user_name: Freqtrade Bot
commit_user_email: 154552126+freqtrade-bot@users.noreply.github.com
commit_author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
+1 -1
View File
@@ -2,7 +2,7 @@ name: Pre-commit auto-update
on: on:
schedule: schedule:
- cron: "13 1 * * 2" - cron: "0 3 * * 2"
# on demand # on demand
workflow_dispatch: workflow_dispatch:
+1 -1
View File
@@ -31,4 +31,4 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run zizmor 🌈 - name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+6 -6
View File
@@ -15,23 +15,23 @@ repos:
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v2.1.0" rev: "v1.20.2"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==7.0.0.20260518 - types-cachetools==6.2.0.20260408
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.33.0.20260518 - types-requests==2.33.0.20260408
- types-tabulate==0.10.0.20260508 - types-tabulate==0.10.0.20260408
- types-python-dateutil==2.9.0.20260518 - types-python-dateutil==2.9.0.20260408
- scipy-stubs==1.17.1.4 - scipy-stubs==1.17.1.4
- SQLAlchemy==2.0.49 - SQLAlchemy==2.0.49
# stages: [push] # stages: [push]
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.15.13' rev: 'v0.15.12'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.14.5-slim-trixie AS base FROM python:3.14.3-slim-trixie AS base
# Setup env # Setup env
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
+2 -42
View File
@@ -1,7 +1,5 @@
# File used in CI to ensure pre-commit dependencies are kept up-to-date. # File used in CI to ensure pre-commit dependencies are kept up-to-date.
import argparse
import re
import sys import sys
from pathlib import Path from pathlib import Path
@@ -12,24 +10,6 @@ pre_commit_file = Path(".pre-commit-config.yaml")
require_dev = Path("requirements-dev.txt") require_dev = Path("requirements-dev.txt")
require = Path("requirements.txt") require = Path("requirements.txt")
parser = argparse.ArgumentParser()
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
def replace_dependency_version(pre_commit_text: str, dependency: str) -> tuple[str, bool]:
"""
Regex-based replacement of a dependency version in the pre-commit config file.
using regex here ensures we only replace the version of the dependency while
keeping the overall file intact.
"""
package_name = dependency.split("==", 1)[0]
pattern = re.compile(rf"^(\s*-\s+){re.escape(package_name)}==.*$", re.MULTILINE)
updated_text, replacements = pattern.subn(rf"\1{dependency}", pre_commit_text, count=1)
return updated_text, replacements > 0 and updated_text != pre_commit_text
with require_dev.open("r") as rfile: with require_dev.open("r") as rfile:
requirements = rfile.readlines() requirements = rfile.readlines()
@@ -43,18 +23,6 @@ supported = ("types-", "SQLAlchemy", "scipy-stubs")
# Only keep the first part of the line up to the first space # Only keep the first part of the line up to the first space
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)] type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
with pre_commit_file.open("r") as file:
pre_commit_text = file.read()
updated = False
for req in type_reqs:
pre_commit_text, req_updated = replace_dependency_version(pre_commit_text, req)
updated = updated or req_updated
if args.update and updated:
with pre_commit_file.open("w") as file:
file.write(pre_commit_text)
with pre_commit_file.open("r") as file: with pre_commit_file.open("r") as file:
f = yaml.load(file, Loader=yaml.SafeLoader) f = yaml.load(file, Loader=yaml.SafeLoader)
@@ -72,20 +40,12 @@ for hook in hooks:
for req in type_reqs: for req in type_reqs:
if req not in hooks: if req not in hooks:
errors.append(f"{req} is missing in pre-commit config file.") errors.append(f"{req} is missing in pre-config file.")
if updated:
if args.update:
errors.append(".pre-commit-config.yaml was updated to match the requirements files.")
else:
errors.append(
".pre-commit-config.yaml is outdated. Run build_helpers/pre_commit_update.py --update."
)
if errors: if errors:
for e in errors: for e in errors:
print(e) print(e)
sys.exit(1 if not (args.update and updated) else 0) sys.exit(1)
sys.exit(0) sys.exit(0)
+1 -2
View File
@@ -1063,8 +1063,7 @@
"jwt_secret_key": { "jwt_secret_key": {
"description": "Secret key for JWT authentication.", "description": "Secret key for JWT authentication.",
"type": "string", "type": "string",
"default": "somethingRandomSomethingRandom123", "default": "somethingRandomSomethingRandom123"
"minLength": 32
}, },
"CORS_origins": { "CORS_origins": {
"description": "List of allowed CORS origins.", "description": "List of allowed CORS origins.",
+1 -1
View File
@@ -166,7 +166,7 @@ Below are the values you can expect to include/use inside a typical strategy dat
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2. | `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float. | `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%` (see details below). <br> **Datatype:** Depends on the feature created by the user. | `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%` (see details below). <br> **Datatype:** Depends on the feature created by the user.
| `df['%%*']` | Any dataframe column prepended with `%%` in `feature_engineering_*()` is treated as a training feature, just the same as the above `%` prepend. However, in this case, the features are returned back to the strategy for FreqUI/plot-dataframe plotting and monitoring in Dry/Live/Backtesting <br> **Datatype:** Depends on the feature created by the user. <br>*Please note* that features created in `feature_engineering_expand()` will have automatic FreqAI naming schemas depending on the expansions that you configured (i.e. `include_timeframes`, `include_corr_pairlist`, `indicators_periods_candles`, `include_shifted_candles`). So if you want to plot `%%-rsi` from `feature_engineering_expand_all()`, the final naming scheme for your plotting config would be: `%%-rsi-period_10_ETH/USDT:USDT_1h` for the `rsi` feature with `period=10`, `timeframe=1h`, and `pair=ETH/USDT:USDT` (the `:USDT` is added if you are using futures pairs). It is useful to simply add `print(dataframe.columns)` in your `populate_indicators()` after `self.freqai.start()` to see the full list of available features that are returned to the strategy for plotting purposes. | `df['%%*']` | Any dataframe column prepended with `%%` in `feature_engineering_*()` is treated as a training feature, just the same as the above `%` prepend. However, in this case, the features are returned back to the strategy for FreqUI/plot-dataframe plotting and monitoring in Dry/Live/Backtesting <br> **Datatype:** Depends on the feature created by the user. Please note that features created in `feature_engineering_expand()` will have automatic FreqAI naming schemas depending on the expansions that you configured (i.e. `include_timeframes`, `include_corr_pairlist`, `indicators_periods_candles`, `include_shifted_candles`). So if you want to plot `%%-rsi` from `feature_engineering_expand_all()`, the final naming scheme for your plotting config would be: `%%-rsi-period_10_ETH/USDT:USDT_1h` for the `rsi` feature with `period=10`, `timeframe=1h`, and `pair=ETH/USDT:USDT` (the `:USDT` is added if you are using futures pairs). It is useful to simply add `print(dataframe.columns)` in your `populate_indicators()` after `self.freqai.start()` to see the full list of available features that are returned to the strategy for plotting purposes.
## Setting the `startup_candle_count` ## Setting the `startup_candle_count`
+1 -1
View File
@@ -2,6 +2,6 @@ markdown==3.10.2
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.7.6 mkdocs-material==9.7.6
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.21.3 pymdown-extensions==10.21.2
jinja2==3.1.6 jinja2==3.1.6
mike==2.2.0 mike==2.2.0
+9 -9
View File
@@ -202,20 +202,20 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
| `/blacklist` | GET | Show the current blacklist. | `/blacklist` | GET | Show the current blacklist.
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `blacklist` (`str`) | `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `blacklist` (`str`)
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`) | `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. | `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return.<br/>*Params:*<br/>- `<column_list>` (`list[str]`) | `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. | `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
| `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return.<br/>*Params:*<br/>- `<column_list>` (`list[str]`) | `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
| `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured). | `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured). **Alpha**
| `/strategies` | GET | List strategies in strategy directory. | `/strategies` | GET | List strategies in strategy directory. **Alpha**
| `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name.<br/>*Params:*<br/>- `<strategy>` (`str`) | `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name. **Alpha**<br/>*Params:*<br/>- `<strategy>` (`str`)
| `/available_pairs` | GET | List available backtest data. | `/available_pairs` | GET | List available backtest data. **Alpha**
| `/version` | GET | Show version. | `/version` | GET | Show version.
| `/sysinfo` | GET | Show information about the system load. | `/sysinfo` | GET | Show information about the system load.
| `/health` | GET | Show bot health (last bot loop). | `/health` | GET | Show bot health (last bot loop).
!!! Warning "Alpha status" !!! Warning "Alpha status"
Endpoints labeled with *Alpha status* or *Beta status* above may change at any time without notice. Endpoints labeled with *Alpha status* above may change at any time without notice.
### Message WebSocket ### Message WebSocket
-2
View File
@@ -910,8 +910,6 @@ if self.dp.runmode.value in ('live', 'dry_run'):
### *check_delisting(pair)* ### *check_delisting(pair)*
Return Datetime of the pair delisting schedule if any, otherwise return None
```python ```python
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs):
if self.dp.runmode.value in ('live', 'dry_run'): if self.dp.runmode.value in ('live', 'dry_run'):
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2026.5-dev" __version__ = "2026.4"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+1 -1
View File
@@ -393,7 +393,7 @@ def start_show_trades(args: dict[str, Any]) -> None:
tfilter = [] tfilter = []
if config.get("trade_ids"): if config.get("trade_ids"):
tfilter.append(Trade.id.in_(int(tid) for tid in config["trade_ids"])) tfilter.append(Trade.id.in_(config["trade_ids"]))
trades = Trade.get_trades(tfilter).all() trades = Trade.get_trades(tfilter).all()
logger.info(f"Printing {len(trades)} Trades: ") logger.info(f"Printing {len(trades)} Trades: ")
-1
View File
@@ -757,7 +757,6 @@ CONF_SCHEMA = {
"description": "Secret key for JWT authentication.", "description": "Secret key for JWT authentication.",
"type": "string", "type": "string",
"default": "somethingRandomSomethingRandom123", "default": "somethingRandomSomethingRandom123",
"minLength": 32,
}, },
"CORS_origins": { "CORS_origins": {
"description": "List of allowed CORS origins.", "description": "List of allowed CORS origins.",
+1 -1
View File
@@ -559,7 +559,7 @@ class Binance(Exchange):
class Binanceusdm(Binance): class Binanceusdm(Binance):
"""Binance USDM Exchange """Binacne USDM Exchange
Same as Binance - only futures trading is supported (via ccxt). Same as Binance - only futures trading is supported (via ccxt).
Not actually necessary, binance should be preferred. Not actually necessary, binance should be preferred.
File diff suppressed because it is too large Load Diff
-6
View File
@@ -214,12 +214,6 @@ def dataframe_to_json(dataframe: pd.DataFrame) -> str:
:param dataframe: A pandas DataFrame :param dataframe: A pandas DataFrame
:returns: A JSON string of the pandas DataFrame :returns: A JSON string of the pandas DataFrame
""" """
date_columns = dataframe.select_dtypes(include=["datetime", "datetime64", "datetimetz"])
# Explicit conversion to ms
# This used to be part of to_json, but was deprecated in pandas 3
for date_column in date_columns:
dataframe[date_column] = date_columns[date_column].dt.as_unit("ms").astype("int64")
return dataframe.to_json(orient="split") return dataframe.to_json(orient="split")
+11 -14
View File
@@ -189,8 +189,8 @@ class Order(ModelBase):
def __repr__(self): def __repr__(self):
return ( return (
f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
f"side={self.side or self.ft_order_side}, filled={self.safe_filled}, " f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
f"price={self.safe_price}, amount={self.amount}, " f"amount={self.amount}, "
f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})" f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})"
) )
@@ -1248,16 +1248,12 @@ class LocalTrade:
close_profit_abs = 0.0 close_profit_abs = 0.0
# Reset funding fees # Reset funding fees
self.funding_fees = 0.0 self.funding_fees = 0.0
# Total funding fees - cumulated over all orders funding_fees = 0.0
total_funding_fees = 0.0 ordercount = len(self.orders) - 1
# current funding fees - resetting on every exit to be aligned with profit calculation,
# as funding fees are part of the profit
current_funding_fee = 0.0
for i, o in enumerate(self.orders): for i, o in enumerate(self.orders):
if o.ft_is_open or not o.filled: if o.ft_is_open or not o.filled:
continue continue
current_funding_fee += o.funding_fee or 0.0 funding_fees += o.funding_fee or 0.0
total_funding_fees += o.funding_fee or 0.0
tmp_amount = FtPrecise(o.safe_amount_after_fee) tmp_amount = FtPrecise(o.safe_amount_after_fee)
tmp_price = FtPrecise(o.safe_price) tmp_price = FtPrecise(o.safe_price)
@@ -1272,8 +1268,11 @@ class LocalTrade:
avg_price = current_stake / current_amount avg_price = current_stake / current_amount
if is_exit: if is_exit:
# Intermediate funding fees for profit calculation # Process exits
self.funding_fees = current_funding_fee if i == ordercount and is_closing:
# Apply funding fees only to the last closing order
self.funding_fees = funding_fees
exit_rate = o.safe_price exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee exit_amount = o.safe_amount_after_fee
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price)) prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
@@ -1282,12 +1281,10 @@ class LocalTrade:
# This needs to be calculated based on the last occurring exit to be aligned # This needs to be calculated based on the last occurring exit to be aligned
# with realized_profit. # with realized_profit.
close_profit = (close_profit_abs / total_stake) * self.leverage close_profit = (close_profit_abs / total_stake) * self.leverage
current_funding_fee = 0.0
else: else:
total_stake += self._calc_open_trade_value(tmp_amount, price) total_stake += self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += tmp_amount * price max_stake_amount += tmp_amount * price
# Assign cumulated funding fees after all orders have been processed self.funding_fees = funding_fees
self.funding_fees = total_funding_fees
self.max_stake_amount = float(max_stake_amount) / (self.leverage or 1.0) self.max_stake_amount = float(max_stake_amount) / (self.leverage or 1.0)
if close_profit: if close_profit:
@@ -38,7 +38,7 @@ class MarketCapPairList(IPairList):
self._max_rank = self._pairlistconfig.get("max_rank", 30) self._max_rank = self._pairlistconfig.get("max_rank", 30)
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._categories = self._pairlistconfig.get("categories", []) self._categories = self._pairlistconfig.get("categories", [])
self._marketcap_cache: FtTTLCache = FtTTLCache(maxsize=2, ttl=self._refresh_period) self._marketcap_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
_coingecko_config = self._config.get("coingecko", {}) _coingecko_config = self._config.get("coingecko", {})
+1 -1
View File
@@ -51,7 +51,7 @@ class StrategyResolver(IResolver):
strategy: IStrategy = StrategyResolver._load_strategy( strategy: IStrategy = StrategyResolver._load_strategy(
strategy_name, config=config, extra_dir=config.get("strategy_path") strategy_name, config=config, extra_dir=config.get("strategy_path")
) )
strategy.ft_set_special_params_from_file() strategy.ft_load_params_from_file()
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
# (Attribute name, default, subkey) # (Attribute name, default, subkey)
+1
View File
@@ -15,6 +15,7 @@ from freqtrade.rpc.api_server.deps import get_api_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ALGORITHM = "HS256" ALGORITHM = "HS256"
__DEFAULT_JWT = "somethingRandomSomethingRandom123"
router_login = APIRouter() router_login = APIRouter()
+4 -6
View File
@@ -796,7 +796,7 @@ class RPC:
results = results.rename({"timestamp": "date"}, axis=1) results = results.rename({"timestamp": "date"}, axis=1)
results.loc[:, "__date_ts"] = results.loc[:, "date"].dt.as_unit("ms").astype("int64") results.loc[:, "__date_ts"] = results.loc[:, "date"].dt.as_unit("ms").astype("int64")
# Exclude non-bot managed for now # Exclude non-bot managed for now
results_filtered = results.loc[results["bot_managed"].astype(bool)] results_filtered = results.loc[results["bot_managed"]]
results_final = ( results_final = (
results_filtered.groupby(["date", "__date_ts"]) results_filtered.groupby(["date", "__date_ts"])
@@ -1548,7 +1548,8 @@ class RPC:
# band-aid until this is fixed: # band-aid until this is fixed:
# https://github.com/pandas-dev/pandas/issues/45836 # https://github.com/pandas-dev/pandas/issues/45836
date_columns = dataframe.select_dtypes(include=["datetime", "datetime64", "datetimetz"]) datetime_types = ["datetime", "datetime64", "datetimetz"]
date_columns = dataframe.select_dtypes(include=datetime_types)
for date_column in date_columns: for date_column in date_columns:
# replace NaT with `None` # replace NaT with `None`
dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None}) dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
@@ -1694,11 +1695,8 @@ class RPC:
else dt_ts(dt_now() - timedelta(days=30)), else dt_ts(dt_now() - timedelta(days=30)),
is_new_pair=True, # history is never available - so always treat as new pair is_new_pair=True, # history is never available - so always treat as new pair
candle_type=config.get("candle_type_def", CandleType.SPOT), candle_type=config.get("candle_type_def", CandleType.SPOT),
until_ms=timerange_parsed.stopts * 1000 if timerange_parsed.stopts else None, until_ms=timerange_parsed.stopts,
) )
if timerange_parsed.stopts and len(data) > 1:
# trim last candle if it is newer than the stop time
data = data.loc[data["date"] <= timerange_parsed.stopdt]
else: else:
_data = load_data( _data = load_data(
datadir=config["datadir"], datadir=config["datadir"],
+4 -8
View File
@@ -51,10 +51,9 @@ class HyperStrategyMixin:
for par in self._ft_hyper_params[space].values(): for par in self._ft_hyper_params[space].values():
yield par.name, par yield par.name, par
def ft_set_special_params_from_file(self) -> None: def ft_load_params_from_file(self) -> None:
""" """
Sets special parameters (stoploss, roi, trailing, max_open_trades) from the Load Parameters from parameter file
previously loaded file.
Should/must run before config values are loaded in strategy_resolver. Should/must run before config values are loaded in strategy_resolver.
""" """
if self._ft_params_from_file: if self._ft_params_from_file:
@@ -97,7 +96,7 @@ class HyperStrategyMixin:
params_values = deep_merge_dicts( params_values = deep_merge_dicts(
self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {}) self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {})
) )
self._ft_set_param(self._ft_hyper_params[space], params_values, space, hyperopt) self._ft_load_params(self._ft_hyper_params[space], params_values, space, hyperopt)
def load_params_from_file(self) -> dict: def load_params_from_file(self) -> dict:
filename_str = getattr(self, "__file__", "") filename_str = getattr(self, "__file__", "")
@@ -119,15 +118,12 @@ class HyperStrategyMixin:
return {} return {}
def _ft_set_param( def _ft_load_params(
self, params: SpaceParams, param_values: dict, space: str, hyperopt: bool = False self, params: SpaceParams, param_values: dict, space: str, hyperopt: bool = False
) -> None: ) -> None:
""" """
Set optimizable parameter values. Set optimizable parameter values.
:param params: Dictionary with new parameter values. :param params: Dictionary with new parameter values.
:param param_values: Dictionary with values to set.
:param space: The space to which the parameters belong.
:param hyperopt: Flag indicating if we are in hyperopt mode.
""" """
if not param_values: if not param_values:
logger.info(f"No params for {space} found, using default values.") logger.info(f"No params for {space} found, using default values.")
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2026.5-dev" __version__ = "2026.4"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+1 -1
View File
@@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.34.2 requests==2.33.1
python-rapidjson==1.23 python-rapidjson==1.23
-1
View File
@@ -222,7 +222,6 @@ exclude-newer = "1 week"
[tool.uv.exclude-newer-package] [tool.uv.exclude-newer-package]
ccxt = false ccxt = false
pymdown-extensions = "6 days"
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
+8 -8
View File
@@ -6,9 +6,9 @@
-r requirements-freqai-rl.txt -r requirements-freqai-rl.txt
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
ruff==0.15.12 ruff==0.15.11
mypy==2.1.0 mypy==1.20.1
pre-commit==4.6.0 pre-commit==4.5.1
pytest==9.0.3 pytest==9.0.3
pytest-asyncio==1.3.0 pytest-asyncio==1.3.0
pytest-cov==7.1.0 pytest-cov==7.1.0
@@ -24,13 +24,13 @@ nbconvert==7.17.1
# mypy types # mypy types
scipy-stubs==1.17.1.4 # keep in sync with `scipy` in `requirements-hyperopt.txt` scipy-stubs==1.17.1.4 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==7.0.0.20260518 types-cachetools==6.2.0.20260408
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.33.0.20260518 types-requests==2.33.0.20260408
types-tabulate==0.10.0.20260508 types-tabulate==0.10.0.20260408
types-python-dateutil==2.9.0.20260518 types-python-dateutil==2.9.0.20260408
pip-audit==2.10.0 pip-audit==2.10.0
# For build step in CI # For build step in CI
build==1.5.0 build==1.4.3
# For pre-commit-update check # For pre-commit-update check
pyyaml==6.0.3 pyyaml==6.0.3
+2 -2
View File
@@ -5,7 +5,7 @@
torch==2.11.0; sys_platform != 'darwin' or platform_machine != 'x86_64' torch==2.11.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==1.2.3 gymnasium==1.2.3
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos # SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable-baselines3==2.8.0; sys_platform != 'darwin' or platform_machine != 'x86_64' stable_baselines3==2.8.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3-contrib==2.8.0; sys_platform != 'darwin' or platform_machine != 'x86_64' sb3_contrib==2.8.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
# Progress bar for stable-baselines3 and sb3-contrib # Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.3 tqdm==4.67.3
+15 -15
View File
@@ -1,5 +1,5 @@
numpy==2.4.5 numpy==2.4.4
pandas==3.0.3 pandas==3.0.2
bottleneck==1.6.0 bottleneck==1.6.0
numexpr==2.14.1 numexpr==2.14.1
# Indicator libraries # Indicator libraries
@@ -7,39 +7,39 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8 ta-lib==0.6.8
technical==1.6.0 technical==1.6.0
ccxt==4.5.54 ccxt==4.5.50
cryptography==48.0.0 cryptography==46.0.7
aiohttp==3.13.5 aiohttp==3.13.5
SQLAlchemy==2.0.49 SQLAlchemy==2.0.49
python-telegram-bot==22.7 python-telegram-bot==22.7
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
humanize==4.15.0 humanize==4.15.0
cachetools==7.1.2 cachetools==7.0.5
requests==2.34.2 requests==2.33.1
urllib3==2.7.0 urllib3==2.6.3
certifi==2026.4.22 certifi==2026.2.25
jsonschema==4.26.0 jsonschema==4.26.0
tabulate==0.10.0 tabulate==0.10.0
pycoingecko==3.2.0 pycoingecko==3.2.0
jinja2==3.1.6 jinja2==3.1.6
joblib==1.5.3 joblib==1.5.3
rich==15.0.0 rich==15.0.0
pyarrow==24.0.0; platform_machine != 'armv7l' pyarrow==23.0.1; platform_machine != 'armv7l'
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.23 python-rapidjson==1.23
# Properly format api responses # Properly format api responses
orjson==3.11.9 orjson==3.11.8
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.136.1 fastapi==0.136.0
pydantic==2.13.4 pydantic==2.13.2
uvicorn==0.47.0 uvicorn==0.44.0
pyjwt==2.12.1 pyjwt==2.12.1
aiofiles==25.1.0 aiofiles==25.1.0
psutil==7.2.2 psutil==7.2.2
@@ -49,7 +49,7 @@ questionary==2.1.1
prompt-toolkit==3.0.52 prompt-toolkit==3.0.52
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytz==2026.2 pytz==2026.1.post1
#Futures #Futures
schedule==1.2.2 schedule==1.2.2
@@ -59,4 +59,4 @@ websockets==16.0
janus==2.0.0 janus==2.0.0
ast-comments==1.3.0 ast-comments==1.3.0
packaging==26.2 packaging==26.1
+1 -1
View File
@@ -190,7 +190,7 @@ class ClientProtocol:
self.logger.info("Empty DataFrame") self.logger.info("Empty DataFrame")
async def _handle_default(self, name, msgtype, data): async def _handle_default(self, name, msgtype, data):
self.logger.info(f"Unknown message of type {msgtype} received...") self.logger.info("Unknown message of type {msgtype} received...")
self.logger.info(data) self.logger.info(data)
+2 -4
View File
@@ -163,11 +163,9 @@ class TestCCXTExchange:
pytest.skip(f"No sample Balances available for exchange {exchangename}") pytest.skip(f"No sample Balances available for exchange {exchangename}")
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE): def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename, exchange_params = exchange exch, _, exchange_params = exchange
pair = exchange_params["pair"] pair = exchange_params["pair"]
if exchangename == "okx":
# TODO: re-enable test once ccxt has this fixed.
pytest.skip("OKX fetch_tickers is currently broken, skipping test")
tickers = exch.get_tickers() tickers = exch.get_tickers()
assert pair in tickers assert pair in tickers
assert "ask" in tickers[pair] assert "ask" in tickers[pair]
-38
View File
@@ -1471,44 +1471,6 @@ def test_api_historic_balance(botclient, mocker, ticker, fee, markets, is_short)
assert "total_quote" in resp1["columns"] assert "total_quote" in resp1["columns"]
def test_api_historic_balance_int_bot_managed(botclient, mocker):
"""
read_sql may return the wallet_history `bot_managed` column as an
integer (e.g. MySQL/MariaDB TINYINT)
"""
_, client = botclient
# Single row: with an int64 `bot_managed`
one_row = pd.DataFrame(
{
"timestamp": pd.to_datetime(["2024-01-01"]),
"total_quote": [100.0],
"bot_managed": [1],
}
).astype({"bot_managed": "int64"})
mocker.patch("freqtrade.rpc.rpc.read_sql", return_value=one_row)
rc = client_get(client, f"{BASE_URI}/historic_balance")
assert_response(rc, 200)
assert rc.json()["length"] == 1
assert rc.json()["data"][0][0] == "2024-01-01T00:00:00"
assert rc.json()["data"][0][2] == 100.0
# Mixed rows: the non-bot-managed row (bot_managed=0) must be excluded.
two_rows = pd.DataFrame(
{
"timestamp": pd.to_datetime(["2024-01-01", "2024-01-02"]),
"total_quote": [100.0, 200.0],
"bot_managed": [0, 1],
}
).astype({"bot_managed": "int64"})
mocker.patch("freqtrade.rpc.rpc.read_sql", return_value=two_rows)
rc = client_get(client, f"{BASE_URI}/historic_balance")
assert_response(rc, 200)
assert rc.json()["length"] == 1
assert rc.json()["data"][0][0] == "2024-01-02T00:00:00"
assert rc.json()["data"][0][2] == 200.0
def test_api_performance(botclient, fee): def test_api_performance(botclient, fee):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
+1 -1
View File
@@ -531,7 +531,7 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
assert "exit_long" in exitdf assert "exit_long" in exitdf
def test_strategy_ft_set_special_params_from_file(mocker, default_conf): def test_strategy_ft_load_params_from_file(mocker, default_conf):
default_conf.update({"strategy": "StrategyTestV2"}) default_conf.update({"strategy": "StrategyTestV2"})
del default_conf["max_open_trades"] del default_conf["max_open_trades"]
mocker.patch( mocker.patch(
+2 -2
View File
@@ -230,7 +230,7 @@ def test_deep_merge_dicts():
def test_dataframe_json(ohlcv_history): def test_dataframe_json(ohlcv_history):
from pandas.testing import assert_frame_equal from pandas.testing import assert_frame_equal
json = dataframe_to_json(ohlcv_history.copy()) json = dataframe_to_json(ohlcv_history)
dataframe = json_to_dataframe(json) dataframe = json_to_dataframe(json)
assert list(ohlcv_history.columns) == list(dataframe.columns) assert list(ohlcv_history.columns) == list(dataframe.columns)
@@ -238,6 +238,6 @@ def test_dataframe_json(ohlcv_history):
assert_frame_equal(ohlcv_history, dataframe) assert_frame_equal(ohlcv_history, dataframe)
ohlcv_history.at[1, "date"] = pd.NaT ohlcv_history.at[1, "date"] = pd.NaT
json = dataframe_to_json(ohlcv_history.copy()) json = dataframe_to_json(ohlcv_history)
dataframe = json_to_dataframe(json) dataframe = json_to_dataframe(json)