Merge branch 'develop' into pr/Axel-CH/10062

This commit is contained in:
Matthias 2024-10-01 20:51:39 +02:00
commit 7e2d55743e
60 changed files with 1545 additions and 696 deletions

View File

@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
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:
- uses: actions/checkout@v4
@ -72,7 +72,7 @@ jobs:
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- 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:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@ -139,10 +139,7 @@ jobs:
strategy:
matrix:
os: [ "macos-12", "macos-13", "macos-14" ]
python-version: ["3.9", "3.10", "3.11", "3.12"]
exclude:
- os: "macos-14"
python-version: "3.9"
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
@ -263,7 +260,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4

View File

@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.6.7'
rev: 'v0.6.8'
hooks:
- id: ruff
- id: ruff-format

View File

@ -61,7 +61,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## 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] **Dry-run**: Run the bot without paying money.
- [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
- [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/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)

View File

@ -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": {
"description": "Telegram settings.",
"type": "object",

View File

@ -229,7 +229,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| | **Plugins**
| `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
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
| | **Telegram**
| `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

View File

@ -75,7 +75,10 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
## 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.
## 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.

View File

@ -241,7 +241,6 @@ No protection should use datetime directly, but use the provided `date_now` vari
!!! Tip "Writing a new Protection"
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

View File

@ -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.
## Optimizing protections
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.

View File

@ -360,14 +360,21 @@ The optional `bearer_token` will be included in the requests Authorization Heade
"method": "MarketCapPairList",
"number_assets": 20,
"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.
`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

View File

@ -1,24 +1,16 @@
## 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.
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.
!!! Tip
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
!!! Note "Backtesting"
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
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.

View File

@ -85,7 +85,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
Alternatively
- Python 3.9+
- Python 3.10+
- pip (pip3)
- git
- TA-Lib

View File

@ -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).
!!! 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.
!!! Warning "Up-to-date clock"
@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
### 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/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [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.
!!! 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"
#### Install necessary dependencies
@ -69,7 +69,7 @@ OS Specific steps are listed first, the common section below is necessary for al
=== "RaspberryPi/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.
@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
** --install **
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`
* Setup your virtualenv under `.venv/`

View File

@ -1,7 +1,7 @@
markdown==3.7
mkdocs==1.6.1
mkdocs-material==9.5.36
mkdocs-material==9.5.39
mdx_truly_sane_lists==1.3
pymdown-extensions==10.10.1
pymdown-extensions==10.11.1
jinja2==3.1.4
mike==2.1.3

View File

@ -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.
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
@ -42,7 +42,7 @@ cd freqtrade
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.
Other versions must be downloaded from the above link.

View File

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

View File

@ -3,7 +3,7 @@
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.9)
> python -m freqtrade (with Python >= 3.10)
"""
from freqtrade import main

View File

@ -1,5 +1,4 @@
import logging
import sys
import time
from pathlib import Path
from typing import Any, Dict
@ -20,9 +19,6 @@ def start_strategy_update(args: Dict[str, Any]) -> 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)
strategy_objs = StrategyResolver.search_all_objects(

View File

@ -4,7 +4,6 @@ from typing import Dict
from freqtrade.constants import (
AVAILABLE_DATAHANDLERS,
AVAILABLE_PAIRLISTS,
AVAILABLE_PROTECTIONS,
BACKTEST_BREAKDOWNS,
DRY_RUN_WALLET,
EXPORT_OPTIONS,
@ -449,60 +448,6 @@ CONF_SCHEMA = {
"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
"telegram": {
"description": "Telegram settings.",

View File

@ -1,7 +1,6 @@
import logging
from collections import Counter
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict
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_edge(conf)
_validate_whitelist(conf)
_validate_protections(conf)
_validate_unlimited_amount(conf)
_validate_ask_orderbook(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.")
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:
ask_strategy = conf.get("exit_pricing", {})
ob_min = ask_strategy.get("order_book_min")

View File

@ -177,4 +177,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
)
if "protections" in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
raise ConfigurationError(
"DEPRECATED: Setting 'protections' in the configuration is deprecated."
)

View File

@ -82,6 +82,11 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
for f in sub_dirs:
subfolder = folder / f
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)
return folder

View File

@ -57,7 +57,6 @@ AVAILABLE_PAIRLISTS = [
"SpreadFilter",
"VolatilityFilter",
]
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "hdf5", "feather", "parquet"]
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]

File diff suppressed because it is too large Load Diff

View File

@ -238,7 +238,13 @@ class Bybit(Exchange):
return orders
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)
if not order:
order = self.fetch_order_emulated(order_id, pair, {})
if (
order.get("status") == "canceled"
and order.get("filled") == 0.0

View File

@ -3,8 +3,6 @@
import logging
from typing import Dict
from ccxt import SIGNIFICANT_DIGITS
from freqtrade.enums import TradingMode
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
@ -36,10 +34,3 @@ class Hyperliquid(Exchange):
config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config)
return config
@property
def precision_mode_price(self) -> int:
"""
Override the default precision mode for price.
"""
return SIGNIFICANT_DIGITS

View File

@ -13,7 +13,7 @@ from freqtrade.exceptions import (
TemporaryError,
)
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.misc import safe_value_fallback2
from freqtrade.util import dt_now, dt_ts
@ -208,6 +208,7 @@ class Okx(Exchange):
order["type"] = "stoploss"
return order
@retrier(retries=API_RETRY_COUNT)
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
if self._config["dry_run"]:
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)
self._log_exchange_response("fetch_stoploss_order", order_reg)
return self._convert_stop_order(pair, order_id, order_reg)
except ccxt.OrderNotFound:
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
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"}
for method in (
self._api.fetch_open_orders,
@ -231,8 +244,16 @@ class Okx(Exchange):
if orders_f:
order = orders_f[0]
return self._convert_stop_order(pair, order_id, order)
except ccxt.BaseError:
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:

View File

@ -86,9 +86,6 @@ class BasePyTorchRegressor(BasePyTorchModel):
dk.feature_pipeline = self.define_data_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"]) = (
dk.feature_pipeline.fit_transform(
dd["train_features"], dd["train_labels"], dd["train_weights"]

View File

@ -141,7 +141,7 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
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
else:
dk.DI_values = np.zeros(outliers.shape[0])

View File

@ -10,8 +10,8 @@ from typing import Any, List, Optional
# check min. python version
if sys.version_info < (3, 9): # pragma: no cover
sys.exit("Freqtrade requires Python version >= 3.9")
if sys.version_info < (3, 10): # pragma: no cover
sys.exit("Freqtrade requires Python version >= 3.10")
from freqtrade import __version__
from freqtrade.commands import Arguments

View File

@ -273,10 +273,6 @@ class Backtesting:
def _load_protections(self, strategy: IStrategy):
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)
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:

View File

@ -39,6 +39,11 @@ class __OptionPairlistParameter(__PairlistParameterBase):
options: List[str]
class __ListPairListParamenter(__PairlistParameterBase):
type: Literal["list"]
default: Union[List[str], None]
class __BoolPairlistParameter(__PairlistParameterBase):
type: Literal["boolean"]
default: Union[bool, None]
@ -49,6 +54,7 @@ PairlistParameter = Union[
__StringPairlistParameter,
__OptionPairlistParameter,
__BoolPairlistParameter,
__ListPairListParamenter,
]

View File

@ -35,6 +35,7 @@ class MarketCapPairList(IPairList):
self._number_assets = self._pairlistconfig["number_assets"]
self._max_rank = self._pairlistconfig.get("max_rank", 30)
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._def_candletype = self._config["candle_type_def"]
@ -45,6 +46,17 @@ class MarketCapPairList(IPairList):
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:
raise OperationalException("This filter only support marketcap rank up to 250.")
@ -85,6 +97,15 @@ class MarketCapPairList(IPairList):
"description": "Max rank of assets",
"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": {
"type": "number",
"default": 86400,
@ -132,15 +153,29 @@ class MarketCapPairList(IPairList):
"""
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:
data = self._coingecko.get_coins_markets(
vs_currency="usd",
order="market_cap_desc",
per_page="250",
page="1",
sparkline="false",
locale="en",
)
data = []
if not self._categories:
data = self._coingecko.get_coins_markets(**default_kwargs)
else:
for category in self._categories:
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:
marketcap_list = [row["symbol"] for row in data]
self._marketcap_cache["marketcap"] = marketcap_list
@ -157,7 +192,7 @@ class MarketCapPairList(IPairList):
for mc_pair in top_marketcap:
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)
if len(filtered_pairlist) == self._number_assets:
break
@ -165,4 +200,5 @@ class MarketCapPairList(IPairList):
if len(filtered_pairlist) > 0:
return filtered_pairlist
return pairlist
# If no pairs are found, return the original pairlist
return []

