mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-03-02 08:12:05 +00:00
Merge pull request #12875 from freqtrade/new_release
New release 2026.2
This commit is contained in:
17
.github/workflows/binance-lev-tier-update.yml
vendored
17
.github/workflows/binance-lev-tier-update.yml
vendored
@@ -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
|
||||
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
2
.github/workflows/devcontainer-build.yml
vendored
2
.github/workflows/devcontainer-build.yml
vendored
@@ -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 }}
|
||||
|
||||
6
.github/workflows/docker-build.yml
vendored
6
.github/workflows/docker-build.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/pre-commit-update.yml
vendored
4
.github/workflows/pre-commit-update.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor_action.yml
vendored
2
.github/workflows/zizmor_action.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||
[](https://www.freqtrade.io)
|
||||
[](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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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"],
|
||||
//...
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||
[](https://www.freqtrade.io)
|
||||
[](https://discord.gg/p7nuUNVfP7)
|
||||
|
||||
<!-- GitHub action buttons -->
|
||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2026.1"
|
||||
__version__ = "2026.2"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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"] = {}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class OrderTypeValues(str, Enum):
|
||||
class OrderTypeValues(StrEnum):
|
||||
limit = "limit"
|
||||
market = "market"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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()}"
|
||||
|
||||
@@ -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()}"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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://":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
48
setup.ps1
48
setup.ps1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
7
setup.sh
7
setup.sh
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user