mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 20:23:57 +00:00
Merge branch 'develop' into pr/Axel-CH/10062
This commit is contained in:
commit
7e2d55743e
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -72,7 +72,7 @@ jobs:
|
||||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||||
env:
|
env:
|
||||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||||
|
@ -139,10 +139,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "macos-12", "macos-13", "macos-14" ]
|
os: [ "macos-12", "macos-13", "macos-14" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
exclude:
|
|
||||||
- os: "macos-14"
|
|
||||||
python-version: "3.9"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -263,7 +260,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
|
@ -31,7 +31,7 @@ repos:
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.6.7'
|
rev: 'v0.6.8'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
|
@ -61,7 +61,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.9+**: For botting on any operating system - Windows, macOS and Linux.
|
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without paying money.
|
- [x] **Dry-run**: Run the bot without paying money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
|
@ -218,7 +218,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -579,57 +579,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protections": {
|
|
||||||
"description": "Configuration for various protections.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"description": "Method used for the protection.",
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"CooldownPeriod",
|
|
||||||
"LowProfitPairs",
|
|
||||||
"MaxDrawdown",
|
|
||||||
"StoplossGuard"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"stop_duration": {
|
|
||||||
"description": "Duration to lock the pair after a protection is triggered, in minutes.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0.0
|
|
||||||
},
|
|
||||||
"stop_duration_candles": {
|
|
||||||
"description": "Duration to lock the pair after a protection is triggered, in number of candles.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"unlock_at": {
|
|
||||||
"description": "Time when trading will be unlocked regularly. Format: HH:MM",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"trade_limit": {
|
|
||||||
"description": "Minimum number of trades required during lookback period.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1
|
|
||||||
},
|
|
||||||
"lookback_period": {
|
|
||||||
"description": "Period to look back for protection checks, in minutes.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1
|
|
||||||
},
|
|
||||||
"lookback_period_candles": {
|
|
||||||
"description": "Period to look back for protection checks, in number of candles.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"description": "Telegram settings.",
|
"description": "Telegram settings.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
@ -229,7 +229,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||||
| | **Plugins**
|
| | **Plugins**
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||||
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
|
|
||||||
| | **Telegram**
|
| | **Telegram**
|
||||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
|
|
|
@ -75,7 +75,10 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
|
||||||
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
||||||
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
||||||
|
|
||||||
|
|
||||||
## Removal of `populate_any_indicators`
|
## Removal of `populate_any_indicators`
|
||||||
|
|
||||||
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
|
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
|
||||||
|
|
||||||
|
## Removal of `protections` from configuration
|
||||||
|
|
||||||
|
Setting protections from the configuration via `"protections": [],` has been removed in 2024.10, after having raised deprecation warnings for over 3 years.
|
||||||
|
|
|
@ -241,7 +241,6 @@ No protection should use datetime directly, but use the provided `date_now` vari
|
||||||
|
|
||||||
!!! Tip "Writing a new Protection"
|
!!! Tip "Writing a new Protection"
|
||||||
Best copy one of the existing Protections to have a good example.
|
Best copy one of the existing Protections to have a good example.
|
||||||
Don't forget to register your protection in `constants.py` under the variable `AVAILABLE_PROTECTIONS` - otherwise it will not be selectable.
|
|
||||||
|
|
||||||
#### Implementation of a new protection
|
#### Implementation of a new protection
|
||||||
|
|
||||||
|
|
|
@ -445,7 +445,6 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
||||||
|
|
||||||
Whether you are using `.range` functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
|
Whether you are using `.range` functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
|
||||||
|
|
||||||
|
|
||||||
## Optimizing protections
|
## Optimizing protections
|
||||||
|
|
||||||
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
|
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
|
||||||
|
|
|
@ -360,14 +360,21 @@ The optional `bearer_token` will be included in the requests Authorization Heade
|
||||||
"method": "MarketCapPairList",
|
"method": "MarketCapPairList",
|
||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"max_rank": 50,
|
"max_rank": 50,
|
||||||
"refresh_period": 86400
|
"refresh_period": 86400,
|
||||||
|
"categories": ["layer-1"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
|
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
|
||||||
|
|
||||||
`refresh_period` setting defines the period (in seconds) at which the marketcap rank data will be refreshed. Defaults to 86,400s (1 day). The pairlist cache (`refresh_period`) is applicable on both generating pairlists (first position in the list) and filtering instances (not the first position in the list).
|
The `refresh_period` setting defines the interval (in seconds) at which the marketcap rank data will be refreshed. The default is 86,400 seconds (1 day). The pairlist cache (`refresh_period`) applies to both generating pairlists (when in the first position in the list) and filtering instances (when not in the first position in the list).
|
||||||
|
|
||||||
|
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
|
||||||
|
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
|
||||||
|
|
||||||
|
!!! Warning "Many categories"
|
||||||
|
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
|
||||||
|
|
||||||
#### AgeFilter
|
#### AgeFilter
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,16 @@
|
||||||
## Protections
|
## Protections
|
||||||
|
|
||||||
!!! Warning "Beta feature"
|
|
||||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
|
||||||
|
|
||||||
Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs.
|
Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs.
|
||||||
All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys.
|
All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys.
|
||||||
|
|
||||||
!!! Note
|
!!! Tip "Usage tips"
|
||||||
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
|
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
|
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
|
||||||
|
|
||||||
!!! Note "Backtesting"
|
!!! Note "Backtesting"
|
||||||
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
||||||
|
|
||||||
!!! Warning "Setting protections from the configuration"
|
|
||||||
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
|
|
||||||
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
|
|
||||||
|
|
||||||
### Available Protections
|
### Available Protections
|
||||||
|
|
||||||
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
||||||
|
|
|
@ -85,7 +85,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||||
|
|
||||||
Alternatively
|
Alternatively
|
||||||
|
|
||||||
- Python 3.9+
|
- Python 3.10+
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
|
|
|
@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||||
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.9 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
Python3.10 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||||
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
|
@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
|
@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
||||||
OS Specific steps are listed first, the common section below is necessary for all systems.
|
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.9 or higher and the corresponding pip are assumed to be available.
|
Python3.10 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
|
@ -69,7 +69,7 @@ OS Specific steps are listed first, the common section below is necessary for al
|
||||||
|
|
||||||
=== "RaspberryPi/Raspbian"
|
=== "RaspberryPi/Raspbian"
|
||||||
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
||||||
This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
|
This image comes with python3.11 preinstalled, making it easy to get freqtrade up and running.
|
||||||
|
|
||||||
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
||||||
** --install **
|
** --install **
|
||||||
|
|
||||||
With this option, the script will install the bot and most dependencies:
|
With this option, the script will install the bot and most dependencies:
|
||||||
You will need to have git and python3.9+ installed beforehand for this to work.
|
You will need to have git and python3.10+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* Mandatory software as: `ta-lib`
|
||||||
* Setup your virtualenv under `.venv/`
|
* Setup your virtualenv under `.venv/`
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
markdown==3.7
|
markdown==3.7
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.5.36
|
mkdocs-material==9.5.39
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.10.1
|
pymdown-extensions==10.11.1
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
mike==2.1.3
|
mike==2.1.3
|
||||||
|
|
|
@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
Otherwise, please follow the instructions below.
|
Otherwise, please follow the instructions below.
|
||||||
|
|
||||||
All instructions assume that python 3.9+ is installed and available.
|
All instructions assume that python 3.10+ is installed and available.
|
||||||
|
|
||||||
## Clone the git repository
|
## Clone the git repository
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ cd freqtrade
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10, 3.11 and 3.12) and for 64bit Windows.
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.10, 3.11 and 3.12) and for 64bit Windows.
|
||||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||||
|
|
||||||
Other versions must be downloaded from the above link.
|
Other versions must be downloaded from the above link.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2024.9-dev"
|
__version__ = "2024.10-dev"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
__main__.py for Freqtrade
|
__main__.py for Freqtrade
|
||||||
To launch Freqtrade as a module
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
> python -m freqtrade (with Python >= 3.9)
|
> python -m freqtrade (with Python >= 3.10)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
@ -20,9 +19,6 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if sys.version_info == (3, 8): # pragma: no cover
|
|
||||||
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
strategy_objs = StrategyResolver.search_all_objects(
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
|
|
@ -4,7 +4,6 @@ from typing import Dict
|
||||||
from freqtrade.constants import (
|
from freqtrade.constants import (
|
||||||
AVAILABLE_DATAHANDLERS,
|
AVAILABLE_DATAHANDLERS,
|
||||||
AVAILABLE_PAIRLISTS,
|
AVAILABLE_PAIRLISTS,
|
||||||
AVAILABLE_PROTECTIONS,
|
|
||||||
BACKTEST_BREAKDOWNS,
|
BACKTEST_BREAKDOWNS,
|
||||||
DRY_RUN_WALLET,
|
DRY_RUN_WALLET,
|
||||||
EXPORT_OPTIONS,
|
EXPORT_OPTIONS,
|
||||||
|
@ -449,60 +448,6 @@ CONF_SCHEMA = {
|
||||||
"required": ["method"],
|
"required": ["method"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"protections": {
|
|
||||||
"description": "Configuration for various protections.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"description": "Method used for the protection.",
|
|
||||||
"type": "string",
|
|
||||||
"enum": AVAILABLE_PROTECTIONS,
|
|
||||||
},
|
|
||||||
"stop_duration": {
|
|
||||||
"description": (
|
|
||||||
"Duration to lock the pair after a protection is triggered, "
|
|
||||||
"in minutes."
|
|
||||||
),
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0.0,
|
|
||||||
},
|
|
||||||
"stop_duration_candles": {
|
|
||||||
"description": (
|
|
||||||
"Duration to lock the pair after a protection is triggered, in "
|
|
||||||
"number of candles."
|
|
||||||
),
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
},
|
|
||||||
"unlock_at": {
|
|
||||||
"description": (
|
|
||||||
"Time when trading will be unlocked regularly. Format: HH:MM"
|
|
||||||
),
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"trade_limit": {
|
|
||||||
"description": "Minimum number of trades required during lookback period.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1,
|
|
||||||
},
|
|
||||||
"lookback_period": {
|
|
||||||
"description": "Period to look back for protection checks, in minutes.",
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1,
|
|
||||||
},
|
|
||||||
"lookback_period_candles": {
|
|
||||||
"description": (
|
|
||||||
"Period to look back for protection checks, in number " "of candles."
|
|
||||||
),
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["method"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
# RPC section
|
# RPC section
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"description": "Telegram settings.",
|
"description": "Telegram settings.",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from jsonschema import Draft4Validator, validators
|
from jsonschema import Draft4Validator, validators
|
||||||
|
@ -84,7 +83,6 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
|
||||||
_validate_price_config(conf)
|
_validate_price_config(conf)
|
||||||
_validate_edge(conf)
|
_validate_edge(conf)
|
||||||
_validate_whitelist(conf)
|
_validate_whitelist(conf)
|
||||||
_validate_protections(conf)
|
|
||||||
_validate_unlimited_amount(conf)
|
_validate_unlimited_amount(conf)
|
||||||
_validate_ask_orderbook(conf)
|
_validate_ask_orderbook(conf)
|
||||||
_validate_freqai_hyperopt(conf)
|
_validate_freqai_hyperopt(conf)
|
||||||
|
@ -196,41 +194,6 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||||
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
|
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
|
||||||
|
|
||||||
|
|
||||||
def _validate_protections(conf: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Validate protection configuration validity
|
|
||||||
"""
|
|
||||||
|
|
||||||
for prot in conf.get("protections", []):
|
|
||||||
parsed_unlock_at = None
|
|
||||||
if (config_unlock_at := prot.get("unlock_at")) is not None:
|
|
||||||
try:
|
|
||||||
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
|
|
||||||
except ValueError:
|
|
||||||
raise ConfigurationError(f"Invalid date format for unlock_at: {config_unlock_at}.")
|
|
||||||
|
|
||||||
if "stop_duration" in prot and "stop_duration_candles" in prot:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
|
||||||
f"Please fix the protection {prot.get('method')}."
|
|
||||||
)
|
|
||||||
|
|
||||||
if "lookback_period" in prot and "lookback_period_candles" in prot:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
|
||||||
f"Please fix the protection {prot.get('method')}."
|
|
||||||
)
|
|
||||||
|
|
||||||
if parsed_unlock_at is not None and (
|
|
||||||
"stop_duration" in prot or "stop_duration_candles" in prot
|
|
||||||
):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Protections must specify either `unlock_at`, `stop_duration` or "
|
|
||||||
"`stop_duration_candles`.\n"
|
|
||||||
f"Please fix the protection {prot.get('method')}."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||||
ask_strategy = conf.get("exit_pricing", {})
|
ask_strategy = conf.get("exit_pricing", {})
|
||||||
ob_min = ask_strategy.get("order_book_min")
|
ob_min = ask_strategy.get("order_book_min")
|
||||||
|
|
|
@ -177,4 +177,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
if "protections" in config:
|
if "protections" in config:
|
||||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
raise ConfigurationError(
|
||||||
|
"DEPRECATED: Setting 'protections' in the configuration is deprecated."
|
||||||
|
)
|
||||||
|
|
|
@ -82,6 +82,11 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
||||||
for f in sub_dirs:
|
for f in sub_dirs:
|
||||||
subfolder = folder / f
|
subfolder = folder / f
|
||||||
if not subfolder.is_dir():
|
if not subfolder.is_dir():
|
||||||
|
if subfolder.exists() or subfolder.is_symlink():
|
||||||
|
raise OperationalException(
|
||||||
|
f"File `{subfolder}` exists already and is not a directory. "
|
||||||
|
"Freqtrade requires this to be a directory."
|
||||||
|
)
|
||||||
subfolder.mkdir(parents=False)
|
subfolder.mkdir(parents=False)
|
||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,6 @@ AVAILABLE_PAIRLISTS = [
|
||||||
"SpreadFilter",
|
"SpreadFilter",
|
||||||
"VolatilityFilter",
|
"VolatilityFilter",
|
||||||
]
|
]
|
||||||
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
|
|
||||||
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "hdf5", "feather", "parquet"]
|
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "hdf5", "feather", "parquet"]
|
||||||
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
|
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
|
||||||
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
|
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -238,7 +238,13 @@ class Bybit(Exchange):
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
||||||
|
if self.exchange_has("fetchOrder"):
|
||||||
|
# Set acknowledged to True to avoid ccxt exception
|
||||||
|
params = {"acknowledged": True}
|
||||||
|
|
||||||
order = super().fetch_order(order_id, pair, params)
|
order = super().fetch_order(order_id, pair, params)
|
||||||
|
if not order:
|
||||||
|
order = self.fetch_order_emulated(order_id, pair, {})
|
||||||
if (
|
if (
|
||||||
order.get("status") == "canceled"
|
order.get("status") == "canceled"
|
||||||
and order.get("filled") == 0.0
|
and order.get("filled") == 0.0
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from ccxt import SIGNIFICANT_DIGITS
|
|
||||||
|
|
||||||
from freqtrade.enums import TradingMode
|
from freqtrade.enums import TradingMode
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.exchange_types import FtHas
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
|
@ -36,10 +34,3 @@ class Hyperliquid(Exchange):
|
||||||
config.update({"options": {"defaultType": "spot"}})
|
config.update({"options": {"defaultType": "spot"}})
|
||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@property
|
|
||||||
def precision_mode_price(self) -> int:
|
|
||||||
"""
|
|
||||||
Override the default precision mode for price.
|
|
||||||
"""
|
|
||||||
return SIGNIFICANT_DIGITS
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.exceptions import (
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange import Exchange, date_minus_candles
|
from freqtrade.exchange import Exchange, date_minus_candles
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||||
from freqtrade.exchange.exchange_types import FtHas
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
from freqtrade.misc import safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback2
|
||||||
from freqtrade.util import dt_now, dt_ts
|
from freqtrade.util import dt_now, dt_ts
|
||||||
|
@ -208,6 +208,7 @@ class Okx(Exchange):
|
||||||
order["type"] = "stoploss"
|
order["type"] = "stoploss"
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
@retrier(retries=API_RETRY_COUNT)
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
||||||
if self._config["dry_run"]:
|
if self._config["dry_run"]:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
@ -217,8 +218,20 @@ class Okx(Exchange):
|
||||||
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
||||||
self._log_exchange_response("fetch_stoploss_order", order_reg)
|
self._log_exchange_response("fetch_stoploss_order", order_reg)
|
||||||
return self._convert_stop_order(pair, order_id, order_reg)
|
return self._convert_stop_order(pair, order_id, order_reg)
|
||||||
except ccxt.OrderNotFound:
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||||
pass
|
pass
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
return self._fetch_stop_order_fallback(order_id, pair)
|
||||||
|
|
||||||
|
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> Dict:
|
||||||
params2 = {"stop": True, "ordType": "conditional"}
|
params2 = {"stop": True, "ordType": "conditional"}
|
||||||
for method in (
|
for method in (
|
||||||
self._api.fetch_open_orders,
|
self._api.fetch_open_orders,
|
||||||
|
@ -231,8 +244,16 @@ class Okx(Exchange):
|
||||||
if orders_f:
|
if orders_f:
|
||||||
order = orders_f[0]
|
order = orders_f[0]
|
||||||
return self._convert_stop_order(pair, order_id, order)
|
return self._convert_stop_order(pair, order_id, order)
|
||||||
except ccxt.BaseError:
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||||
pass
|
pass
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||||
|
|
|
@ -86,9 +86,6 @@ class BasePyTorchRegressor(BasePyTorchModel):
|
||||||
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||||
dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
|
dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
|
||||||
|
|
||||||
dd["train_labels"], _, _ = dk.label_pipeline.fit_transform(dd["train_labels"])
|
|
||||||
dd["test_labels"], _, _ = dk.label_pipeline.transform(dd["test_labels"])
|
|
||||||
|
|
||||||
(dd["train_features"], dd["train_labels"], dd["train_weights"]) = (
|
(dd["train_features"], dd["train_labels"], dd["train_weights"]) = (
|
||||||
dk.feature_pipeline.fit_transform(
|
dk.feature_pipeline.fit_transform(
|
||||||
dd["train_features"], dd["train_labels"], dd["train_weights"]
|
dd["train_features"], dd["train_labels"], dd["train_weights"]
|
||||||
|
|
|
@ -141,7 +141,7 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
||||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||||
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||||
|
|
||||||
if self.freqai_info.get("DI_threshold", 0) > 0:
|
if self.ft_params.get("DI_threshold", 0) > 0:
|
||||||
dk.DI_values = dk.feature_pipeline["di"].di_values
|
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||||
else:
|
else:
|
||||||
dk.DI_values = np.zeros(outliers.shape[0])
|
dk.DI_values = np.zeros(outliers.shape[0])
|
||||||
|
|
|
@ -10,8 +10,8 @@ from typing import Any, List, Optional
|
||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
if sys.version_info < (3, 9): # pragma: no cover
|
if sys.version_info < (3, 10): # pragma: no cover
|
||||||
sys.exit("Freqtrade requires Python version >= 3.9")
|
sys.exit("Freqtrade requires Python version >= 3.10")
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
|
|
|
@ -273,10 +273,6 @@ class Backtesting:
|
||||||
|
|
||||||
def _load_protections(self, strategy: IStrategy):
|
def _load_protections(self, strategy: IStrategy):
|
||||||
if self.config.get("enable_protections", False):
|
if self.config.get("enable_protections", False):
|
||||||
conf = self.config
|
|
||||||
if hasattr(strategy, "protections"):
|
|
||||||
conf = deepcopy(conf)
|
|
||||||
conf["protections"] = strategy.protections
|
|
||||||
self.protections = ProtectionManager(self.config, strategy.protections)
|
self.protections = ProtectionManager(self.config, strategy.protections)
|
||||||
|
|
||||||
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
|
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
|
||||||
|
|
|
@ -39,6 +39,11 @@ class __OptionPairlistParameter(__PairlistParameterBase):
|
||||||
options: List[str]
|
options: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class __ListPairListParamenter(__PairlistParameterBase):
|
||||||
|
type: Literal["list"]
|
||||||
|
default: Union[List[str], None]
|
||||||
|
|
||||||
|
|
||||||
class __BoolPairlistParameter(__PairlistParameterBase):
|
class __BoolPairlistParameter(__PairlistParameterBase):
|
||||||
type: Literal["boolean"]
|
type: Literal["boolean"]
|
||||||
default: Union[bool, None]
|
default: Union[bool, None]
|
||||||
|
@ -49,6 +54,7 @@ PairlistParameter = Union[
|
||||||
__StringPairlistParameter,
|
__StringPairlistParameter,
|
||||||
__OptionPairlistParameter,
|
__OptionPairlistParameter,
|
||||||
__BoolPairlistParameter,
|
__BoolPairlistParameter,
|
||||||
|
__ListPairListParamenter,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ class MarketCapPairList(IPairList):
|
||||||
self._number_assets = self._pairlistconfig["number_assets"]
|
self._number_assets = self._pairlistconfig["number_assets"]
|
||||||
self._max_rank = self._pairlistconfig.get("max_rank", 30)
|
self._max_rank = self._pairlistconfig.get("max_rank", 30)
|
||||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
||||||
|
self._categories = self._pairlistconfig.get("categories", [])
|
||||||
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
||||||
self._def_candletype = self._config["candle_type_def"]
|
self._def_candletype = self._config["candle_type_def"]
|
||||||
|
|
||||||
|
@ -45,6 +46,17 @@ class MarketCapPairList(IPairList):
|
||||||
is_demo=_coingecko_config.get("is_demo", True),
|
is_demo=_coingecko_config.get("is_demo", True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._categories:
|
||||||
|
categories = self._coingecko.get_coins_categories_list()
|
||||||
|
category_ids = [cat["category_id"] for cat in categories]
|
||||||
|
|
||||||
|
for category in self._categories:
|
||||||
|
if category not in category_ids:
|
||||||
|
raise OperationalException(
|
||||||
|
f"Category {category} not in coingecko category list. "
|
||||||
|
f"You can choose from {category_ids}"
|
||||||
|
)
|
||||||
|
|
||||||
if self._max_rank > 250:
|
if self._max_rank > 250:
|
||||||
raise OperationalException("This filter only support marketcap rank up to 250.")
|
raise OperationalException("This filter only support marketcap rank up to 250.")
|
||||||
|
|
||||||
|
@ -85,6 +97,15 @@ class MarketCapPairList(IPairList):
|
||||||
"description": "Max rank of assets",
|
"description": "Max rank of assets",
|
||||||
"help": "Maximum rank of assets to use from the pairlist",
|
"help": "Maximum rank of assets to use from the pairlist",
|
||||||
},
|
},
|
||||||
|
"categories": {
|
||||||
|
"type": "list",
|
||||||
|
"default": [],
|
||||||
|
"description": "Coin Categories",
|
||||||
|
"help": (
|
||||||
|
"The Category of the coin e.g layer-1 default [] "
|
||||||
|
"(https://www.coingecko.com/en/categories)"
|
||||||
|
),
|
||||||
|
},
|
||||||
"refresh_period": {
|
"refresh_period": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 86400,
|
"default": 86400,
|
||||||
|
@ -132,15 +153,29 @@ class MarketCapPairList(IPairList):
|
||||||
"""
|
"""
|
||||||
marketcap_list = self._marketcap_cache.get("marketcap")
|
marketcap_list = self._marketcap_cache.get("marketcap")
|
||||||
|
|
||||||
|
default_kwargs = {
|
||||||
|
"vs_currency": "usd",
|
||||||
|
"order": "market_cap_desc",
|
||||||
|
"per_page": "250",
|
||||||
|
"page": "1",
|
||||||
|
"sparkline": "false",
|
||||||
|
"locale": "en",
|
||||||
|
}
|
||||||
|
|
||||||
if marketcap_list is None:
|
if marketcap_list is None:
|
||||||
data = self._coingecko.get_coins_markets(
|
data = []
|
||||||
vs_currency="usd",
|
|
||||||
order="market_cap_desc",
|
if not self._categories:
|
||||||
per_page="250",
|
data = self._coingecko.get_coins_markets(**default_kwargs)
|
||||||
page="1",
|
else:
|
||||||
sparkline="false",
|
for category in self._categories:
|
||||||
locale="en",
|
category_data = self._coingecko.get_coins_markets(
|
||||||
)
|
**default_kwargs, **({"category": category} if category else {})
|
||||||
|
)
|
||||||
|
data += category_data
|
||||||
|
|
||||||
|
data.sort(key=lambda d: float(d.get("market_cap") or 0.0), reverse=True)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
marketcap_list = [row["symbol"] for row in data]
|
marketcap_list = [row["symbol"] for row in data]
|
||||||
self._marketcap_cache["marketcap"] = marketcap_list
|
self._marketcap_cache["marketcap"] = marketcap_list
|
||||||
|
@ -157,7 +192,7 @@ class MarketCapPairList(IPairList):
|
||||||
|
|
||||||
for mc_pair in top_marketcap:
|
for mc_pair in top_marketcap:
|
||||||
test_pair = f"{mc_pair.upper()}/{pair_format}"
|
test_pair = f"{mc_pair.upper()}/{pair_format}"
|
||||||
if test_pair in pairlist:
|
if test_pair in pairlist and test_pair not in filtered_pairlist:
|
||||||
filtered_pairlist.append(test_pair)
|
filtered_pairlist.append(test_pair)
|
||||||
if len(filtered_pairlist) == self._number_assets:
|
if len(filtered_pairlist) == self._number_assets:
|
||||||
break
|
break
|
||||||
|
@ -165,4 +200,5 @@ class MarketCapPairList(IPairList):
|
||||||
if len(filtered_pairlist) > 0:
|
if len(filtered_pairlist) > 0:
|
||||||
return filtered_pairlist
|
return filtered_pairlist
|
||||||
|
|
||||||
return pairlist
|
# If no pairs are found, return the original pairlist
|
||||||
|
return []
|
||||||
|
|
|
@ -4,9 +4,10 @@ Protection manager class
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config, LongShort
|
from freqtrade.constants import Config, LongShort
|
||||||
|
from freqtrade.exceptions import ConfigurationError
|
||||||
from freqtrade.persistence import PairLocks
|
from freqtrade.persistence import PairLocks
|
||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.protections import IProtection
|
from freqtrade.plugins.protections import IProtection
|
||||||
|
@ -21,6 +22,7 @@ class ProtectionManager:
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
self._protection_handlers: List[IProtection] = []
|
self._protection_handlers: List[IProtection] = []
|
||||||
|
self.validate_protections(protections)
|
||||||
for protection_handler_config in protections:
|
for protection_handler_config in protections:
|
||||||
protection_handler = ProtectionResolver.load_protection(
|
protection_handler = ProtectionResolver.load_protection(
|
||||||
protection_handler_config["method"],
|
protection_handler_config["method"],
|
||||||
|
@ -76,3 +78,40 @@ class ProtectionManager:
|
||||||
pair, lock.until, lock.reason, now=now, side=lock.lock_side
|
pair, lock.until, lock.reason, now=now, side=lock.lock_side
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_protections(protections: List[Dict[str, Any]]) -> None:
|
||||||
|
"""
|
||||||
|
Validate protection setup validity
|
||||||
|
"""
|
||||||
|
|
||||||
|
for prot in protections:
|
||||||
|
parsed_unlock_at = None
|
||||||
|
if (config_unlock_at := prot.get("unlock_at")) is not None:
|
||||||
|
try:
|
||||||
|
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
raise ConfigurationError(
|
||||||
|
f"Invalid date format for unlock_at: {config_unlock_at}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if "stop_duration" in prot and "stop_duration_candles" in prot:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
||||||
|
f"Please fix the protection {prot.get('method')}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if "lookback_period" in prot and "lookback_period_candles" in prot:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Protections must specify either `lookback_period` or "
|
||||||
|
f"`lookback_period_candles`.\n Please fix the protection {prot.get('method')}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if parsed_unlock_at is not None and (
|
||||||
|
"stop_duration" in prot or "stop_duration_candles" in prot
|
||||||
|
):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Protections must specify either `unlock_at`, `stop_duration` or "
|
||||||
|
"`stop_duration_candles`.\n"
|
||||||
|
f"Please fix the protection {prot.get('method')}."
|
||||||
|
)
|
||||||
|
|
|
@ -69,7 +69,6 @@ class StrategyResolver(IResolver):
|
||||||
("order_time_in_force", None),
|
("order_time_in_force", None),
|
||||||
("stake_currency", None),
|
("stake_currency", None),
|
||||||
("stake_amount", None),
|
("stake_amount", None),
|
||||||
("protections", None),
|
|
||||||
("startup_candle_count", None),
|
("startup_candle_count", None),
|
||||||
("unfilledtimeout", None),
|
("unfilledtimeout", None),
|
||||||
("use_exit_signal", True),
|
("use_exit_signal", True),
|
||||||
|
|
|
@ -77,7 +77,6 @@ def __run_backtest_bg(btconfig: Config):
|
||||||
|
|
||||||
lastconfig["timerange"] = btconfig["timerange"]
|
lastconfig["timerange"] = btconfig["timerange"]
|
||||||
lastconfig["timeframe"] = strat.timeframe
|
lastconfig["timeframe"] = strat.timeframe
|
||||||
lastconfig["protections"] = btconfig.get("protections", [])
|
|
||||||
lastconfig["enable_protections"] = btconfig.get("enable_protections")
|
lastconfig["enable_protections"] = btconfig.get("enable_protections")
|
||||||
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")
|
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")
|
||||||
|
|
||||||
|
|
|
@ -31,16 +31,6 @@ async def ui_version():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def is_relative_to(path: Path, base: Path) -> bool:
|
|
||||||
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
|
|
||||||
try:
|
|
||||||
path.relative_to(base)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@router_ui.get("/{rest_of_path:path}", include_in_schema=False)
|
@router_ui.get("/{rest_of_path:path}", include_in_schema=False)
|
||||||
async def index_html(rest_of_path: str):
|
async def index_html(rest_of_path: str):
|
||||||
"""
|
"""
|
||||||
|
@ -56,7 +46,7 @@ async def index_html(rest_of_path: str):
|
||||||
if filename.suffix == ".js":
|
if filename.suffix == ".js":
|
||||||
# Force text/javascript for .js files - Circumvent faulty system configuration
|
# Force text/javascript for .js files - Circumvent faulty system configuration
|
||||||
media_type = "application/javascript"
|
media_type = "application/javascript"
|
||||||
if filename.is_file() and is_relative_to(filename, uibase):
|
if filename.is_file() and filename.is_relative_to(uibase):
|
||||||
return FileResponse(str(filename), media_type=media_type)
|
return FileResponse(str(filename), media_type=media_type)
|
||||||
|
|
||||||
index_file = uibase / "index.html"
|
index_file = uibase / "index.html"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from freqtrade_client.ft_rest_client import FtRestClient
|
from freqtrade_client.ft_rest_client import FtRestClient
|
||||||
|
|
||||||
|
|
||||||
__version__ = "2024.9-dev"
|
__version__ = "2024.10-dev"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
@ -13,14 +13,13 @@ authors = [
|
||||||
|
|
||||||
description = "Freqtrade - Client scripts"
|
description = "Freqtrade - Client scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
license = {text = "GPLv3"}
|
license = {text = "GPLv3"}
|
||||||
# license = "GPLv3"
|
# license = "GPLv3"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Science/Research",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
|
|
@ -13,14 +13,12 @@ authors = [
|
||||||
|
|
||||||
description = "Freqtrade - Crypto Trading Bot"
|
description = "Freqtrade - Crypto Trading Bot"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
license = {text = "GPLv3"}
|
license = {text = "GPLv3"}
|
||||||
# license = "GPLv3"
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Science/Research",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
@ -125,7 +123,6 @@ extend-exclude = [".env", ".venv"]
|
||||||
target-version = "py38"
|
target-version = "py38"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# Exclude UP036 as it's causing the "exit if < 3.9" to fail.
|
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"C90", # mccabe
|
"C90", # mccabe
|
||||||
"B", # bugbear
|
"B", # bugbear
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==4.0.1
|
coveralls==4.0.1
|
||||||
ruff==0.6.7
|
ruff==0.6.8
|
||||||
mypy==1.11.2
|
mypy==1.11.2
|
||||||
pre-commit==3.8.0
|
pre-commit==3.8.0
|
||||||
pytest==8.3.3
|
pytest==8.3.3
|
||||||
|
|
|
@ -11,5 +11,5 @@ catboost==1.2.7; 'arm' not in platform_machine
|
||||||
matplotlib==3.9.2
|
matplotlib==3.9.2
|
||||||
lightgbm==4.5.0
|
lightgbm==4.5.0
|
||||||
xgboost==2.0.3
|
xgboost==2.0.3
|
||||||
tensorboard==2.17.1
|
tensorboard==2.18.0
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.14.1; python_version >= "3.10"
|
scipy==1.14.1
|
||||||
scipy==1.13.1; python_version < "3.10"
|
|
||||||
scikit-learn==1.5.2
|
scikit-learn==1.5.2
|
||||||
ft-scikit-optimize==0.9.2
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.16.1
|
filelock==3.16.1
|
||||||
|
|
|
@ -4,10 +4,10 @@ bottleneck==1.4.0
|
||||||
numexpr==2.10.1
|
numexpr==2.10.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.4.6
|
ccxt==4.4.9
|
||||||
cryptography==42.0.8; platform_machine == 'armv7l'
|
cryptography==42.0.8; platform_machine == 'armv7l'
|
||||||
cryptography==43.0.1; platform_machine != 'armv7l'
|
cryptography==43.0.1; platform_machine != 'armv7l'
|
||||||
aiohttp==3.10.5
|
aiohttp==3.10.8
|
||||||
SQLAlchemy==2.0.35
|
SQLAlchemy==2.0.35
|
||||||
python-telegram-bot==21.6
|
python-telegram-bot==21.6
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
|
@ -22,9 +22,7 @@ technical==1.4.4
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
# Tables 3.10 dropped support for Python 3.9
|
tables==3.10.1
|
||||||
tables==3.9.1; python_version < "3.10"
|
|
||||||
tables==3.10.1; python_version >= "3.10"
|
|
||||||
joblib==1.4.2
|
joblib==1.4.2
|
||||||
rich==13.8.1
|
rich==13.8.1
|
||||||
pyarrow==17.0.0; platform_machine != 'armv7l'
|
pyarrow==17.0.0; platform_machine != 'armv7l'
|
||||||
|
@ -43,7 +41,7 @@ sdnotify==0.3.2
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.115.0
|
fastapi==0.115.0
|
||||||
pydantic==2.9.2
|
pydantic==2.9.2
|
||||||
uvicorn==0.30.6
|
uvicorn==0.31.0
|
||||||
pyjwt==2.9.0
|
pyjwt==2.9.0
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
psutil==6.0.0
|
psutil==6.0.0
|
||||||
|
|
|
@ -153,16 +153,13 @@ function Find-PythonExecutable {
|
||||||
"python3.12",
|
"python3.12",
|
||||||
"python3.11",
|
"python3.11",
|
||||||
"python3.10",
|
"python3.10",
|
||||||
"python3.9",
|
|
||||||
"python3",
|
"python3",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\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:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
|
|
||||||
"C:\Python312\python.exe",
|
"C:\Python312\python.exe",
|
||||||
"C:\Python311\python.exe",
|
"C:\Python311\python.exe",
|
||||||
"C:\Python310\python.exe",
|
"C:\Python310\python.exe"
|
||||||
"C:\Python39\python.exe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,10 +175,10 @@ function Main {
|
||||||
"Starting the operations..." | Out-File $LogFilePath -Append
|
"Starting the operations..." | Out-File $LogFilePath -Append
|
||||||
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
||||||
|
|
||||||
# Exit on lower versions than Python 3.9 or when Python executable not found
|
# Exit on lower versions than Python 3.10 or when Python executable not found
|
||||||
$PythonExecutable = Find-PythonExecutable
|
$PythonExecutable = Find-PythonExecutable
|
||||||
if ($null -eq $PythonExecutable) {
|
if ($null -eq $PythonExecutable) {
|
||||||
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
|
Write-Log "No suitable Python executable found. Please ensure that Python 3.10 or higher is installed and available in the system PATH." -Level 'ERROR'
|
||||||
Exit 1
|
Exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
setup.sh
10
setup.sh
|
@ -25,7 +25,7 @@ function check_installed_python() {
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for v in 12 11 10 9
|
for v in 12 11 10
|
||||||
do
|
do
|
||||||
PYTHON="python3.${v}"
|
PYTHON="python3.${v}"
|
||||||
which $PYTHON
|
which $PYTHON
|
||||||
|
@ -36,7 +36,7 @@ function check_installed_python() {
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "No usable python found. Please make sure to have python3.9 or newer installed."
|
echo "No usable python found. Please make sure to have python3.10 or newer installed."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ function install_macos() {
|
||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||||
|
|
||||||
if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9
|
if [[ $version -ge 10 ]]; then #Checks if python version >= 3.10
|
||||||
install_mac_newer_python_dependencies
|
install_mac_newer_python_dependencies
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
@ -277,7 +277,7 @@ function install() {
|
||||||
install_redhat
|
install_redhat
|
||||||
else
|
else
|
||||||
echo "This script does not support your OS."
|
echo "This script does not support your OS."
|
||||||
echo "If you have Python version 3.9 - 3.12, pip, virtualenv, ta-lib you can continue."
|
echo "If you have Python version 3.10 - 3.12, pip, virtualenv, ta-lib you can continue."
|
||||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
sleep 10
|
sleep 10
|
||||||
fi
|
fi
|
||||||
|
@ -304,7 +304,7 @@ function help() {
|
||||||
echo " -p,--plot Install dependencies for Plotting scripts."
|
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify if 3.9+ is installed
|
# Verify if 3.10+ is installed
|
||||||
check_installed_python
|
check_installed_python
|
||||||
|
|
||||||
case $* in
|
case $* in
|
||||||
|
|
|
@ -38,7 +38,7 @@ def mock_trade_1(fee, is_short: bool):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="ETH/BTC",
|
pair="ETH/BTC",
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=50.0,
|
||||||
amount_requested=123.0,
|
amount_requested=123.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
@ -201,7 +201,7 @@ def mock_trade_4(fee, is_short: bool):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="ETC/BTC",
|
pair="ETC/BTC",
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=0.0,
|
||||||
amount_requested=124.0,
|
amount_requested=124.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
|
|
@ -224,7 +224,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="NEO/USDT",
|
pair="NEO/USDT",
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=10.0,
|
amount=0.0,
|
||||||
amount_requested=10.01,
|
amount_requested=10.01,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
|
|
@ -3569,7 +3569,7 @@ def test_cancel_order_with_result(
|
||||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.cancel_order = MagicMock(return_value=corder)
|
api_mock.cancel_order = MagicMock(return_value=corder)
|
||||||
api_mock.fetch_order = MagicMock(return_value={})
|
api_mock.fetch_order = MagicMock(return_value={"id": "1234"})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
|
||||||
res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234)
|
res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import pytest
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import RetryableOrderError, TemporaryError
|
from freqtrade.exceptions import RetryableOrderError, TemporaryError
|
||||||
|
from freqtrade.exchange.common import API_RETRY_COUNT
|
||||||
from freqtrade.exchange.exchange import timeframe_to_minutes
|
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||||
from tests.conftest import EXMS, get_patched_exchange, log_has
|
from tests.conftest import EXMS, get_patched_exchange, log_has
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
@ -551,6 +552,7 @@ def test__set_leverage_okx(mocker, default_conf):
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_fetch_stoploss_order_okx(default_conf, mocker):
|
def test_fetch_stoploss_order_okx(default_conf, mocker):
|
||||||
default_conf["dry_run"] = False
|
default_conf["dry_run"] = False
|
||||||
|
mocker.patch("freqtrade.exchange.common.time.sleep")
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_order = MagicMock()
|
api_mock.fetch_order = MagicMock()
|
||||||
|
|
||||||
|
@ -569,10 +571,10 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
|
||||||
|
|
||||||
with pytest.raises(RetryableOrderError):
|
with pytest.raises(RetryableOrderError):
|
||||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||||
assert api_mock.fetch_order.call_count == 1
|
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||||
assert api_mock.fetch_open_orders.call_count == 1
|
assert api_mock.fetch_open_orders.call_count == API_RETRY_COUNT + 1
|
||||||
assert api_mock.fetch_closed_orders.call_count == 1
|
assert api_mock.fetch_closed_orders.call_count == API_RETRY_COUNT + 1
|
||||||
assert api_mock.fetch_canceled_orders.call_count == 1
|
assert api_mock.fetch_canceled_orders.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
api_mock.fetch_order.reset_mock()
|
api_mock.fetch_order.reset_mock()
|
||||||
api_mock.fetch_open_orders.reset_mock()
|
api_mock.fetch_open_orders.reset_mock()
|
||||||
|
@ -610,6 +612,39 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
|
||||||
assert dro_mock.call_count == 1
|
assert dro_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_stoploss_order_okx_exceptions(default_conf_usdt, mocker):
|
||||||
|
default_conf_usdt["dry_run"] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf_usdt,
|
||||||
|
api_mock,
|
||||||
|
"okx",
|
||||||
|
"fetch_stoploss_order",
|
||||||
|
"fetch_order",
|
||||||
|
retries=API_RETRY_COUNT + 1,
|
||||||
|
order_id="12345",
|
||||||
|
pair="ETH/USDT",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 2nd part of the function
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound())
|
||||||
|
api_mock.fetch_closed_orders = MagicMock(return_value=[])
|
||||||
|
api_mock.fetch_canceled_orders = MagicMock(return_value=[])
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf_usdt,
|
||||||
|
api_mock,
|
||||||
|
"okx",
|
||||||
|
"fetch_stoploss_order",
|
||||||
|
"fetch_open_orders",
|
||||||
|
retries=API_RETRY_COUNT + 1,
|
||||||
|
order_id="12345",
|
||||||
|
pair="ETH/USDT",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
|
"sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
|
||||||
)
|
)
|
||||||
|
|
|
@ -553,7 +553,7 @@ def test_enter_positions_global_pairlock(
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
|
def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
|
||||||
default_conf_usdt["protections"] = [
|
default_conf_usdt["_strategy_protections"] = [
|
||||||
{"method": "CooldownPeriod", "stop_duration": 60},
|
{"method": "CooldownPeriod", "stop_duration": 60},
|
||||||
{
|
{
|
||||||
"method": "StoplossGuard",
|
"method": "StoplossGuard",
|
||||||
|
|
|
@ -1299,7 +1299,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
||||||
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
|
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
|
||||||
# results do not carry-over to the next run, which is not given by using parametrize.
|
# results do not carry-over to the next run, which is not given by using parametrize.
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration": 3,
|
"stop_duration": 3,
|
||||||
|
@ -1358,7 +1358,7 @@ def test_backtest_pricecontours(
|
||||||
default_conf, mocker, testdatadir, protections, contour, expected
|
default_conf, mocker, testdatadir, protections, contour, expected
|
||||||
) -> None:
|
) -> None:
|
||||||
if protections:
|
if protections:
|
||||||
default_conf["protections"] = protections
|
default_conf["_strategy_protections"] = protections
|
||||||
default_conf["enable_protections"] = True
|
default_conf["enable_protections"] = True
|
||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
|
@ -2212,7 +2212,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"pairlists,trade_mode,result",
|
"pairlists,trade_mode,result,coin_market_calls",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2222,6 +2222,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT"],
|
["BTC/USDT", "ETH/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2231,6 +2232,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT", "XRP/USDT", "ADA/USDT"],
|
["BTC/USDT", "ETH/USDT", "XRP/USDT", "ADA/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2240,6 +2242,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2249,6 +2252,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2257,6 +2261,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2265,6 +2270,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"spot",
|
"spot",
|
||||||
["BTC/USDT", "ETH/USDT"],
|
["BTC/USDT", "ETH/USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2273,6 +2279,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"futures",
|
"futures",
|
||||||
["ETH/USDT:USDT"],
|
["ETH/USDT:USDT"],
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
@ -2281,11 +2288,34 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||||
],
|
],
|
||||||
"futures",
|
"futures",
|
||||||
["ETH/USDT:USDT", "ADA/USDT:USDT"],
|
["ETH/USDT:USDT", "ADA/USDT:USDT"],
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
# MarketCapPairList as generator - futures, 1 category
|
||||||
|
{"method": "MarketCapPairList", "number_assets": 2, "categories": ["layer-1"]}
|
||||||
|
],
|
||||||
|
"futures",
|
||||||
|
["ETH/USDT:USDT", "ADA/USDT:USDT"],
|
||||||
|
["layer-1"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
# MarketCapPairList as generator - futures, 1 category
|
||||||
|
{
|
||||||
|
"method": "MarketCapPairList",
|
||||||
|
"number_assets": 2,
|
||||||
|
"categories": ["layer-1", "protocol"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"futures",
|
||||||
|
["ETH/USDT:USDT", "ADA/USDT:USDT"],
|
||||||
|
["layer-1", "protocol"],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_MarketCapPairList_filter(
|
def test_MarketCapPairList_filter(
|
||||||
mocker, default_conf_usdt, trade_mode, markets, pairlists, result
|
mocker, default_conf_usdt, trade_mode, markets, pairlists, result, coin_market_calls
|
||||||
):
|
):
|
||||||
test_value = [
|
test_value = [
|
||||||
{"symbol": "btc"},
|
{"symbol": "btc"},
|
||||||
|
@ -2309,8 +2339,16 @@ def test_MarketCapPairList_filter(
|
||||||
markets=PropertyMock(return_value=markets),
|
markets=PropertyMock(return_value=markets),
|
||||||
exchange_has=MagicMock(return_value=True),
|
exchange_has=MagicMock(return_value=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_categories_list",
|
||||||
|
return_value=[
|
||||||
|
{"category_id": "layer-1"},
|
||||||
|
{"category_id": "protocol"},
|
||||||
|
{"category_id": "defi"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
gcm_mock = mocker.patch(
|
||||||
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
|
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
|
||||||
return_value=test_value,
|
return_value=test_value,
|
||||||
)
|
)
|
||||||
|
@ -2319,6 +2357,15 @@ def test_MarketCapPairList_filter(
|
||||||
|
|
||||||
pm = PairListManager(exchange, default_conf_usdt)
|
pm = PairListManager(exchange, default_conf_usdt)
|
||||||
pm.refresh_pairlist()
|
pm.refresh_pairlist()
|
||||||
|
if isinstance(coin_market_calls, int):
|
||||||
|
assert gcm_mock.call_count == coin_market_calls
|
||||||
|
else:
|
||||||
|
assert gcm_mock.call_count == len(coin_market_calls)
|
||||||
|
for call in coin_market_calls:
|
||||||
|
assert any(
|
||||||
|
"category" in c.kwargs and c.kwargs["category"] == call
|
||||||
|
for c in gcm_mock.call_args_list
|
||||||
|
)
|
||||||
|
|
||||||
assert pm.whitelist == result
|
assert pm.whitelist == result
|
||||||
|
|
||||||
|
@ -2376,6 +2423,33 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi
|
||||||
assert markets_mock.call_count == 3
|
assert markets_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_MarketCapPairList_filter_special_no_pair_from_coingecko(
|
||||||
|
mocker,
|
||||||
|
default_conf_usdt,
|
||||||
|
markets,
|
||||||
|
):
|
||||||
|
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList", "number_assets": 2}]
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate no pair returned from coingecko
|
||||||
|
gcm_mock = mocker.patch(
|
||||||
|
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
|
||||||
|
return_value=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
pm = PairListManager(exchange, default_conf_usdt)
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
assert gcm_mock.call_count == 1
|
||||||
|
assert pm.whitelist == []
|
||||||
|
|
||||||
|
|
||||||
def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
|
def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||||
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList"}]
|
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList"}]
|
||||||
|
@ -2391,6 +2465,27 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
|
||||||
):
|
):
|
||||||
PairListManager(exchange, default_conf_usdt)
|
PairListManager(exchange, default_conf_usdt)
|
||||||
|
|
||||||
|
# Test invalid coinmarkets list
|
||||||
|
mocker.patch(
|
||||||
|
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_categories_list",
|
||||||
|
return_value=[
|
||||||
|
{"category_id": "layer-1"},
|
||||||
|
{"category_id": "protocol"},
|
||||||
|
{"category_id": "defi"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
default_conf_usdt["pairlists"] = [
|
||||||
|
{
|
||||||
|
"method": "MarketCapPairList",
|
||||||
|
"number_assets": 20,
|
||||||
|
"categories": ["layer-1", "defi", "layer250"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException, match="Category layer250 not in coingecko category list."
|
||||||
|
):
|
||||||
|
PairListManager(exchange, default_conf_usdt)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"pairlists,expected_error,expected_warning",
|
"pairlists,expected_error,expected_warning",
|
||||||
|
|
|
@ -3,14 +3,17 @@ from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.enums import ExitType
|
from freqtrade.enums import ExitType
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.persistence.trade_model import Order
|
from freqtrade.persistence.trade_model import Order
|
||||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||||
from tests.conftest import get_patched_freqtradebot, log_has_re
|
from tests.conftest import get_patched_freqtradebot, log_has_re
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
|
||||||
|
|
||||||
|
|
||||||
def generate_mock_trade(
|
def generate_mock_trade(
|
||||||
pair: str,
|
pair: str,
|
||||||
fee: float,
|
fee: float,
|
||||||
|
@ -88,19 +91,76 @@ def generate_mock_trade(
|
||||||
|
|
||||||
|
|
||||||
def test_protectionmanager(mocker, default_conf):
|
def test_protectionmanager(mocker, default_conf):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{"method": protection} for protection in constants.AVAILABLE_PROTECTIONS
|
{"method": protection} for protection in AVAILABLE_PROTECTIONS
|
||||||
]
|
]
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
for handler in freqtrade.protections._protection_handlers:
|
for handler in freqtrade.protections._protection_handlers:
|
||||||
assert handler.name in constants.AVAILABLE_PROTECTIONS
|
assert handler.name in AVAILABLE_PROTECTIONS
|
||||||
if not handler.has_global_stop:
|
if not handler.has_global_stop:
|
||||||
assert handler.global_stop(datetime.now(timezone.utc), "*") is None
|
assert handler.global_stop(datetime.now(timezone.utc), "*") is None
|
||||||
if not handler.has_local_stop:
|
if not handler.has_local_stop:
|
||||||
assert handler.stop_per_pair("XRP/BTC", datetime.now(timezone.utc), "*") is None
|
assert handler.stop_per_pair("XRP/BTC", datetime.now(timezone.utc), "*") is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"protconf,expected",
|
||||||
|
[
|
||||||
|
([], None),
|
||||||
|
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
|
||||||
|
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period_candles": 20,
|
||||||
|
"lookback_period": 2000,
|
||||||
|
"stop_duration": 10,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
r"Protections must specify either `lookback_period`.*",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period": 20,
|
||||||
|
"stop_duration": 10,
|
||||||
|
"stop_duration_candles": 10,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
r"Protections must specify either `stop_duration`.*",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period": 20,
|
||||||
|
"stop_duration": 10,
|
||||||
|
"unlock_at": "20:02",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
|
||||||
|
"Invalid date format for unlock_at: 55:102.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_protections(protconf, expected):
|
||||||
|
if expected:
|
||||||
|
with pytest.raises(OperationalException, match=expected):
|
||||||
|
ProtectionManager.validate_protections(protconf)
|
||||||
|
else:
|
||||||
|
ProtectionManager.validate_protections(protconf)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"timeframe,expected_lookback,expected_stop,protconf",
|
"timeframe,expected_lookback,expected_stop,protconf",
|
||||||
[
|
[
|
||||||
|
@ -196,7 +256,7 @@ def test_protections_init(default_conf, timeframe, expected_lookback, expected_s
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
|
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
|
||||||
# Active for both sides (long and short)
|
# Active for both sides (long and short)
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3}
|
{"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3}
|
||||||
]
|
]
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
@ -268,7 +328,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
|
||||||
@pytest.mark.parametrize("only_per_side", [False, True])
|
@pytest.mark.parametrize("only_per_side", [False, True])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side):
|
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "StoplossGuard",
|
"method": "StoplossGuard",
|
||||||
"lookback_period": 60,
|
"lookback_period": 60,
|
||||||
|
@ -379,7 +439,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
|
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration": 60,
|
"stop_duration": 60,
|
||||||
|
@ -425,7 +485,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
|
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"unlock_at": "05:00",
|
"unlock_at": "05:00",
|
||||||
|
@ -509,7 +569,7 @@ def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machin
|
||||||
@pytest.mark.parametrize("only_per_side", [False, True])
|
@pytest.mark.parametrize("only_per_side", [False, True])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
|
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "LowProfitPairs",
|
"method": "LowProfitPairs",
|
||||||
"lookback_period": 400,
|
"lookback_period": 400,
|
||||||
|
@ -599,7 +659,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
"lookback_period": 1000,
|
"lookback_period": 1000,
|
||||||
|
@ -812,7 +872,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
||||||
def test_protection_manager_desc(
|
def test_protection_manager_desc(
|
||||||
mocker, default_conf, protectionconf, desc_expected, exception_expected
|
mocker, default_conf, protectionconf, desc_expected, exception_expected
|
||||||
):
|
):
|
||||||
default_conf["protections"] = [protectionconf]
|
default_conf["_strategy_protections"] = [protectionconf]
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
short_desc = str(freqtrade.protections.short_desc())
|
short_desc = str(freqtrade.protections.short_desc())
|
||||||
|
|
|
@ -1269,7 +1269,7 @@ def test_api_mix_tag(botclient, fee):
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short,current_rate,open_trade_value",
|
"is_short,current_rate,open_trade_value",
|
||||||
[(True, 1.098e-05, 15.0911775), (False, 1.099e-05, 15.1668225)],
|
[(True, 1.098e-05, 6.134625), (False, 1.099e-05, 6.165375)],
|
||||||
)
|
)
|
||||||
def test_api_status(
|
def test_api_status(
|
||||||
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
|
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
|
||||||
|
@ -1294,7 +1294,7 @@ def test_api_status(
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json()) == 4
|
assert len(rc.json()) == 4
|
||||||
assert rc.json()[0] == {
|
assert rc.json()[0] == {
|
||||||
"amount": 123.0,
|
"amount": 50.0,
|
||||||
"amount_requested": 123.0,
|
"amount_requested": 123.0,
|
||||||
"close_date": None,
|
"close_date": None,
|
||||||
"close_timestamp": None,
|
"close_timestamp": None,
|
||||||
|
|
|
@ -173,7 +173,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf) -> None:
|
||||||
telegram_mock.reset_mock()
|
telegram_mock.reset_mock()
|
||||||
default_conf["dry_run"] = True
|
default_conf["dry_run"] = True
|
||||||
default_conf["whitelist"] = {"method": "VolumePairList", "config": {"number_assets": 20}}
|
default_conf["whitelist"] = {"method": "VolumePairList", "config": {"number_assets": 20}}
|
||||||
default_conf["protections"] = [
|
default_conf["_strategy_protections"] = [
|
||||||
{"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}
|
{"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}
|
||||||
]
|
]
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
|
@ -75,15 +75,13 @@ class StrategyTestV3(IStrategy):
|
||||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
||||||
# @property
|
@property
|
||||||
# def protections(self):
|
def protections(self):
|
||||||
# prot = []
|
prot = []
|
||||||
# if self.protection_enabled.value:
|
if self.protection_enabled.value:
|
||||||
# prot.append({
|
# Workaround to simplify tests. This will not work in real scenarios.
|
||||||
# "method": "CooldownPeriod",
|
prot = self.config.get("_strategy_protections", {})
|
||||||
# "stop_duration_candles": self.protection_cooldown_lookback.value
|
return prot
|
||||||
# })
|
|
||||||
# return prot
|
|
||||||
|
|
||||||
bot_started = False
|
bot_started = False
|
||||||
|
|
||||||
|
|
|
@ -812,65 +812,6 @@ def test_validate_whitelist(default_conf):
|
||||||
validate_config_consistency(conf)
|
validate_config_consistency(conf)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"protconf,expected",
|
|
||||||
[
|
|
||||||
([], None),
|
|
||||||
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
|
|
||||||
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
|
|
||||||
(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"method": "StoplossGuard",
|
|
||||||
"lookback_period_candles": 20,
|
|
||||||
"lookback_period": 2000,
|
|
||||||
"stop_duration": 10,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
r"Protections must specify either `lookback_period`.*",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"method": "StoplossGuard",
|
|
||||||
"lookback_period": 20,
|
|
||||||
"stop_duration": 10,
|
|
||||||
"stop_duration_candles": 10,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
r"Protections must specify either `stop_duration`.*",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"method": "StoplossGuard",
|
|
||||||
"lookback_period": 20,
|
|
||||||
"stop_duration": 10,
|
|
||||||
"unlock_at": "20:02",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
|
|
||||||
"Invalid date format for unlock_at: 55:102.",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_validate_protections(default_conf, protconf, expected):
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf["protections"] = protconf
|
|
||||||
if expected:
|
|
||||||
with pytest.raises(OperationalException, match=expected):
|
|
||||||
validate_config_consistency(conf)
|
|
||||||
else:
|
|
||||||
validate_config_consistency(conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_ask_orderbook(default_conf, caplog) -> None:
|
def test_validate_ask_orderbook(default_conf, caplog) -> None:
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf["exit_pricing"]["use_order_book"] = True
|
conf["exit_pricing"]["use_order_book"] = True
|
||||||
|
@ -1533,8 +1474,8 @@ def test_process_deprecated_protections(default_conf, caplog):
|
||||||
assert not log_has(message, caplog)
|
assert not log_has(message, caplog)
|
||||||
|
|
||||||
config["protections"] = []
|
config["protections"] = []
|
||||||
process_temporary_deprecated_settings(config)
|
with pytest.raises(ConfigurationError, match=message):
|
||||||
assert log_has(message, caplog)
|
process_temporary_deprecated_settings(config)
|
||||||
|
|
||||||
|
|
||||||
def test_flat_vars_to_nested_dict(caplog):
|
def test_flat_vars_to_nested_dict(caplog):
|
||||||
|
|
|
@ -362,7 +362,8 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
|
||||||
assert len(freqtrade.wallets._wallets) == 5
|
assert len(freqtrade.wallets._wallets) == 5
|
||||||
assert len(freqtrade.wallets._positions) == 0
|
assert len(freqtrade.wallets._positions) == 0
|
||||||
bal = freqtrade.wallets.get_all_balances()
|
bal = freqtrade.wallets.get_all_balances()
|
||||||
assert bal["NEO"].total == 10
|
# NEO trade is not filled yet.
|
||||||
|
assert bal["NEO"].total == 0
|
||||||
assert bal["XRP"].total == 10
|
assert bal["XRP"].total == 10
|
||||||
assert bal["LTC"].total == 2
|
assert bal["LTC"].total == 2
|
||||||
usdt_bal = bal["USDT"]
|
usdt_bal = bal["USDT"]
|
||||||
|
@ -410,11 +411,11 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
|
||||||
def test_check_exit_amount(mocker, default_conf, fee):
|
def test_check_exit_amount(mocker, default_conf, fee):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50.0)
|
||||||
|
|
||||||
create_mock_trades(fee, is_short=None)
|
create_mock_trades(fee, is_short=None)
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
assert trade.amount == 123
|
assert trade.amount == 50.0
|
||||||
|
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||||
assert update_mock.call_count == 0
|
assert update_mock.call_count == 0
|
||||||
|
@ -423,7 +424,7 @@ def test_check_exit_amount(mocker, default_conf, fee):
|
||||||
update_mock.reset_mock()
|
update_mock.reset_mock()
|
||||||
# Reduce returned amount to below the trade amount - which should
|
# Reduce returned amount to below the trade amount - which should
|
||||||
# trigger a wallet update and return False, triggering "order refinding"
|
# trigger a wallet update and return False, triggering "order refinding"
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=100)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=40)
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||||
assert update_mock.call_count == 1
|
assert update_mock.call_count == 1
|
||||||
assert total_mock.call_count == 2
|
assert total_mock.call_count == 2
|
||||||
|
@ -433,12 +434,12 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
|
||||||
default_conf["trading_mode"] = "futures"
|
default_conf["trading_mode"] = "futures"
|
||||||
default_conf["margin_mode"] = "isolated"
|
default_conf["margin_mode"] = "isolated"
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50)
|
||||||
|
|
||||||
create_mock_trades(fee, is_short=None)
|
create_mock_trades(fee, is_short=None)
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.trading_mode = "futures"
|
trade.trading_mode = "futures"
|
||||||
assert trade.amount == 123
|
assert trade.amount == 50
|
||||||
|
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||||
assert total_mock.call_count == 0
|
assert total_mock.call_count == 0
|
||||||
|
|
Loading…
Reference in New Issue
Block a user