View File

@ -4,9 +4,10 @@ Protection manager class
import logging
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.exceptions import ConfigurationError
from freqtrade.persistence import PairLocks
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.protections import IProtection
@ -21,6 +22,7 @@ class ProtectionManager:
self._config = config
self._protection_handlers: List[IProtection] = []
self.validate_protections(protections)
for protection_handler_config in protections:
protection_handler = ProtectionResolver.load_protection(
protection_handler_config["method"],
@ -76,3 +78,40 @@ class ProtectionManager:
pair, lock.until, lock.reason, now=now, side=lock.lock_side
)
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')}."
)

View File

@ -69,7 +69,6 @@ class StrategyResolver(IResolver):
("order_time_in_force", None),
("stake_currency", None),
("stake_amount", None),
("protections", None),
("startup_candle_count", None),
("unfilledtimeout", None),
("use_exit_signal", True),

View File

@ -77,7 +77,6 @@ def __run_backtest_bg(btconfig: Config):
lastconfig["timerange"] = btconfig["timerange"]
lastconfig["timeframe"] = strat.timeframe
lastconfig["protections"] = btconfig.get("protections", [])
lastconfig["enable_protections"] = btconfig.get("enable_protections")
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")

View File

@ -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)
async def index_html(rest_of_path: str):
"""
@ -56,7 +46,7 @@ async def index_html(rest_of_path: str):
if filename.suffix == ".js":
# Force text/javascript for .js files - Circumvent faulty system configuration
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)
index_file = uibase / "index.html"

View File

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

View File

@ -13,14 +13,13 @@ authors = [
description = "Freqtrade - Client scripts"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",

View File

@ -13,14 +13,12 @@ authors = [
description = "Freqtrade - Crypto Trading Bot"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@ -125,7 +123,6 @@ extend-exclude = [".env", ".venv"]
target-version = "py38"
[tool.ruff.lint]
# Exclude UP036 as it's causing the "exit if < 3.9" to fail.
extend-select = [
"C90", # mccabe
"B", # bugbear

View File

@ -7,7 +7,7 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.6.7
ruff==0.6.8
mypy==1.11.2
pre-commit==3.8.0
pytest==8.3.3

View File

@ -11,5 +11,5 @@ catboost==1.2.7; 'arm' not in platform_machine
matplotlib==3.9.2
lightgbm==4.5.0
xgboost==2.0.3
tensorboard==2.17.1
tensorboard==2.18.0
datasieve==0.1.7

View File

@ -2,8 +2,7 @@
-r requirements.txt
# Required for hyperopt
scipy==1.14.1; python_version >= "3.10"
scipy==1.13.1; python_version < "3.10"
scipy==1.14.1
scikit-learn==1.5.2
ft-scikit-optimize==0.9.2
filelock==3.16.1

View File

@ -4,10 +4,10 @@ bottleneck==1.4.0
numexpr==2.10.1
pandas-ta==0.3.14b
ccxt==4.4.6
ccxt==4.4.9
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==43.0.1; platform_machine != 'armv7l'
aiohttp==3.10.5
aiohttp==3.10.8
SQLAlchemy==2.0.35
python-telegram-bot==21.6
# can't be hard-pinned due to telegram-bot pinning httpx with ~
@ -22,9 +22,7 @@ technical==1.4.4
tabulate==0.9.0
pycoingecko==3.1.0
jinja2==3.1.4
# Tables 3.10 dropped support for Python 3.9
tables==3.9.1; python_version < "3.10"
tables==3.10.1; python_version >= "3.10"
tables==3.10.1
joblib==1.4.2
rich==13.8.1
pyarrow==17.0.0; platform_machine != 'armv7l'
@ -43,7 +41,7 @@ sdnotify==0.3.2
# API Server
fastapi==0.115.0
pydantic==2.9.2
uvicorn==0.30.6
uvicorn==0.31.0
pyjwt==2.9.0
aiofiles==24.1.0
psutil==6.0.0

View File

@ -153,16 +153,13 @@ function Find-PythonExecutable {
"python3.12",
"python3.11",
"python3.10",
"python3.9",
"python3",
"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\Python310\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe",
"C:\Python39\python.exe"
"C:\Python310\python.exe"
)
@ -178,10 +175,10 @@ function Main {
"Starting the operations..." | 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
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
}

View File

@ -25,7 +25,7 @@ function check_installed_python() {
exit 2
fi
for v in 12 11 10 9
for v in 12 11 10
do
PYTHON="python3.${v}"
which $PYTHON
@ -36,7 +36,7 @@ function check_installed_python() {
fi
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
}
@ -166,7 +166,7 @@ function install_macos() {
#Gets number after decimal in python version
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
fi
}
@ -277,7 +277,7 @@ function install() {
install_redhat
else
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."
sleep 10
fi
@ -304,7 +304,7 @@ function help() {
echo " -p,--plot Install dependencies for Plotting scripts."
}
# Verify if 3.9+ is installed
# Verify if 3.10+ is installed
check_installed_python
case $* in

View File

@ -38,7 +38,7 @@ def mock_trade_1(fee, is_short: bool):
trade = Trade(
pair="ETH/BTC",
stake_amount=0.001,
amount=123.0,
amount=50.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
@ -201,7 +201,7 @@ def mock_trade_4(fee, is_short: bool):
trade = Trade(
pair="ETC/BTC",
stake_amount=0.001,
amount=123.0,
amount=0.0,
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,

View File

@ -224,7 +224,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
trade = Trade(
pair="NEO/USDT",
stake_amount=20.0,
amount=10.0,
amount=0.0,
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,

View File

@ -3569,7 +3569,7 @@ def test_cancel_order_with_result(
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock = MagicMock()
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)
res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234)
assert isinstance(res, dict)

View File

@ -6,6 +6,7 @@ import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import RetryableOrderError, TemporaryError
from freqtrade.exchange.common import API_RETRY_COUNT
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import EXMS, get_patched_exchange, log_has
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")
def test_fetch_stoploss_order_okx(default_conf, mocker):
default_conf["dry_run"] = False
mocker.patch("freqtrade.exchange.common.time.sleep")
api_mock = MagicMock()
api_mock.fetch_order = MagicMock()
@ -569,10 +571,10 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
with pytest.raises(RetryableOrderError):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_order.call_count == 1
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 1
assert api_mock.fetch_canceled_orders.call_count == 1
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_open_orders.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_closed_orders.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_canceled_orders.call_count == API_RETRY_COUNT + 1
api_mock.fetch_order.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
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(
"sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
)

View File

@ -553,7 +553,7 @@ def test_enter_positions_global_pairlock(
@pytest.mark.parametrize("is_short", [False, True])
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": "StoplossGuard",

View File

@ -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
# results do not carry-over to the next run, which is not given by using parametrize.
patch_exchange(mocker)
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 3,
@ -1358,7 +1358,7 @@ def test_backtest_pricecontours(
default_conf, mocker, testdatadir, protections, contour, expected
) -> None:
if protections:
default_conf["protections"] = protections
default_conf["_strategy_protections"] = protections
default_conf["enable_protections"] = True
patch_exchange(mocker)

View File

@ -2212,7 +2212,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
@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",
["BTC/USDT", "ETH/USDT"],
1,
),
(
[
@ -2231,6 +2232,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT", "ADA/USDT"],
1,
),
(
[
@ -2240,6 +2242,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@ -2249,6 +2252,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@ -2257,6 +2261,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@ -2265,6 +2270,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT"],
1,
),
(
[
@ -2273,6 +2279,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"futures",
["ETH/USDT:USDT"],
1,
),
(
[
@ -2281,11 +2288,34 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"futures",
["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(
mocker, default_conf_usdt, trade_mode, markets, pairlists, result
mocker, default_conf_usdt, trade_mode, markets, pairlists, result, coin_market_calls
):
test_value = [
{"symbol": "btc"},
@ -2309,8 +2339,16 @@ def test_MarketCapPairList_filter(
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
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",
return_value=test_value,
)
@ -2319,6 +2357,15 @@ def test_MarketCapPairList_filter(
pm = PairListManager(exchange, default_conf_usdt)
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
@ -2376,6 +2423,33 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi
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):
exchange = get_patched_exchange(mocker, default_conf_usdt)
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList"}]
@ -2391,6 +2465,27 @@ def test_MarketCapPairList_exceptions(mocker, 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(
"pairlists,expected_error,expected_warning",

View File

@ -3,14 +3,17 @@ from datetime import datetime, timedelta, timezone
import pytest
from freqtrade import constants
from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.trade_model import Order
from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
def generate_mock_trade(
pair: str,
fee: float,
@ -88,19 +91,76 @@ def generate_mock_trade(
def test_protectionmanager(mocker, default_conf):
default_conf["protections"] = [
{"method": protection} for protection in constants.AVAILABLE_PROTECTIONS
default_conf["_strategy_protections"] = [
{"method": protection} for protection in AVAILABLE_PROTECTIONS
]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
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:
assert handler.global_stop(datetime.now(timezone.utc), "*") is None
if not handler.has_local_stop:
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(
"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")
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_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}
]
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.usefixtures("init_persistence")
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",
"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")
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 60,
@ -425,7 +485,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"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.usefixtures("init_persistence")
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "LowProfitPairs",
"lookback_period": 400,
@ -599,7 +659,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown(mocker, default_conf, fee, caplog):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "MaxDrawdown",
"lookback_period": 1000,
@ -812,7 +872,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
def test_protection_manager_desc(
mocker, default_conf, protectionconf, desc_expected, exception_expected
):
default_conf["protections"] = [protectionconf]
default_conf["_strategy_protections"] = [protectionconf]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
short_desc = str(freqtrade.protections.short_desc())

View File

@ -1269,7 +1269,7 @@ def test_api_mix_tag(botclient, fee):
@pytest.mark.parametrize(
"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(
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
@ -1294,7 +1294,7 @@ def test_api_status(
assert_response(rc)
assert len(rc.json()) == 4
assert rc.json()[0] == {
"amount": 123.0,
"amount": 50.0,
"amount_requested": 123.0,
"close_date": None,
"close_timestamp": None,

View File

@ -173,7 +173,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf) -> None:
telegram_mock.reset_mock()
default_conf["dry_run"] = True
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}
]
freqtradebot = get_patched_freqtradebot(mocker, default_conf)

View File

@ -75,15 +75,13 @@ class StrategyTestV3(IStrategy):
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
@property
def protections(self):
prot = []
if self.protection_enabled.value:
# Workaround to simplify tests. This will not work in real scenarios.
prot = self.config.get("_strategy_protections", {})
return prot
bot_started = False

View File

@ -812,65 +812,6 @@ def test_validate_whitelist(default_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:
conf = deepcopy(default_conf)
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)
config["protections"] = []
process_temporary_deprecated_settings(config)
assert log_has(message, caplog)
with pytest.raises(ConfigurationError, match=message):
process_temporary_deprecated_settings(config)
def test_flat_vars_to_nested_dict(caplog):

View File

@ -362,7 +362,8 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
assert len(freqtrade.wallets._wallets) == 5
assert len(freqtrade.wallets._positions) == 0
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["LTC"].total == 2
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):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
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)
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 update_mock.call_count == 0
@ -423,7 +424,7 @@ def test_check_exit_amount(mocker, default_conf, fee):
update_mock.reset_mock()
# Reduce returned amount to below the trade amount - which should
# 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 update_mock.call_count == 1
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["margin_mode"] = "isolated"
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)
trade = Trade.session.scalars(select(Trade)).first()
trade.trading_mode = "futures"
assert trade.amount == 123
assert trade.amount == 50
assert freqtrade.wallets.check_exit_amount(trade) is True
assert total_mock.call_count == 0