diff --git a/build_helpers/schema.json b/build_helpers/schema.json
index bc1985b17e5..9d7a95c9777 100644
--- a/build_helpers/schema.json
+++ b/build_helpers/schema.json
@@ -1273,13 +1273,19 @@
"description": "Name of the exchange.",
"type": "string"
},
+ "api_key": {
+ "description": "API key for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__API_KEY",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"key": {
- "description": "API key for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__KEY",
+ "description": "API key for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__KEY Deprecated, use api_key instead.",
"type": [
"string",
"null"
- ],
- "default": null
+ ]
},
"secret": {
"description": "API secret for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__SECRET",
diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json
index b62a72c3b06..aad6c992740 100644
--- a/config_examples/config_binance.example.json
+++ b/config_examples/config_binance.example.json
@@ -31,7 +31,7 @@
},
"exchange": {
"name": "binance",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {
diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json
index a23b3f60c7a..91897bc787d 100644
--- a/config_examples/config_freqai.example.json
+++ b/config_examples/config_freqai.example.json
@@ -17,8 +17,8 @@
},
"exchange": {
"name": "binance",
- "key": "",
- "secret": "",
+ "api_key": "your_exchange_api_key",
+ "secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json
index 94726813ff6..ce9d057adff 100644
--- a/config_examples/config_full.example.json
+++ b/config_examples/config_full.example.json
@@ -111,7 +111,7 @@
],
"exchange": {
"name": "binance",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"password": "",
"log_responses": false,
diff --git a/config_examples/config_kraken.example.json b/config_examples/config_kraken.example.json
index c9f79134daf..f3dde3b756a 100644
--- a/config_examples/config_kraken.example.json
+++ b/config_examples/config_kraken.example.json
@@ -31,8 +31,8 @@
},
"exchange": {
"name": "kraken",
- "key": "your_exchange_key",
- "secret": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
+ "secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {
},
diff --git a/docs/configuration.md b/docs/configuration.md
index 1aecb8d220b..229258b71e2 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -217,7 +217,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1
| | **Exchange**
| `exchange.name` | **Required.** Name of the exchange class to use.
**Datatype:** String
-| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
+| `exchange.api_key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
@@ -699,7 +699,7 @@ creating trades on the exchange.
```json
"exchange": {
"name": "binance",
- "key": "key",
+ "api_key": "api_key",
"secret": "secret",
...
}
@@ -749,7 +749,7 @@ API Keys are usually only required for live trading (trading for real money, bot
{
"exchange": {
"name": "binance",
- "key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
+ "api_key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
//"password": "", // Optional, not needed by all exchanges)
// ...
diff --git a/docs/exchanges.md b/docs/exchanges.md
index 02c278f2b26..2bdf9517d87 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -25,7 +25,7 @@ A exchange configuration for "binance" would look as follows:
```json
"exchange": {
"name": "binance",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
@@ -40,7 +40,7 @@ In case of problems related to rate-limits (usually DDOS Exceptions in your logs
```json
"exchange": {
"name": "kraken",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
@@ -95,7 +95,7 @@ They can however also be configured via configuration file. Since json doesn't s
``` json
// ...
- "key": "",
+ "api_key": "",
"secret": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBABACAFQA<...>s8KX8=\n-----END PRIVATE KEY-----"
// ...
```
@@ -224,7 +224,7 @@ Kraken Futures uses the exchange id `krakenfutures` and supports isolated future
```jsonc
"exchange": {
"name": "krakenfutures",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret"
},
"trading_mode": "futures",
@@ -250,7 +250,7 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
```json
"exchange": {
"name": "kucoin",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
// ...
@@ -283,7 +283,7 @@ OKX requires a passphrase for each api key, you will therefore need to add this
```json
"exchange": {
"name": "okx",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
// ...
@@ -364,7 +364,7 @@ Bitget requires a passphrase for each api key, you will therefore need to add th
```json
"exchange": {
"name": "bitget",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
// ...
@@ -511,7 +511,7 @@ If your account is required to use an operatorId, you can set it in the configur
``` json
"exchange": {
"name": "bitvavo",
- "key": "",
+ "api_key": "",
"secret": "",
"ccxt_config": {
"options": {
diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md
index c34ace15c28..4bdb9ce0d43 100644
--- a/docs/includes/pairlists.md
+++ b/docs/includes/pairlists.md
@@ -181,7 +181,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
* `max_value`: Sets a maximum percentage change threshold. Pairs with a percentage change above this value will be filtered out.
* `sort_direction`: Specifies the order in which pairs are sorted based on their percentage change. Accepts two values: `asc` for ascending order and `desc` for descending order.
* `refresh_period`: Defines the interval (in seconds) at which the pairlist will be refreshed. The default is 1800 seconds (30 minutes).
-* `lookback_days`: Number of days to look back. When `lookback_days` is selected, the `lookback_timeframe` is defaulted to 1 day.
+* `lookback_days`: Number of days to look back. `lookback_days` implies a `lookback_timeframe` of 1 day - combining it with a different `lookback_timeframe` will result in an error.
* `lookback_timeframe`: Timeframe to use for the lookback period.
* `lookback_period`: Number of periods to look back at.
@@ -635,7 +635,9 @@ If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio
#### RangeStabilityFilter
-Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change` or above `max_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
+Removes pairs where the difference between lowest low and highest high over `lookback_period` candles of `lookback_timeframe` (defaults to `1d`) is below `min_rate_of_change` or above `max_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
+
+For convenience, `lookback_days` can be used instead, which implies daily candles (equivalent to setting `lookback_period` with a `lookback_timeframe` of `1d`). One of `lookback_days` or `lookback_period` must be set - setting both is ambiguous and will result in an error, as does combining `lookback_days` with a `lookback_timeframe` other than `1d`. Setting neither is deprecated and currently falls back to a lookback of 10 days - this fallback will be removed in a future version.
In the below example:
If the trading range over the last 10 days is <1% or >99%, remove the pair from the whitelist.
@@ -644,7 +646,8 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
"pairlists": [
{
"method": "RangeStabilityFilter",
- "lookback_days": 10,
+ "lookback_timeframe": "1d",
+ "lookback_period": 10,
"min_rate_of_change": 0.01,
"max_rate_of_change": 0.99,
"refresh_period": 86400
@@ -652,6 +655,21 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
]
```
+The same filter based on a trading range of 72 1h candles (3 days) would look as follows:
+
+```json
+"pairlists": [
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_timeframe": "1h",
+ "lookback_period": 72,
+ "min_rate_of_change": 0.01,
+ "max_rate_of_change": 0.99,
+ "refresh_period": 3600
+ }
+]
+```
+
Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting for this pairlist.
!!! Tip
@@ -660,9 +678,11 @@ Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting f
#### VolatilityFilter
-Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
+Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic candle-to-candle returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
-This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
+This filter removes pairs if the average volatility over `lookback_period` candles of `lookback_timeframe` (defaults to `1d`) is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
+
+For convenience, `lookback_days` can be used instead, which implies daily candles (equivalent to setting `lookback_period` with a `lookback_timeframe` of `1d`). One of `lookback_days` or `lookback_period` must be set - setting both is ambiguous and will result in an error, as does combining `lookback_days` with a `lookback_timeframe` other than `1d`. Setting neither is deprecated and currently falls back to a lookback of 10 days - this fallback will be removed in a future version.
This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs.
@@ -673,7 +693,8 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove
"pairlists": [
{
"method": "VolatilityFilter",
- "lookback_days": 10,
+ "lookback_timeframe": "1d",
+ "lookback_period": 10,
"min_volatility": 0.05,
"max_volatility": 0.50,
"refresh_period": 86400
@@ -681,6 +702,21 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove
]
```
+The same filter based on the volatility of 72 1h candles (3 days) would look as follows:
+
+```json
+"pairlists": [
+ {
+ "method": "VolatilityFilter",
+ "lookback_timeframe": "1h",
+ "lookback_period": 72,
+ "min_volatility": 0.05,
+ "max_volatility": 0.50,
+ "refresh_period": 3600
+ }
+]
+```
+
Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting mode for this pairlist.
### Full example of Pairlist Handlers
diff --git a/docs/utils.md b/docs/utils.md
index 0ad901e98ea..66b64bf6673 100644
--- a/docs/utils.md
+++ b/docs/utils.md
@@ -70,7 +70,7 @@ Your combined configuration is:
"stake_currency": "USDT",
"exchange": {
"name": "binance",
- "key": "REDACTED",
+ "api_key": "REDACTED",
"secret": "REDACTED",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/config_schema/config_schema.py b/freqtrade/config_schema/config_schema.py
index aaee625d302..2036a3f5037 100644
--- a/freqtrade/config_schema/config_schema.py
+++ b/freqtrade/config_schema/config_schema.py
@@ -936,12 +936,18 @@
"type": "object",
"properties": {
"name": {"description": "Name of the exchange.", "type": "string"},
+ "api_key": {
+ "description": (
+ f"API key for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__API_KEY"
+ ),
+ "type": ["string", "null"],
+ },
"key": {
"description": (
f"API key for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__KEY"
+ " Deprecated, use api_key instead."
),
"type": ["string", "null"],
- "default": None,
},
"secret": {
"description": (
diff --git a/freqtrade/configuration/deploy_config.py b/freqtrade/configuration/deploy_config.py
index 071ee0b1179..5401750d62b 100644
--- a/freqtrade/configuration/deploy_config.py
+++ b/freqtrade/configuration/deploy_config.py
@@ -137,8 +137,8 @@ def ask_user_config() -> dict[str, Any]:
},
{
"type": "password",
- "name": "exchange_key",
- "message": "Insert Exchange Key",
+ "name": "exchange_api_key",
+ "message": "Insert Exchange API Key",
"when": lambda x: not x["dry_run"],
},
{
@@ -149,7 +149,7 @@ def ask_user_config() -> dict[str, Any]:
},
{
"type": "password",
- "name": "exchange_key_password",
+ "name": "exchange_api_key_password",
"message": "Insert Exchange API Key password",
"when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
},
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index dafbd411e6b..b2f6517bd9c 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -196,7 +196,7 @@
"dry_run": True,
"exchange": {
"name": "",
- "key": None,
+ "api_key": None,
"secret": None,
"pair_whitelist": [],
"ccxt_async_config": {},
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index b33690a168e..303f7617d53 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -2923,37 +2923,43 @@ async def gather_coroutines(coro):
return results_df
def refresh_ohlcv_with_cache(
- self, pairs: list[PairWithTimeframe], since_ms: int
+ self, pairs: list[PairWithTimeframe], *, lookback_period: int
) -> dict[PairWithTimeframe, DataFrame]:
"""
Refresh ohlcv data for all pairs in needed_pairs if necessary.
- Caches data with expiring per timeframe.
- Should only be used for pairlists which need "on time" expirarion, and no longer cache.
+ Caches data per (timeframe, lookback_period), expiring with each new candle.
+ Should only be used for pairlists which need "on time" expiration, and no longer cache.
+ :param pairs: List of pairs, timeframes to refresh
+ :param lookback_period: Amount of candles to fetch.
+ Downloads lookback_period + 1 candles, as measuring a change over N candles
+ requires N + 1 candles of data.
"""
timeframes = {p[1] for p in pairs}
for timeframe in timeframes:
- if (timeframe, since_ms) not in self._expiring_candle_cache:
+ if (timeframe, lookback_period) not in self._expiring_candle_cache:
timeframe_in_sec = timeframe_to_seconds(timeframe)
# Initialise cache
- self._expiring_candle_cache[(timeframe, since_ms)] = PeriodicCache(
+ self._expiring_candle_cache[(timeframe, lookback_period)] = PeriodicCache(
ttl=timeframe_in_sec, maxsize=1000
)
# Get candles from cache
candles = {
- c: self._expiring_candle_cache[(c[1], since_ms)].get(c, None)
+ c: self._expiring_candle_cache[(c[1], lookback_period)].get(c, None)
for c in pairs
- if c in self._expiring_candle_cache[(c[1], since_ms)]
+ if c in self._expiring_candle_cache[(c[1], lookback_period)]
}
pairs_to_download = [p for p in pairs if p not in candles]
- if pairs_to_download:
- candles_new = self.refresh_latest_ohlcv(
- pairs_to_download, since_ms=since_ms, cache=False
- )
+ for timeframe in timeframes:
+ tf_pairs = [p for p in pairs_to_download if p[1] == timeframe]
+ if not tf_pairs:
+ continue
+ since_ms = dt_ts(date_minus_candles(timeframe, lookback_period + 1))
+ candles_new = self.refresh_latest_ohlcv(tf_pairs, since_ms=since_ms, cache=False)
for c, val in candles_new.items():
candles[c] = val
- self._expiring_candle_cache[(c[1], since_ms)][c] = val
+ self._expiring_candle_cache[(c[1], lookback_period)][c] = val
return candles
def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py
index fc3908fbe34..5c5ff3a5a55 100644
--- a/freqtrade/plugins/pairlist/IPairList.py
+++ b/freqtrade/plugins/pairlist/IPairList.py
@@ -144,6 +144,118 @@ def refresh_period_parameter() -> dict[str, PairlistParameter]:
}
}
+ @staticmethod
+ def lookback_parameters(default_period: int = 0) -> dict[str, PairlistParameter]:
+ """
+ Lookback parameters, for Pairlist Handlers supporting a lookback range.
+ :param default_period: Default lookback_period - 0 when the lookback range is optional.
+ """
+ return {
+ "lookback_days": {
+ "type": "number",
+ "default": None,
+ "description": "Lookback Days",
+ "help": "Number of days to look back at. Implies a lookback_timeframe of 1d.",
+ },
+ "lookback_timeframe": {
+ "type": "string",
+ "default": "1d",
+ "description": "Lookback Timeframe",
+ "help": "Timeframe to use for lookback.",
+ },
+ "lookback_period": {
+ "type": "number",
+ "default": default_period,
+ "description": "Lookback Period",
+ "help": "Number of periods to look back at.",
+ },
+ }
+
+ def _init_lookback_config(
+ self, *, required: bool = False, deprecated_fallback: int = 0
+ ) -> None:
+ """
+ Resolve the lookback configuration (`lookback_days`, `lookback_timeframe` and
+ `lookback_period`) into `self._lookback_timeframe` and `self._lookback_period`,
+ and validate it against the exchange's max request size.
+ `lookback_days` is a convenience alias for `lookback_period` on daily candles.
+ :param required: Whether this handler needs a lookback range. Handlers with an optional
+ lookback end up with a period of 0, disabling the lookback range.
+ :param deprecated_fallback: Number of days to fall back to when `required` is set, but
+ neither `lookback_days` nor `lookback_period` is configured.
+ Deprecated - will be removed in a future version.
+ """
+ lookback_days: int = self._pairlistconfig.get("lookback_days", 0) or 0
+ lookback_period: int | None = self._pairlistconfig.get("lookback_period", None)
+ self._lookback_timeframe: str = self._pairlistconfig.get("lookback_timeframe", "1d")
+
+ has_period = (lookback_period or 0) > 0
+
+ if lookback_days > 0 and has_period:
+ raise OperationalException(
+ "Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
+ "config. Please set lookback_days only or lookback_period and lookback_timeframe "
+ "and restart the bot."
+ )
+ # 0 means "no lookback" - only acceptable if the lookback is optional,
+ # or if lookback_period provides the range instead.
+ min_days = 1 if required and not has_period else 0
+ if "lookback_days" in self._pairlistconfig and lookback_days < min_days:
+ raise OperationalException(f"{self.name} requires lookback_days to be >= {min_days}")
+
+ # lookback_days implies daily candles
+ if lookback_days > 0:
+ if self._lookback_timeframe != "1d":
+ raise OperationalException(
+ "Ambiguous configuration: lookback_days implies a lookback_timeframe of 1d, "
+ f"but lookback_timeframe is set to {self._lookback_timeframe}. Please set "
+ "lookback_period instead of lookback_days and restart the bot."
+ )
+ lookback_period = lookback_days
+
+ if lookback_period is None and required:
+ if "lookback_timeframe" in self._pairlistconfig:
+ raise OperationalException(
+ f"{self.name} requires lookback_period to be set when using lookback_timeframe."
+ )
+ if not deprecated_fallback:
+ raise OperationalException(
+ f"{self.name} requires either lookback_days or lookback_period to be set."
+ )
+ logger.warning(
+ f"DEPRECATED: Using {self.name} without lookback_days or lookback_period is "
+ "deprecated and will result in an error in a future version. "
+ "Please set either lookback_days or lookback_period and lookback_timeframe. "
+ f"Falling back to lookback_days: {deprecated_fallback}."
+ )
+ lookback_period = deprecated_fallback
+
+ self._lookback_period: int = lookback_period or 0
+
+ min_period = 1 if required else 0
+ if self._lookback_period < min_period:
+ raise OperationalException(
+ f"{self.name} requires lookback_period to be >= {min_period}"
+ )
+
+ if self._lookback_period == 0 and "lookback_timeframe" in self._pairlistconfig:
+ # Required handlers raise above - for optional ones, the timeframe has no effect.
+ logger.warning(
+ f"{self.name} is configured with lookback_timeframe "
+ f"{self._lookback_timeframe}, but without lookback_period - "
+ "the lookback range is disabled and the timeframe has no effect. "
+ "Please set lookback_period to enable it."
+ )
+
+ candle_limit = self._exchange.ohlcv_candle_limit(
+ self._lookback_timeframe, self._config["candle_type_def"]
+ )
+ if self._lookback_period > candle_limit:
+ raise OperationalException(
+ f"{self.name} requires lookback_period to not "
+ f"exceed exchange max request size ({candle_limit})"
+ )
+
@abstractmethod
def short_desc(self) -> str:
"""
diff --git a/freqtrade/plugins/pairlist/PercentChangePairList.py b/freqtrade/plugins/pairlist/PercentChangePairList.py
index 0f63db77f23..04f061a326a 100644
--- a/freqtrade/plugins/pairlist/PercentChangePairList.py
+++ b/freqtrade/plugins/pairlist/PercentChangePairList.py
@@ -7,17 +7,17 @@
"""
import logging
-from datetime import timedelta
from typing import TypedDict
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.exceptions import OperationalException
-from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
+from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange_types import Ticker, Tickers
+from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
-from freqtrade.util import FtTTLCache, dt_now, format_ms_time
+from freqtrade.util import FtTTLCache
logger = logging.getLogger(__name__)
@@ -47,23 +47,10 @@ def __init__(self, *args, **kwargs) -> None:
self._max_value = self._pairlistconfig.get("max_value", None)
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
- self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
- self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
- self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", "desc")
self._def_candletype = self._config["candle_type_def"]
- if (self._lookback_days > 0) and (self._lookback_period > 0):
- raise OperationalException(
- "Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
- "config. Please set lookback_days only or lookback_period and lookback_timeframe "
- "and restart the bot."
- )
-
- # overwrite lookback timeframe and days when lookback_days is set
- if self._lookback_days > 0:
- self._lookback_timeframe = "1d"
- self._lookback_period = self._lookback_days
+ self._init_lookback_config()
# get timeframe in minutes and seconds
self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe)
@@ -89,16 +76,6 @@ def __init__(self, *args, **kwargs) -> None:
"or switch to using candles and restart the bot."
)
- candle_limit = self._exchange.ohlcv_candle_limit(
- self._lookback_timeframe, self._def_candletype
- )
-
- if self._lookback_period > candle_limit:
- raise OperationalException(
- "ChangeFilter requires lookback_period to not "
- f"exceed exchange max request size ({candle_limit})"
- )
-
@property
def needstickers(self) -> bool:
"""
@@ -147,24 +124,7 @@ def available_parameters() -> dict[str, PairlistParameter]:
"help": "Sort Pairlist ascending or descending by rate of change.",
},
**IPairList.refresh_period_parameter(),
- "lookback_days": {
- "type": "number",
- "default": 0,
- "description": "Lookback Days",
- "help": "Number of days to look back at.",
- },
- "lookback_timeframe": {
- "type": "string",
- "default": "1d",
- "description": "Lookback Timeframe",
- "help": "Timeframe to use for lookback.",
- },
- "lookback_period": {
- "type": "number",
- "default": 0,
- "description": "Lookback Period",
- "help": "Number of periods to look back at.",
- },
+ **IPairList.lookback_parameters(),
}
def gen_pairlist(self, tickers: Tickers) -> list[str]:
@@ -247,37 +207,18 @@ def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]:
def fetch_candles_for_lookback_period(
self, filtered_tickers: list[SymbolWithPercentage]
) -> dict[PairWithTimeframe, DataFrame]:
- since_ms = (
- int(
- timeframe_to_prev_date(
- self._lookback_timeframe,
- dt_now()
- + timedelta(
- minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min
- ),
- ).timestamp()
- )
- * 1000
- )
- to_ms = (
- int(
- timeframe_to_prev_date(
- self._lookback_timeframe, dt_now() - timedelta(minutes=self._tf_in_min)
- ).timestamp()
- )
- * 1000
- )
self.log_once(
- f"Using change range of {self._lookback_period} candles, timeframe: "
- f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
- f"till {format_ms_time(to_ms)}",
+ f"Using change range of {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')}.",
logger.info,
)
needed_pairs: ListPairsWithTimeframes = [
(p, self._lookback_timeframe, self._def_candletype)
for p in [s["symbol"] for s in filtered_tickers]
]
- candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
+ candles = self._exchange.refresh_ohlcv_with_cache(
+ needed_pairs, lookback_period=self._lookback_period
+ )
return candles
def fetch_percent_change_from_lookback_period(
diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py
index 942e596f592..b2ee820f674 100644
--- a/freqtrade/plugins/pairlist/VolatilityFilter.py
+++ b/freqtrade/plugins/pairlist/VolatilityFilter.py
@@ -4,7 +4,6 @@
import logging
import sys
-from datetime import timedelta
import numpy as np
from pandas import DataFrame
@@ -14,7 +13,7 @@
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
-from freqtrade.util import FtTTLCache, dt_floor_day, dt_now, dt_ts
+from freqtrade.util import FtTTLCache
logger = logging.getLogger(__name__)
@@ -30,7 +29,6 @@ class VolatilityFilter(IPairList):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
- self._days = self._pairlistconfig.get("lookback_days", 10)
self._min_volatility = self._pairlistconfig.get("min_volatility", 0)
self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize)
self._refresh_period = self._pairlistconfig.get("refresh_period", 1440)
@@ -39,17 +37,11 @@ def __init__(self, *args, **kwargs) -> None:
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1000, ttl=self._refresh_period)
- candle_limit = self._exchange.ohlcv_candle_limit("1d", self._def_candletype)
- if self._days < 1:
- raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
- if self._days > candle_limit:
- raise OperationalException(
- "VolatilityFilter requires lookback_days to not "
- f"exceed exchange max request size ({candle_limit})"
- )
+ self._init_lookback_config(required=True, deprecated_fallback=10)
+
if self._sort_direction not in [None, "asc", "desc"]:
raise OperationalException(
- "VolatilityFilter requires sort_direction to be "
+ f"{self.name} requires sort_direction to be "
"either None (undefined), 'asc' or 'desc'"
)
@@ -59,8 +51,9 @@ def short_desc(self) -> str:
"""
return (
f"{self.name} - Filtering pairs with volatility range "
- f"{self._min_volatility}-{self._max_volatility} "
- f" the last {self._days} {plural(self._days, 'day')}."
+ f"{self._min_volatility}-{self._max_volatility} over the "
+ f"last {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')}."
)
@staticmethod
@@ -70,12 +63,6 @@ def description() -> str:
@staticmethod
def available_parameters() -> dict[str, PairlistParameter]:
return {
- "lookback_days": {
- "type": "number",
- "default": 10,
- "description": "Lookback Days",
- "help": "Number of days to look back at.",
- },
"min_volatility": {
"type": "number",
"default": 0,
@@ -96,6 +83,7 @@ def available_parameters() -> dict[str, PairlistParameter]:
"help": "Sort Pairlist ascending or descending by volatility.",
},
**IPairList.refresh_period_parameter(),
+ **IPairList.lookback_parameters(default_period=10),
}
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
@@ -106,18 +94,21 @@ def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
:return: new allowlist
"""
needed_pairs: ListPairsWithTimeframes = [
- (p, "1d", self._def_candletype) for p in pairlist if p not in self._pair_cache
+ (p, self._lookback_timeframe, self._def_candletype)
+ for p in pairlist
+ if p not in self._pair_cache
]
- since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days))
- candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
+ candles = self._exchange.refresh_ohlcv_with_cache(
+ needed_pairs, lookback_period=self._lookback_period
+ )
resulting_pairlist: list[str] = []
volatilitys: dict[str, float] = {}
for p in pairlist:
- daily_candles = candles.get((p, "1d", self._def_candletype), None)
+ pair_candles = candles.get((p, self._lookback_timeframe, self._def_candletype), None)
- volatility_avg = self._calculate_volatility(p, daily_candles)
+ volatility_avg = self._calculate_volatility(p, pair_candles)
if volatility_avg is not None:
if self._validate_pair_loc(p, volatility_avg):
@@ -136,16 +127,18 @@ def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
)
return resulting_pairlist
- def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> float | None:
+ def _calculate_volatility(self, pair: str, pair_candles: DataFrame) -> float | None:
# Check symbol in cache
if (volatility_avg := self._pair_cache.get(pair, None)) is not None:
return volatility_avg
- if daily_candles is not None and not daily_candles.empty:
- returns = np.log(daily_candles["close"].shift(1) / daily_candles["close"])
+ if pair_candles is not None and not pair_candles.empty:
+ returns = np.log(pair_candles["close"].shift(1) / pair_candles["close"])
returns.fillna(0, inplace=True)
- volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days)
+ volatility_series = returns.rolling(window=self._lookback_period).std() * np.sqrt(
+ self._lookback_period
+ )
volatility_avg = volatility_series.mean()
self._pair_cache[pair] = volatility_avg
@@ -166,7 +159,8 @@ def _validate_pair_loc(self, pair: str, volatility_avg: float) -> bool:
else:
self.log_once(
f"Removed {pair} from whitelist, because volatility "
- f"over {self._days} {plural(self._days, 'day')} "
+ f"over {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')} "
f"is: {volatility_avg:.3f} "
f"which is not in the configured range of "
f"{self._min_volatility}-{self._max_volatility}.",
diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py
index 72b8dbec3e8..132dafe777e 100644
--- a/freqtrade/plugins/pairlist/VolumePairList.py
+++ b/freqtrade/plugins/pairlist/VolumePairList.py
@@ -5,15 +5,15 @@
"""
import logging
-from datetime import timedelta
from typing import Any, Literal
from freqtrade.constants import DOCS_LINK, ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
-from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
+from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange_types import Tickers
+from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
-from freqtrade.util import FtTTLCache, dt_now, format_ms_time
+from freqtrade.util import FtTTLCache
logger = logging.getLogger(__name__)
@@ -42,22 +42,9 @@ def __init__(self, *args, **kwargs) -> None:
self._max_value: float | None = self._pairlistconfig.get("max_value", None)
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
- self._lookback_days: int = self._pairlistconfig.get("lookback_days", 0)
- self._lookback_timeframe: str = self._pairlistconfig.get("lookback_timeframe", "1d")
- self._lookback_period: int = self._pairlistconfig.get("lookback_period", 0)
self._def_candletype = self._config["candle_type_def"]
- if (self._lookback_days > 0) and (self._lookback_period > 0):
- raise OperationalException(
- "Ambiguous configuration: lookback_days and lookback_period both set in pairlist "
- "config. Please set lookback_days only or lookback_period and lookback_timeframe "
- "and restart the bot."
- )
-
- # overwrite lookback timeframe and days when lookback_days is set
- if self._lookback_days > 0:
- self._lookback_timeframe = "1d"
- self._lookback_period = self._lookback_days
+ self._init_lookback_config()
# get timeframe in minutes and seconds
self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe)
@@ -88,17 +75,6 @@ def __init__(self, *args, **kwargs) -> None:
if not self._validate_keys(self._sort_key):
raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
- candle_limit = self._exchange.ohlcv_candle_limit(
- self._lookback_timeframe, self._def_candletype
- )
- if self._lookback_period < 0:
- raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
- if self._lookback_period > candle_limit:
- raise OperationalException(
- "VolumeFilter requires lookback_period to not "
- f"exceed exchange max request size ({candle_limit})"
- )
-
@property
def needstickers(self) -> bool:
"""
@@ -150,24 +126,7 @@ def available_parameters() -> dict[str, PairlistParameter]:
"help": "Maximum value to use for filtering the pairlist.",
},
**IPairList.refresh_period_parameter(),
- "lookback_days": {
- "type": "number",
- "default": 0,
- "description": "Lookback Days",
- "help": "Number of days to look back at.",
- },
- "lookback_timeframe": {
- "type": "string",
- "default": "",
- "description": "Lookback Timeframe",
- "help": "Timeframe to use for lookback.",
- },
- "lookback_period": {
- "type": "number",
- "default": 0,
- "description": "Lookback Period",
- "help": "Number of periods to look back at.",
- },
+ **IPairList.lookback_parameters(),
}
def gen_pairlist(self, tickers: Tickers) -> list[str]:
@@ -224,33 +183,9 @@ def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]:
# Create bare minimum from tickers structure.
filtered_tickers: list[dict[str, Any]] = [{"symbol": k} for k in pairlist]
- # get lookback period in ms, for exchange ohlcv fetch
- since_ms = (
- int(
- timeframe_to_prev_date(
- self._lookback_timeframe,
- dt_now()
- + timedelta(
- minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min
- ),
- ).timestamp()
- )
- * 1000
- )
-
- to_ms = (
- int(
- timeframe_to_prev_date(
- self._lookback_timeframe, dt_now() - timedelta(minutes=self._tf_in_min)
- ).timestamp()
- )
- * 1000
- )
-
self.log_once(
- f"Using volume range of {self._lookback_period} candles, timeframe: "
- f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
- f"till {format_ms_time(to_ms)}",
+ f"Using volume range of {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')}.",
logger.info,
)
needed_pairs: ListPairsWithTimeframes = [
@@ -258,7 +193,9 @@ def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]:
for p in [s["symbol"] for s in filtered_tickers]
]
- candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
+ candles = self._exchange.refresh_ohlcv_with_cache(
+ needed_pairs, lookback_period=self._lookback_period
+ )
for i, p in enumerate(filtered_tickers):
contract_size = self._exchange.markets[p["symbol"]].get("contractSize", 1.0) or 1.0
diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py
index 40ba0fc1e1e..c39812aad3e 100644
--- a/freqtrade/plugins/pairlist/rangestabilityfilter.py
+++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py
@@ -3,7 +3,6 @@
"""
import logging
-from datetime import timedelta
from pandas import DataFrame
@@ -12,7 +11,7 @@
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
-from freqtrade.util import FtTTLCache, dt_floor_day, dt_now, dt_ts
+from freqtrade.util import FtTTLCache
logger = logging.getLogger(__name__)
@@ -24,7 +23,6 @@ class RangeStabilityFilter(IPairList):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
- self._days = self._pairlistconfig.get("lookback_days", 10)
self._min_rate_of_change = self._pairlistconfig.get("min_rate_of_change", 0.01)
self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change")
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
@@ -33,17 +31,11 @@ def __init__(self, *args, **kwargs) -> None:
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1000, ttl=self._refresh_period)
- candle_limit = self._exchange.ohlcv_candle_limit("1d", self._def_candletype)
- if self._days < 1:
- raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
- if self._days > candle_limit:
- raise OperationalException(
- "RangeStabilityFilter requires lookback_days to not "
- f"exceed exchange max request size ({candle_limit})"
- )
+ self._init_lookback_config(required=True, deprecated_fallback=10)
+
if self._sort_direction not in [None, "asc", "desc"]:
raise OperationalException(
- "RangeStabilityFilter requires sort_direction to be "
+ f"{self.name} requires sort_direction to be "
"either None (undefined), 'asc' or 'desc'"
)
@@ -57,7 +49,8 @@ def short_desc(self) -> str:
return (
f"{self.name} - Filtering pairs with rate of change below "
f"{self._min_rate_of_change}{max_rate_desc} over the "
- f"last {plural(self._days, 'day')}."
+ f"last {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')}."
)
@staticmethod
@@ -67,12 +60,6 @@ def description() -> str:
@staticmethod
def available_parameters() -> dict[str, PairlistParameter]:
return {
- "lookback_days": {
- "type": "number",
- "default": 10,
- "description": "Lookback Days",
- "help": "Number of days to look back at.",
- },
"min_rate_of_change": {
"type": "number",
"default": 0.01,
@@ -93,6 +80,7 @@ def available_parameters() -> dict[str, PairlistParameter]:
"help": "Sort Pairlist ascending or descending by rate of change.",
},
**IPairList.refresh_period_parameter(),
+ **IPairList.lookback_parameters(default_period=10),
}
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
@@ -103,19 +91,22 @@ def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
:return: new allowlist
"""
needed_pairs: ListPairsWithTimeframes = [
- (p, "1d", self._def_candletype) for p in pairlist if p not in self._pair_cache
+ (p, self._lookback_timeframe, self._def_candletype)
+ for p in pairlist
+ if p not in self._pair_cache
]
- since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1))
- candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
+ candles = self._exchange.refresh_ohlcv_with_cache(
+ needed_pairs, lookback_period=self._lookback_period
+ )
resulting_pairlist: list[str] = []
pct_changes: dict[str, float] = {}
for p in pairlist:
- daily_candles = candles.get((p, "1d", self._def_candletype), None)
+ pair_candles = candles.get((p, self._lookback_timeframe, self._def_candletype), None)
- pct_change = self._calculate_rate_of_change(p, daily_candles)
+ pct_change = self._calculate_rate_of_change(p, pair_candles)
if pct_change is not None:
if self._validate_pair_loc(p, pct_change):
@@ -132,13 +123,13 @@ def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
)
return resulting_pairlist
- def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> float | None:
+ def _calculate_rate_of_change(self, pair: str, pair_candles: DataFrame) -> float | None:
# Check symbol in cache
if (pct_change := self._pair_cache.get(pair, None)) is not None:
return pct_change
- if daily_candles is not None and not daily_candles.empty:
- highest_high = daily_candles["high"].max()
- lowest_low = daily_candles["low"].min()
+ if pair_candles is not None and not pair_candles.empty:
+ highest_high = pair_candles["high"].max()
+ lowest_low = pair_candles["low"].min()
pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0
self._pair_cache[pair] = pct_change
return pct_change
@@ -157,7 +148,8 @@ def _validate_pair_loc(self, pair: str, pct_change: float) -> bool:
if pct_change < self._min_rate_of_change:
self.log_once(
f"Removed {pair} from whitelist, because rate of change "
- f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
+ f"over {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')} is {pct_change:.3f}, "
f"which is below the threshold of {self._min_rate_of_change}.",
logger.info,
)
@@ -166,7 +158,8 @@ def _validate_pair_loc(self, pair: str, pct_change: float) -> bool:
if pct_change > self._max_rate_of_change:
self.log_once(
f"Removed {pair} from whitelist, because rate of change "
- f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
+ f"over {self._lookback_period} x {self._lookback_timeframe} "
+ f"{plural(self._lookback_period, 'candle')} is {pct_change:.3f}, "
f"which is above the threshold of {self._max_rate_of_change}.",
logger.info,
)
diff --git a/freqtrade/templates/subtemplates/exchange_binance.j2 b/freqtrade/templates/subtemplates/exchange_binance.j2
index dc2272119b2..4b60709b171 100644
--- a/freqtrade/templates/subtemplates/exchange_binance.j2
+++ b/freqtrade/templates/subtemplates/exchange_binance.j2
@@ -1,6 +1,6 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_bittrex.j2 b/freqtrade/templates/subtemplates/exchange_bittrex.j2
index 118e8104e48..23f93ff93bc 100644
--- a/freqtrade/templates/subtemplates/exchange_bittrex.j2
+++ b/freqtrade/templates/subtemplates/exchange_bittrex.j2
@@ -7,7 +7,7 @@
},
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_gateio.j2 b/freqtrade/templates/subtemplates/exchange_gateio.j2
index 63dff052fbe..fcc702586dd 100644
--- a/freqtrade/templates/subtemplates/exchange_gateio.j2
+++ b/freqtrade/templates/subtemplates/exchange_gateio.j2
@@ -1,6 +1,6 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"unknown_fee_rate": 1,
"ccxt_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_generic.j2 b/freqtrade/templates/subtemplates/exchange_generic.j2
index 01f63763887..bfb75d074c4 100644
--- a/freqtrade/templates/subtemplates/exchange_generic.j2
+++ b/freqtrade/templates/subtemplates/exchange_generic.j2
@@ -1,6 +1,6 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_huobi.j2 b/freqtrade/templates/subtemplates/exchange_huobi.j2
index 3cb521785e5..d6bdadd4952 100644
--- a/freqtrade/templates/subtemplates/exchange_huobi.j2
+++ b/freqtrade/templates/subtemplates/exchange_huobi.j2
@@ -1,6 +1,6 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_kraken.j2 b/freqtrade/templates/subtemplates/exchange_kraken.j2
index 7dd646720b7..08ba9aab45e 100644
--- a/freqtrade/templates/subtemplates/exchange_kraken.j2
+++ b/freqtrade/templates/subtemplates/exchange_kraken.j2
@@ -1,7 +1,7 @@
"download_trades": true,
"exchange": {
"name": "kraken",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/freqtrade/templates/subtemplates/exchange_kucoin.j2 b/freqtrade/templates/subtemplates/exchange_kucoin.j2
index b797dda41a2..dc109848e0e 100644
--- a/freqtrade/templates/subtemplates/exchange_kucoin.j2
+++ b/freqtrade/templates/subtemplates/exchange_kucoin.j2
@@ -1,8 +1,8 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
- "password": "{{ exchange_key_password }}",
+ "password": "{{ exchange_api_key_password }}",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
diff --git a/freqtrade/templates/subtemplates/exchange_okex.j2 b/freqtrade/templates/subtemplates/exchange_okex.j2
index b797dda41a2..dc109848e0e 100644
--- a/freqtrade/templates/subtemplates/exchange_okex.j2
+++ b/freqtrade/templates/subtemplates/exchange_okex.j2
@@ -1,8 +1,8 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
- "key": "{{ exchange_key }}",
+ "api_key": "{{ exchange_api_key }}",
"secret": "{{ exchange_secret }}",
- "password": "{{ exchange_key_password }}",
+ "password": "{{ exchange_api_key_password }}",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py
index 9d4a57db172..c175dbb0f28 100644
--- a/tests/commands/test_build_config.py
+++ b/tests/commands/test_build_config.py
@@ -51,7 +51,7 @@ def test_start_new_config(mocker, caplog, exchange):
"trading_mode": "spot",
"margin_mode": "",
"exchange_name": exchange,
- "exchange_key": "sampleKey",
+ "exchange_api_key": "sampleKey",
"exchange_secret": "Samplesecret",
"telegram": False,
"telegram_token": "asdf1244",
@@ -75,6 +75,7 @@ def test_start_new_config(mocker, caplog, exchange):
parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS,
)
assert result["exchange"]["name"] == exchange
+ assert result["exchange"]["api_key"] == "sampleKey"
assert result["timeframe"] == "15m"
diff --git a/tests/conftest.py b/tests/conftest.py
index 7bf1dc5a6af..62bd9040116 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -639,7 +639,7 @@ def get_default_conf(testdatadir):
},
"exchange": {
"name": "binance",
- "key": "key",
+ "api_key": "key",
"enable_ws": False,
"secret": "secret",
"pair_whitelist": ["ETH/BTC", "LTC/BTC", "XRP/BTC", "NEO/BTC"],
@@ -685,7 +685,7 @@ def get_default_conf_usdt(testdatadir):
"exchange": {
"name": "binance",
"enabled": True,
- "key": "key",
+ "api_key": "key",
"enable_ws": False,
"secret": "secret",
"pair_whitelist": [
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 5685655fe52..c005e4a98d5 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -31,6 +31,7 @@
Kraken,
date_minus_candles,
market_is_active,
+ timeframe_to_msecs,
timeframe_to_prev_date,
)
from freqtrade.exchange.common import (
@@ -3019,23 +3020,31 @@ def ohlcv_side_effect(requested_pairs, *args, **kwargs):
assert len(exchange._expiring_candle_cache) == 0
- res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
- assert ohlcv_mock.call_count == 1
- assert ohlcv_mock.call_args_list[0][0][0] == pairs
- assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=5)
+ # One download call per timeframe
+ assert ohlcv_mock.call_count == 3
+ requested = [p for call in ohlcv_mock.call_args_list for p in call[0][0]]
+ assert set(requested) == set(pairs)
+ assert len(requested) == 5
+ for call in ohlcv_mock.call_args_list:
+ timeframe = call[0][0][0][1]
+ expected_since = dt_ts(timeframe_to_prev_date(timeframe, start)) - 6 * timeframe_to_msecs(
+ timeframe
+ )
+ assert call[1]["since_ms"] == expected_since
assert len(res) == 5
# length of 3 - as we have 3 different timeframes
assert len(exchange._expiring_candle_cache) == 3
ohlcv_mock.reset_mock()
- res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=5)
assert ohlcv_mock.call_count == 0
assert len(res) == 5
# # re-run with one additional pair
res = exchange.refresh_ohlcv_with_cache(
- pairs + [("NEW/PAIR", "1d", CandleType.SPOT)], start.timestamp()
+ pairs + [("NEW/PAIR", "1d", CandleType.SPOT)], lookback_period=5
)
assert ohlcv_mock.call_count == 1
assert len(res) == 6
@@ -3044,7 +3053,7 @@ def ohlcv_side_effect(requested_pairs, *args, **kwargs):
time_machine.move_to(start + timedelta(minutes=6), tick=False)
ohlcv_mock.reset_mock()
- res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=5)
assert ohlcv_mock.call_count == 1
assert len(ohlcv_mock.call_args_list[0][0][0]) == 1
assert len(res) == 5
@@ -3053,21 +3062,32 @@ def ohlcv_side_effect(requested_pairs, *args, **kwargs):
time_machine.move_to(start + timedelta(hours=2), tick=False)
ohlcv_mock.reset_mock()
- res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
- assert ohlcv_mock.call_count == 1
- assert len(ohlcv_mock.call_args_list[0][0][0]) == 2
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=5)
+ assert ohlcv_mock.call_count == 2
+ requested = [p for call in ohlcv_mock.call_args_list for p in call[0][0]]
+ assert len(requested) == 2
assert len(res) == 5
# Expire all caches
time_machine.move_to(start + timedelta(days=1, hours=2), tick=False)
ohlcv_mock.reset_mock()
- res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
- assert ohlcv_mock.call_count == 1
- assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
- assert ohlcv_mock.call_args_list[0][0][0] == pairs
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=5)
+ assert ohlcv_mock.call_count == 3
+ requested = [p for call in ohlcv_mock.call_args_list for p in call[0][0]]
+ assert set(requested) == set(pairs)
assert len(res) == 5
+ # Cache keys must remain stable over time
+ assert len(exchange._expiring_candle_cache) == 3
+ # Expired entries are evicted when the cache is written to again -
+ # NEW/PAIR was never re-requested, so only the 3 base pairs remain.
+ assert exchange._expiring_candle_cache[("1d", 5)].currsize == 3
+
+ # A different lookback period uses separate caches
+ res = exchange.refresh_ohlcv_with_cache(pairs, lookback_period=6)
+ assert len(exchange._expiring_candle_cache) == 6
+
def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None:
ohlcv = generate_test_data_raw("1h", 24, "2025-01-02 12:00:00+00:00")
diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py
index 08ff88ac201..b25ca8b9fba 100644
--- a/tests/exchange_online/conftest.py
+++ b/tests/exchange_online/conftest.py
@@ -773,7 +773,7 @@ class TestExchangeOnlineSetup(TypedDict):
def exchange_conf():
config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve())
config["exchange"]["pair_whitelist"] = []
- config["exchange"]["apiKey"] = None
+ config["exchange"]["api_key"] = None
config["exchange"]["secret"] = None
config["dry_run"] = False
config["entry_pricing"]["use_order_book"] = True
diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py
index 0176c33ac59..7b931f471a4 100644
--- a/tests/freqtradebot/test_freqtradebot.py
+++ b/tests/freqtradebot/test_freqtradebot.py
@@ -188,15 +188,15 @@ def test_load_strategy_no_keys(default_conf_usdt, mocker, runmode, caplog) -> No
strategy_config = freqtrade.strategy.config
assert id(strategy_config["exchange"]) == id(conf["exchange"])
# Keys have been removed and are not passed to the exchange
- assert strategy_config["exchange"]["key"] is None
+ assert strategy_config["exchange"]["api_key"] is None
assert strategy_config["exchange"]["secret"] is None
assert erm.call_count == 1
ex_conf = erm.call_args_list[0][1]["exchange_config"]
assert id(ex_conf) != id(conf["exchange"])
# Keys are still present
- assert ex_conf["key"] is not None
- assert ex_conf["key"] == default_conf_usdt["exchange"]["key"]
+ assert ex_conf["api_key"] is not None
+ assert ex_conf["api_key"] == default_conf_usdt["exchange"]["api_key"]
assert ex_conf["secret"] is not None
assert ex_conf["secret"] == default_conf_usdt["exchange"]["secret"]
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index 3126a955055..c76a7362863 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -1079,13 +1079,13 @@ def test_VolumePairList_range(
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
elif volumefilter_result == "lookback_period_negative":
with pytest.raises(
- OperationalException, match=r"VolumeFilter requires lookback_period to be >= 0"
+ OperationalException, match=r"VolumePairList requires lookback_period to be >= 0"
):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
elif volumefilter_result == "lookback_exceeds_exchange_request_size":
with pytest.raises(
OperationalException,
- match=r"VolumeFilter requires lookback_period to not exceed "
+ match=r"VolumePairList requires lookback_period to not exceed "
r"exchange max request size \([0-9]+\)",
):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
@@ -1150,16 +1150,36 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
assert log_has("PerformanceFilter is not available in this mode.", caplog)
-def test_VolatilityFilter_error(mocker, whitelist_conf) -> None:
- volatility_filter = {"method": "VolatilityFilter", "lookback_days": -1}
+def test_VolatilityFilter_error(mocker, whitelist_conf, caplog) -> None:
+ volatility_filter = {"method": "VolatilityFilter"}
whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
exchange_mock = MagicMock()
exchange_mock.ohlcv_candle_limit = MagicMock(return_value=1000)
+ # Missing lookback configuration is deprecated
+ PairListManager(exchange_mock, whitelist_conf, MagicMock())
+ assert log_has_re(
+ r"DEPRECATED: Using VolatilityFilter without lookback_days or lookback_period.*", caplog
+ )
+
+ # The fallback does not apply once a lookback_timeframe is given
+ volatility_filter = {"method": "VolatilityFilter", "lookback_timeframe": "1h"}
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"VolatilityFilter requires lookback_period to be set when using lookback_timeframe",
+ ):
+ PairListManager(exchange_mock, whitelist_conf, MagicMock())
+
+ volatility_filter = {"method": "VolatilityFilter", "lookback_days": -1}
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+
with pytest.raises(
- OperationalException, match=r"VolatilityFilter requires lookback_days to be >= 1*"
+ OperationalException,
+ match=r"VolatilityFilter requires lookback_days to be >= 1",
):
PairListManager(exchange_mock, whitelist_conf, MagicMock())
@@ -1167,11 +1187,49 @@ def test_VolatilityFilter_error(mocker, whitelist_conf) -> None:
whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
with pytest.raises(
OperationalException,
- match=r"VolatilityFilter requires lookback_days to not exceed exchange max",
+ match=r"VolatilityFilter requires lookback_period to not exceed exchange max",
+ ):
+ PairListManager(exchange_mock, whitelist_conf, MagicMock())
+
+ volatility_filter = {"method": "VolatilityFilter", "lookback_period": 2000}
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+ with pytest.raises(
+ OperationalException,
+ match=r"VolatilityFilter requires lookback_period to not exceed exchange max",
+ ):
+ PairListManager(exchange_mock, whitelist_conf, MagicMock())
+
+ volatility_filter = {"method": "VolatilityFilter", "lookback_period": -1}
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+ with pytest.raises(
+ OperationalException, match=r"VolatilityFilter requires lookback_period to be >= 1"
+ ):
+ PairListManager(exchange_mock, whitelist_conf, MagicMock())
+
+ volatility_filter = {"method": "VolatilityFilter", "lookback_days": 10, "lookback_period": 10}
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+ with pytest.raises(
+ OperationalException,
+ match=r"Ambiguous configuration: lookback_days and lookback_period both set in pairlist",
):
PairListManager(exchange_mock, whitelist_conf, MagicMock())
- volatility_filter = {"method": "VolatilityFilter", "sort_direction": "Random"}
+ # An explicit "lookback_days: 0" must not invalidate a configured lookback_period
+ volatility_filter = {
+ "method": "VolatilityFilter",
+ "lookback_days": 0,
+ "lookback_timeframe": "1h",
+ "lookback_period": 5,
+ }
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
+ handler = PairListManager(exchange_mock, whitelist_conf, MagicMock())._pairlist_handlers[1]
+ assert (handler._lookback_timeframe, handler._lookback_period) == ("1h", 5)
+
+ volatility_filter = {
+ "method": "VolatilityFilter",
+ "lookback_days": 10,
+ "sort_direction": "Random",
+ }
whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter]
with pytest.raises(
OperationalException,
@@ -1489,6 +1547,60 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, t
pairlist_handler._whitelist_for_active_markets(["ETH/BTC"])
+@pytest.mark.parametrize(
+ "pairlist_conf",
+ [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "PercentChangePairList", "number_assets": 10},
+ {"method": "VolatilityFilter"},
+ {"method": "RangeStabilityFilter"},
+ ],
+)
+def test_lookback_parameters_defaults(mocker, whitelist_conf, pairlist_conf):
+ # The lookback defaults must match what the handler resolves to without config
+ whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, pairlist_conf]
+
+ mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
+ exchange = get_patched_exchange(mocker, whitelist_conf)
+ handler = PairListManager(exchange, whitelist_conf, MagicMock())._pairlist_handlers[1]
+ params = handler.available_parameters()
+
+ assert handler._lookback_timeframe == params["lookback_timeframe"]["default"]
+ assert handler._lookback_period == params["lookback_period"]["default"]
+ assert params["lookback_days"]["default"] is None
+
+
+@pytest.mark.parametrize(
+ "lookback_conf",
+ [
+ {},
+ {"lookback_days": 0},
+ {"lookback_period": 0},
+ {"lookback_days": 0, "lookback_period": 0},
+ {"lookback_timeframe": "1d"},
+ ],
+)
+def test_volumepairlist_no_range(mocker, whitelist_conf, lookback_conf, caplog):
+ # An explicit lookback of 0 must not enable the lookback range
+ whitelist_conf["pairlists"][0].update(lookback_conf)
+
+ mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
+ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
+ pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
+ assert pairlist_handler._use_range is False
+ assert pairlist_handler.needstickers is True
+
+ # A lookback_timeframe without lookback_period has no effect - warn about it
+ warning = r"VolumePairList is configured with lookback_timeframe .* the lookback range"
+ assert log_has_re(warning, caplog) == ("lookback_timeframe" in lookback_conf)
+
+ whitelist_conf["pairlists"][0].update({"lookback_days": -1})
+ with pytest.raises(
+ OperationalException, match=r"VolumePairList requires lookback_days to be >= 0"
+ ):
+ get_patched_freqtradebot(mocker, whitelist_conf)
+
+
def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf):
whitelist_conf["pairlists"][0].update({"sort_key": "asdf"})
@@ -1646,10 +1758,10 @@ def test_OffsetFilter_error(mocker, whitelist_conf) -> None:
PairListManager(MagicMock, whitelist_conf)
-def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
+def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers, caplog):
default_conf["pairlists"] = [
{"method": "VolumePairList", "number_assets": 10},
- {"method": "RangeStabilityFilter", "lookback_days": 99999},
+ {"method": "RangeStabilityFilter"},
]
mocker.patch.multiple(
@@ -1659,9 +1771,34 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
get_tickers=tickers,
)
+ # Missing lookback configuration is deprecated
+ get_patched_freqtradebot(mocker, default_conf)
+ assert log_has_re(
+ r"DEPRECATED: Using RangeStabilityFilter without lookback_days or lookback_period.*",
+ caplog,
+ )
+
+ # The fallback does not apply once a lookback_timeframe is given
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "RangeStabilityFilter", "lookback_timeframe": "1h"},
+ ]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"RangeStabilityFilter requires lookback_period to be set when using "
+ r"lookback_timeframe",
+ ):
+ get_patched_freqtradebot(mocker, default_conf)
+
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "RangeStabilityFilter", "lookback_days": 99999},
+ ]
+
with pytest.raises(
OperationalException,
- match=r"RangeStabilityFilter requires lookback_days to not exceed "
+ match=r"RangeStabilityFilter requires lookback_period to not exceed "
r"exchange max request size \([0-9]+\)",
):
get_patched_freqtradebot(mocker, default_conf)
@@ -1676,6 +1813,41 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
):
get_patched_freqtradebot(mocker, default_conf)
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "RangeStabilityFilter", "lookback_timeframe": "1h", "lookback_period": 99999},
+ ]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"RangeStabilityFilter requires lookback_period to not exceed "
+ r"exchange max request size \([0-9]+\)",
+ ):
+ get_patched_freqtradebot(mocker, default_conf)
+
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "RangeStabilityFilter", "lookback_days": 10, "lookback_period": 10},
+ ]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"Ambiguous configuration: lookback_days and lookback_period both set in pairlist",
+ ):
+ get_patched_freqtradebot(mocker, default_conf)
+
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ {"method": "RangeStabilityFilter", "lookback_days": 10, "lookback_timeframe": "1h"},
+ ]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"Ambiguous configuration: lookback_days implies a lookback_timeframe of 1d, "
+ r"but lookback_timeframe is set to 1h\..*",
+ ):
+ get_patched_freqtradebot(mocker, default_conf)
+
default_conf["pairlists"] = [
{"method": "VolumePairList", "number_assets": 10},
{"method": "RangeStabilityFilter", "sort_direction": "something"},
@@ -1747,6 +1919,51 @@ def test_rangestabilityfilter_caching(
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count
+@pytest.mark.parametrize(
+ "pairlistconfig",
+ [
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_timeframe": "1h",
+ "lookback_period": 24,
+ "min_rate_of_change": 0,
+ },
+ {
+ "method": "VolatilityFilter",
+ "lookback_timeframe": "1h",
+ "lookback_period": 24,
+ "min_volatility": 0,
+ },
+ ],
+)
+def test_range_volatility_filter_lookback_timeframe(
+ mocker, markets, default_conf, tickers, pairlistconfig
+):
+ default_conf["pairlists"] = [
+ {"method": "VolumePairList", "number_assets": 10},
+ pairlistconfig,
+ ]
+ df = generate_test_data("1h", 30, "2022-01-13 12:00:00+00:00", random_seed=42)
+ ohlcv_data = {
+ (pair, "1h", CandleType.SPOT): df
+ for pair in ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC", "HOT/BTC", "BLK/BTC"]
+ }
+ refresh_mock = MagicMock(return_value=ohlcv_data)
+ mocker.patch.multiple(
+ EXMS,
+ markets=PropertyMock(return_value=markets),
+ exchange_has=MagicMock(return_value=True),
+ get_tickers=tickers,
+ refresh_latest_ohlcv=refresh_mock,
+ )
+ freqtrade = get_patched_freqtradebot(mocker, default_conf)
+ freqtrade.pairlists.refresh_pairlist()
+ assert len(freqtrade.pairlists.whitelist) == 5
+ assert refresh_mock.call_count == 1
+ # Candles must be requested in the configured lookback timeframe
+ assert all(p[1] == "1h" for p in refresh_mock.call_args_list[0][0][0])
+
+
def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplog):
default_conf["pairlists"] = [
{"method": "VolumePairList", "number_assets": 10},
@@ -1860,7 +2077,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
{"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01},
(
"[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate "
- "of change below 0.01 over the last days.'}]"
+ "of change below 0.01 over the last 10 x 1d candles.'}]"
),
None,
),
@@ -1873,7 +2090,20 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
},
(
"[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate "
- "of change below 0.01 and above 0.99 over the last days.'}]"
+ "of change below 0.01 and above 0.99 over the last 10 x 1d candles.'}]"
+ ),
+ None,
+ ),
+ (
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_timeframe": "1h",
+ "lookback_period": 72,
+ "min_rate_of_change": 0.01,
+ },
+ (
+ "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate "
+ "of change below 0.01 over the last 72 x 1h candles.'}]"
),
None,
),
diff --git a/tests/plugins/test_percentchangepairlist.py b/tests/plugins/test_percentchangepairlist.py
index bf362d4fbf6..352945ea902 100644
--- a/tests/plugins/test_percentchangepairlist.py
+++ b/tests/plugins/test_percentchangepairlist.py
@@ -93,6 +93,25 @@ def test_volume_change_pair_list_init_wrong_lookback_period(mocker, rpl_config):
):
get_patched_freqtradebot(mocker, rpl_config)
+ rpl_config["pairlists"] = [
+ {
+ "method": "PercentChangePairList",
+ "number_assets": 2,
+ "sort_key": "percentage",
+ "min_value": 0,
+ "refresh_period": 86400,
+ "lookback_days": 10,
+ "lookback_timeframe": "1h",
+ }
+ ]
+
+ with pytest.raises(
+ OperationalException,
+ match=r"Ambiguous configuration: lookback_days implies a lookback_timeframe "
+ r"of 1d, but lookback_timeframe is set to 1h\..*",
+ ):
+ get_patched_freqtradebot(mocker, rpl_config)
+
rpl_config["pairlists"] = [
{
"method": "PercentChangePairList",
@@ -106,7 +125,7 @@ def test_volume_change_pair_list_init_wrong_lookback_period(mocker, rpl_config):
with pytest.raises(
OperationalException,
- match=r"ChangeFilter requires lookback_period to not exceed"
+ match=r"PercentChangePairList requires lookback_period to not exceed"
r" exchange max request size \(\d+\)",
):
get_patched_freqtradebot(mocker, rpl_config)
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index dc8e1974dc9..59516f6cd27 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -169,7 +169,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
conf1 = deepcopy(default_conf)
conf2 = deepcopy(default_conf)
- del conf1["exchange"]["key"]
+ del conf1["exchange"]["api_key"]
del conf1["exchange"]["secret"]
del conf2["exchange"]["name"]
conf2["exchange"]["pair_whitelist"] += ["NANO/BTC"]
@@ -192,7 +192,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
exchange_conf = default_conf["exchange"]
assert validated_conf["exchange"]["name"] == exchange_conf["name"]
- assert validated_conf["exchange"]["key"] == exchange_conf["key"]
+ assert validated_conf["exchange"]["api_key"] == exchange_conf["api_key"]
assert validated_conf["exchange"]["secret"] == exchange_conf["secret"]
assert validated_conf["exchange"]["pair_whitelist"] != conf1["exchange"]["pair_whitelist"]
assert validated_conf["exchange"]["pair_whitelist"] == conf2["exchange"]["pair_whitelist"]
@@ -203,7 +203,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
def test_from_config(default_conf, mocker, caplog) -> None:
conf1 = deepcopy(default_conf)
conf2 = deepcopy(default_conf)
- del conf1["exchange"]["key"]
+ del conf1["exchange"]["api_key"]
del conf1["exchange"]["secret"]
del conf2["exchange"]["name"]
conf2["exchange"]["pair_whitelist"] += ["NANO/BTC"]
@@ -218,7 +218,7 @@ def test_from_config(default_conf, mocker, caplog) -> None:
exchange_conf = default_conf["exchange"]
assert validated_conf["exchange"]["name"] == exchange_conf["name"]
- assert validated_conf["exchange"]["key"] == exchange_conf["key"]
+ assert validated_conf["exchange"]["api_key"] == exchange_conf["api_key"]
assert validated_conf["exchange"]["secret"] == exchange_conf["secret"]
assert validated_conf["exchange"]["pair_whitelist"] != conf1["exchange"]["pair_whitelist"]
assert validated_conf["exchange"]["pair_whitelist"] == conf2["exchange"]["pair_whitelist"]
@@ -1116,31 +1116,28 @@ def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
@pytest.mark.parametrize(
- "keys",
+ "base,key,expected",
[
- ("exchange", "key", None),
("exchange", "secret", None),
("exchange", "password", None),
],
)
-def test_load_config_default_subkeys(all_conf, keys) -> None:
+def test_load_config_default_subkeys(all_conf, base, key, expected) -> None:
"""
Test for parameters with default values in sub-paths
so they can be omitted in the config and the default value
should is added to the config.
"""
- # Get first level key
- key = keys[0]
# get second level key
- subkey = keys[1]
+ subkey = key
- del all_conf[key][subkey]
+ del all_conf[base][subkey]
- assert subkey not in all_conf[key]
+ assert subkey not in all_conf[base]
validate_config_schema(all_conf)
- assert subkey in all_conf[key]
- assert all_conf[key][subkey] == keys[2]
+ assert subkey in all_conf[base]
+ assert all_conf[base][subkey] == expected
def test_pairlist_resolving():
@@ -1593,19 +1590,19 @@ def test_setup_freqai_backtesting(mocker, default_conf) -> None:
def test_sanitize_config(default_conf_usdt):
- assert default_conf_usdt["exchange"]["key"] != "REDACTED"
+ assert default_conf_usdt["exchange"]["api_key"] != "REDACTED"
res = sanitize_config(default_conf_usdt)
# Didn't modify original dict
- assert default_conf_usdt["exchange"]["key"] != "REDACTED"
+ assert default_conf_usdt["exchange"]["api_key"] != "REDACTED"
assert "accountId" not in default_conf_usdt["exchange"]
- assert res["exchange"]["key"] == "REDACTED"
+ assert res["exchange"]["api_key"] == "REDACTED"
assert res["exchange"]["secret"] == "REDACTED"
# Didn't add a non-existing key
assert "accountId" not in res["exchange"]
res = sanitize_config(default_conf_usdt, show_sensitive=True)
- assert res["exchange"]["key"] == default_conf_usdt["exchange"]["key"]
+ assert res["exchange"]["api_key"] == default_conf_usdt["exchange"]["api_key"]
assert res["exchange"]["secret"] == default_conf_usdt["exchange"]["secret"]
@@ -1613,11 +1610,11 @@ def test_remove_exchange_credentials(default_conf) -> None:
conf = deepcopy(default_conf)
remove_exchange_credentials(conf["exchange"], False)
- assert conf["exchange"]["key"] is not None
+ assert conf["exchange"]["api_key"] is not None
assert conf["exchange"]["secret"] is not None
remove_exchange_credentials(conf["exchange"], True)
- assert conf["exchange"]["key"] is None
+ assert conf["exchange"]["api_key"] is None
assert conf["exchange"]["secret"] is None
assert conf["exchange"].get("password") is None
assert conf["exchange"].get("uid") is None
diff --git a/tests/testdata/testconfigs/main_test_config.json b/tests/testdata/testconfigs/main_test_config.json
index d4ce5b176df..25d415fd2ad 100644
--- a/tests/testdata/testconfigs/main_test_config.json
+++ b/tests/testdata/testconfigs/main_test_config.json
@@ -30,7 +30,7 @@
},
"exchange": {
"name": "binance",
- "key": "your_exchange_key",
+ "api_key": "your_exchange_api_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
diff --git a/tests/testdata/testconfigs/test_base_config.json b/tests/testdata/testconfigs/test_base_config.json
index 40a114150f7..ad09c8a0287 100644
--- a/tests/testdata/testconfigs/test_base_config.json
+++ b/tests/testdata/testconfigs/test_base_config.json
@@ -3,7 +3,7 @@
"dry_run": false,
"exchange": {
"name": "",
- "key": "",
+ "api_key": "",
"secret": "",
"pair_whitelist": [],
"ccxt_async_config": {