mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 02:12:01 +00:00
Merge pull request #10454 from jainanuj94/feature/10348
Add percent change pairlist
This commit is contained in:
commit
d33c930f26
|
@ -562,6 +562,7 @@
|
|||
"enum": [
|
||||
"StaticPairList",
|
||||
"VolumePairList",
|
||||
"PercentChangePairList",
|
||||
"ProducerPairList",
|
||||
"RemotePairList",
|
||||
"MarketCapPairList",
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
||||
|
||||
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList` or `MarketCapPairList` as the starting Pairlist Handler.
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
|
||||
|
||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||
|
||||
|
@ -22,6 +22,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
|||
|
||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`PercentChangePairList`](#percent-change-pair-list)
|
||||
* [`ProducerPairList`](#producerpairlist)
|
||||
* [`RemotePairList`](#remotepairlist)
|
||||
* [`MarketCapPairList`](#marketcappairlist)
|
||||
|
@ -152,6 +153,89 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
|
|||
!!! Note
|
||||
`VolumePairList` does not support backtesting mode.
|
||||
|
||||
#### Percent Change Pair List
|
||||
|
||||
`PercentChangePairList` filters and sorts pairs based on the percentage change in their price over the last 24 hours or any defined timeframe as part of advanced options. This allows traders to focus on assets that have experienced significant price movements, either positive or negative.
|
||||
|
||||
**Configuration Options**
|
||||
|
||||
* `number_assets`: Specifies the number of top pairs to select based on the 24-hour percentage change.
|
||||
* `min_value`: Sets a minimum percentage change threshold. Pairs with a percentage change below this value will be filtered out.
|
||||
* `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_timeframe`: Timeframe to use for the lookback period.
|
||||
* `lookback_period`: Number of periods to look back at.
|
||||
|
||||
When PercentChangePairList is used after other Pairlist Handlers, it will operate on the outputs of those handlers. If it is the leading Pairlist Handler, it will select pairs from all available markets with the specified stake currency.
|
||||
|
||||
`PercentChangePairList` uses ticker data from the exchange, provided via the ccxt library:
|
||||
The percentage change is calculated as the change in price over the last 24 hours.
|
||||
|
||||
??? Note "Unsupported exchanges"
|
||||
On some exchanges (like HTX), regular PercentChangePairList does not work as the api does not natively provide 24h percent change in price. This can be worked around by using candle data to calculate the percentage change. To roughly simulate 24h percent change, you can use the following configuration. Please note that these pairlists will only refresh once per day.
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 20,
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 1
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
**Example Configuration to Read from Ticker**
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 15,
|
||||
"min_value": -10,
|
||||
"max_value": 50
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
|
||||
1. The top 15 pairs are selected based on the highest percentage change in price over the last 24 hours.
|
||||
2. Only pairs with a percentage change between -10% and 50% are considered.
|
||||
|
||||
**Example Configuration to Read from Candles**
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 15,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 3600,
|
||||
"lookback_timeframe": "1h",
|
||||
"lookback_period": 72
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
This example builds the percent change pairs based on a rolling period of 3 days of 1-hour candles by using `lookback_timeframe` for candle size and `lookback_period` which specifies the number of candles.
|
||||
|
||||
The percent change in price is calculated using the following formula, which expresses the percentage difference between the current candle's close price and the previous candle's close price, as defined by the specified timeframe and lookback period:
|
||||
|
||||
$$ Percent Change = (\frac{Current Close - Previous Close}{Previous Close}) * 100 $$
|
||||
|
||||
!!! Warning "Range look back and refresh period"
|
||||
When used in conjunction with `lookback_days` and `lookback_timeframe` the `refresh_period` can not be smaller than the candle size in seconds. As this will result in unnecessary requests to the exchanges API.
|
||||
|
||||
!!! Warning "Performance implications when using lookback range"
|
||||
If used in first position in combination with lookback, the computation of the range-based percent change can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `PercentChangePairList` to narrow the pairlist down for further percent-change calculation.
|
||||
|
||||
!!! Note "Backtesting"
|
||||
`PercentChangePairList` does not support backtesting mode.
|
||||
|
||||
#### ProducerPairList
|
||||
|
||||
With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer.
|
||||
|
|
|
@ -42,6 +42,7 @@ HYPEROPT_LOSS_BUILTIN = [
|
|||
AVAILABLE_PAIRLISTS = [
|
||||
"StaticPairList",
|
||||
"VolumePairList",
|
||||
"PercentChangePairList",
|
||||
"ProducerPairList",
|
||||
"RemotePairList",
|
||||
"MarketCapPairList",
|
||||
|
|
|
@ -128,6 +128,7 @@ class Exchange:
|
|||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||
"tickers_have_quoteVolume": True,
|
||||
"tickers_have_percentage": True,
|
||||
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
|
||||
"tickers_have_price": True,
|
||||
"trades_limit": 1000, # Limit for 1 call to fetch_trades
|
||||
|
|
|
@ -12,6 +12,7 @@ class Ticker(TypedDict):
|
|||
last: Optional[float]
|
||||
quoteVolume: Optional[float]
|
||||
baseVolume: Optional[float]
|
||||
percentage: Optional[float]
|
||||
# Several more - only listing required.
|
||||
|
||||
|
||||
|
|
329
freqtrade/plugins/pairlist/PercentChangePairList.py
Normal file
329
freqtrade/plugins/pairlist/PercentChangePairList.py
Normal file
|
@ -0,0 +1,329 @@
|
|||
"""
|
||||
Percent Change PairList provider
|
||||
|
||||
Provides dynamic pair list based on trade change
|
||||
sorted based on percentage change in price over a
|
||||
defined period or as coming from ticker
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from cachetools import TTLCache
|
||||
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.types import Ticker, Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util import dt_now, format_ms_time
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PercentChangePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "number_assets" not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
"`number_assets` not specified. Please check your configuration "
|
||||
'for "pairlist.config.number_assets"'
|
||||
)
|
||||
|
||||
self._stake_currency = self._config["stake_currency"]
|
||||
self._number_pairs = self._pairlistconfig["number_assets"]
|
||||
self._min_value = self._pairlistconfig.get("min_value", None)
|
||||
self._max_value = self._pairlistconfig.get("max_value", None)
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
|
||||
self._pair_cache: TTLCache = TTLCache(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: Optional[str] = self._pairlistconfig.get("sort_direction", "desc")
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
if (self._lookback_days > 0) & (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
|
||||
|
||||
# get timeframe in minutes and seconds
|
||||
self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe)
|
||||
_tf_in_sec = self._tf_in_min * 60
|
||||
|
||||
# whether to use range lookback or not
|
||||
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
|
||||
|
||||
if self._use_range & (self._refresh_period < _tf_in_sec):
|
||||
raise OperationalException(
|
||||
f"Refresh period of {self._refresh_period} seconds is smaller than one "
|
||||
f"timeframe of {self._lookback_timeframe}. Please adjust refresh_period "
|
||||
f"to at least {_tf_in_sec} and restart the bot."
|
||||
)
|
||||
|
||||
if not self._use_range and not (
|
||||
self._exchange.exchange_has("fetchTickers")
|
||||
and self._exchange.get_option("tickers_have_percentage")
|
||||
):
|
||||
raise OperationalException(
|
||||
"Exchange does not support dynamic whitelist in this configuration. "
|
||||
"Please edit your config and either remove PercentChangePairList, "
|
||||
"or switch to using candles. and restart the bot."
|
||||
)
|
||||
|
||||
candle_limit = self._exchange.ohlcv_candle_limit(
|
||||
self._lookback_timeframe, self._config["candle_type_def"]
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return not self._use_range
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} percent change pairs."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Provides dynamic pair list based on percentage change."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist",
|
||||
},
|
||||
"min_value": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Minimum value",
|
||||
"help": "Minimum value to use for filtering the pairlist.",
|
||||
},
|
||||
"max_value": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum value",
|
||||
"help": "Maximum value to use for filtering the pairlist.",
|
||||
},
|
||||
"sort_direction": {
|
||||
"type": "option",
|
||||
"default": "desc",
|
||||
"options": ["", "asc", "desc"],
|
||||
"description": "Sort pairlist",
|
||||
"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.",
|
||||
},
|
||||
}
|
||||
|
||||
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||
:return: List of pairs
|
||||
"""
|
||||
pairlist = self._pair_cache.get("pairlist")
|
||||
if pairlist:
|
||||
# Item found - no refresh necessary
|
||||
return pairlist.copy()
|
||||
else:
|
||||
# Use fresh pairlist
|
||||
# Check if pair quote currency equals to the stake currency.
|
||||
_pairlist = [
|
||||
k
|
||||
for k in self._exchange.get_markets(
|
||||
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
|
||||
).keys()
|
||||
]
|
||||
|
||||
# No point in testing for blacklisted pairs...
|
||||
_pairlist = self.verify_blacklist(_pairlist, logger.info)
|
||||
if not self._use_range:
|
||||
filtered_tickers = [
|
||||
v
|
||||
for k, v in tickers.items()
|
||||
if (
|
||||
self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and (self._use_range or v.get("percentage") is not None)
|
||||
and v["symbol"] in _pairlist
|
||||
)
|
||||
]
|
||||
pairlist = [s["symbol"] for s in filtered_tickers]
|
||||
else:
|
||||
pairlist = _pairlist
|
||||
|
||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||
self._pair_cache["pairlist"] = pairlist.copy()
|
||||
|
||||
return pairlist
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
Called on each bot iteration - please use internal caching if necessary
|
||||
:param pairlist: pairlist to filter or sort
|
||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||
:return: new whitelist
|
||||
"""
|
||||
filtered_tickers: List[Dict[str, Any]] = [{"symbol": k} for k in pairlist]
|
||||
if self._use_range:
|
||||
# calculating using lookback_period
|
||||
self.fetch_percent_change_from_lookback_period(filtered_tickers)
|
||||
else:
|
||||
# Fetching 24h change by default from supported exchange tickers
|
||||
self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
|
||||
|
||||
if self._min_value is not None:
|
||||
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
|
||||
if self._max_value is not None:
|
||||
filtered_tickers = [v for v in filtered_tickers if v["percentage"] < self._max_value]
|
||||
|
||||
sorted_tickers = sorted(
|
||||
filtered_tickers,
|
||||
reverse=self._sort_direction == "desc",
|
||||
key=lambda t: t["percentage"],
|
||||
)
|
||||
|
||||
# Validate whitelist to only have active market pairs
|
||||
pairs = self._whitelist_for_active_markets([s["symbol"] for s in sorted_tickers])
|
||||
pairs = self.verify_blacklist(pairs, logmethod=logger.info)
|
||||
# Limit pairlist to the requested number of pairs
|
||||
pairs = pairs[: self._number_pairs]
|
||||
|
||||
return pairs
|
||||
|
||||
def fetch_candles_for_lookback_period(
|
||||
self, filtered_tickers: List[Dict[str, str]]
|
||||
) -> 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
|
||||
)
|
||||
# todo: utc date output for starting date
|
||||
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)}",
|
||||
logger.info,
|
||||
)
|
||||
needed_pairs: ListPairsWithTimeframes = [
|
||||
(p, self._lookback_timeframe, self._def_candletype)
|
||||
for p in [s["symbol"] for s in filtered_tickers]
|
||||
if p not in self._pair_cache
|
||||
]
|
||||
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
|
||||
return candles
|
||||
|
||||
def fetch_percent_change_from_lookback_period(self, filtered_tickers: List[Dict[str, Any]]):
|
||||
# get lookback period in ms, for exchange ohlcv fetch
|
||||
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
|
||||
|
||||
for i, p in enumerate(filtered_tickers):
|
||||
pair_candles = (
|
||||
candles[(p["symbol"], self._lookback_timeframe, self._def_candletype)]
|
||||
if (p["symbol"], self._lookback_timeframe, self._def_candletype) in candles
|
||||
else None
|
||||
)
|
||||
|
||||
# in case of candle data calculate typical price and change for candle
|
||||
if pair_candles is not None and not pair_candles.empty:
|
||||
current_close = pair_candles["close"].iloc[-1]
|
||||
previous_close = pair_candles["close"].shift(self._lookback_period).iloc[-1]
|
||||
pct_change = (
|
||||
((current_close - previous_close) / previous_close) if previous_close > 0 else 0
|
||||
)
|
||||
|
||||
# replace change with a range change sum calculated above
|
||||
filtered_tickers[i]["percentage"] = pct_change
|
||||
else:
|
||||
filtered_tickers[i]["percentage"] = 0
|
||||
|
||||
def fetch_percent_change_from_tickers(self, filtered_tickers: List[Dict[str, Any]], tickers):
|
||||
for i, p in enumerate(filtered_tickers):
|
||||
# Filter out assets
|
||||
if not self._validate_pair(
|
||||
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
|
||||
):
|
||||
filtered_tickers.remove(p)
|
||||
else:
|
||||
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Check if one price-step (pip) is > than a certain barrier.
|
||||
:param pair: Pair that's currently validated
|
||||
:param ticker: ticker dict as returned from ccxt.fetch_ticker
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
"""
|
||||
if not ticker or "percentage" not in ticker or ticker["percentage"] is None:
|
||||
self.log_once(
|
||||
f"Removed {pair} from whitelist, because "
|
||||
"ticker['percentage'] is empty (Usually no trade in the last 24h).",
|
||||
logger.info,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
|
@ -2188,7 +2188,7 @@ def tickers():
|
|||
"first": None,
|
||||
"last": 530.21,
|
||||
"change": 0.558,
|
||||
"percentage": None,
|
||||
"percentage": 2.349,
|
||||
"average": None,
|
||||
"baseVolume": 72300.0659,
|
||||
"quoteVolume": 37670097.3022171,
|
||||
|
|
370
tests/plugins/test_percentchangepairlist.py
Normal file
370
tests/plugins/test_percentchangepairlist.py
Normal file
|
@ -0,0 +1,370 @@
|
|||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.plugins.pairlist.PercentChangePairList import PercentChangePairList
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import (
|
||||
EXMS,
|
||||
generate_test_data_raw,
|
||||
get_patched_exchange,
|
||||
get_patched_freqtradebot,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def rpl_config(default_conf):
|
||||
default_conf["stake_currency"] = "USDT"
|
||||
|
||||
default_conf["exchange"]["pair_whitelist"] = [
|
||||
"ETH/USDT",
|
||||
"XRP/USDT",
|
||||
]
|
||||
default_conf["exchange"]["pair_blacklist"] = ["BLK/USDT"]
|
||||
|
||||
return default_conf
|
||||
|
||||
|
||||
def test_volume_change_pair_list_init_exchange_support(mocker, rpl_config):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"Exchange does not support dynamic whitelist in this configuration. "
|
||||
r"Please edit your config and either remove PercentChangePairList, "
|
||||
r"or switch to using candles. and restart the bot.",
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_volume_change_pair_list_init_wrong_refresh_period(mocker, rpl_config):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800,
|
||||
"lookback_days": 4,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"Refresh period of 1800 seconds is smaller than one "
|
||||
r"timeframe of 1d. Please adjust refresh_period "
|
||||
r"to at least 86400 and restart the bot.",
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_volume_change_pair_list_init_wrong_lookback_period(mocker, rpl_config):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 3,
|
||||
"lookback_period": 3,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"Ambiguous configuration: lookback_days "
|
||||
r"and lookback_period both set in pairlist config. "
|
||||
r"Please set lookback_days only or lookback_period "
|
||||
r"and lookback_timeframe and restart the bot.",
|
||||
):
|
||||
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": 1001,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"ChangeFilter requires lookback_period to not exceed"
|
||||
r" exchange max request size \(1000\)",
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_volume_change_pair_list_init_wrong_config(mocker, rpl_config):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"`number_assets` not specified. Please check your configuration "
|
||||
r'for "pairlist.config.number_assets"',
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_gen_pairlist_with_valid_change_pair_list_config(mocker, rpl_config, tickers, time_machine):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 4,
|
||||
}
|
||||
]
|
||||
start = datetime(2024, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
|
||||
mock_ohlcv_data = {
|
||||
("ETH/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
ohlcv_to_dataframe(
|
||||
generate_test_data_raw("1d", 100, start.strftime("%Y-%m-%d"), random_seed=12),
|
||||
"1d",
|
||||
pair="ETH/USDT",
|
||||
fill_missing=True,
|
||||
)
|
||||
),
|
||||
("BTC/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
ohlcv_to_dataframe(
|
||||
generate_test_data_raw("1d", 100, start.strftime("%Y-%m-%d"), random_seed=13),
|
||||
"1d",
|
||||
pair="BTC/USDT",
|
||||
fill_missing=True,
|
||||
)
|
||||
),
|
||||
("XRP/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
ohlcv_to_dataframe(
|
||||
generate_test_data_raw("1d", 100, start.strftime("%Y-%m-%d"), random_seed=14),
|
||||
"1d",
|
||||
pair="XRP/USDT",
|
||||
fill_missing=True,
|
||||
)
|
||||
),
|
||||
("NEO/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
ohlcv_to_dataframe(
|
||||
generate_test_data_raw("1d", 100, start.strftime("%Y-%m-%d"), random_seed=15),
|
||||
"1d",
|
||||
pair="NEO/USDT",
|
||||
fill_missing=True,
|
||||
)
|
||||
),
|
||||
("TKN/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
# Make sure always have highest percentage
|
||||
{
|
||||
"timestamp": [
|
||||
"2024-07-01 00:00:00",
|
||||
"2024-07-01 01:00:00",
|
||||
"2024-07-01 02:00:00",
|
||||
"2024-07-01 03:00:00",
|
||||
"2024-07-01 04:00:00",
|
||||
"2024-07-01 05:00:00",
|
||||
],
|
||||
"open": [100, 102, 101, 103, 104, 105],
|
||||
"high": [102, 103, 102, 104, 105, 106],
|
||||
"low": [99, 101, 100, 102, 103, 104],
|
||||
"close": [101, 102, 103, 104, 105, 106],
|
||||
"volume": [1000, 1500, 2000, 2500, 3000, 3500],
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", MagicMock(return_value=mock_ohlcv_data))
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = PercentChangePairList(
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
|
||||
result = remote_pairlist.gen_pairlist(tickers)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result == ["NEO/USDT", "TKN/USDT"]
|
||||
|
||||
|
||||
def test_filter_pairlist_with_empty_ticker(mocker, rpl_config, tickers, time_machine):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"sort_direction": "asc",
|
||||
"lookback_days": 4,
|
||||
}
|
||||
]
|
||||
start = datetime(2024, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
|
||||
mock_ohlcv_data = {
|
||||
("ETH/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
{
|
||||
"timestamp": [
|
||||
"2024-07-01 00:00:00",
|
||||
"2024-07-01 01:00:00",
|
||||
"2024-07-01 02:00:00",
|
||||
"2024-07-01 03:00:00",
|
||||
"2024-07-01 04:00:00",
|
||||
"2024-07-01 05:00:00",
|
||||
],
|
||||
"open": [100, 102, 101, 103, 104, 105],
|
||||
"high": [102, 103, 102, 104, 105, 106],
|
||||
"low": [99, 101, 100, 102, 103, 104],
|
||||
"close": [101, 102, 103, 104, 105, 105],
|
||||
"volume": [1000, 1500, 2000, 2500, 3000, 3500],
|
||||
}
|
||||
),
|
||||
("XRP/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
{
|
||||
"timestamp": [
|
||||
"2024-07-01 00:00:00",
|
||||
"2024-07-01 01:00:00",
|
||||
"2024-07-01 02:00:00",
|
||||
"2024-07-01 03:00:00",
|
||||
"2024-07-01 04:00:00",
|
||||
"2024-07-01 05:00:00",
|
||||
],
|
||||
"open": [100, 102, 101, 103, 104, 105],
|
||||
"high": [102, 103, 102, 104, 105, 106],
|
||||
"low": [99, 101, 100, 102, 103, 104],
|
||||
"close": [101, 102, 103, 104, 105, 104],
|
||||
"volume": [1000, 1500, 2000, 2500, 3000, 3400],
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", MagicMock(return_value=mock_ohlcv_data))
|
||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = PercentChangePairList(
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
|
||||
result = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {})
|
||||
|
||||
assert len(result) == 2
|
||||
assert result == ["XRP/USDT", "ETH/USDT"]
|
||||
|
||||
|
||||
def test_filter_pairlist_with_max_value_set(mocker, rpl_config, tickers, time_machine):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"max_value": 15,
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 4,
|
||||
}
|
||||
]
|
||||
|
||||
start = datetime(2024, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
|
||||
mock_ohlcv_data = {
|
||||
("ETH/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
{
|
||||
"timestamp": [
|
||||
"2024-07-01 00:00:00",
|
||||
"2024-07-01 01:00:00",
|
||||
"2024-07-01 02:00:00",
|
||||
"2024-07-01 03:00:00",
|
||||
"2024-07-01 04:00:00",
|
||||
"2024-07-01 05:00:00",
|
||||
],
|
||||
"open": [100, 102, 101, 103, 104, 105],
|
||||
"high": [102, 103, 102, 104, 105, 106],
|
||||
"low": [99, 101, 100, 102, 103, 104],
|
||||
"close": [101, 102, 103, 104, 105, 106],
|
||||
"volume": [1000, 1500, 2000, 1800, 2400, 2500],
|
||||
}
|
||||
),
|
||||
("XRP/USDT", "1d", CandleType.SPOT): pd.DataFrame(
|
||||
{
|
||||
"timestamp": [
|
||||
"2024-07-01 00:00:00",
|
||||
"2024-07-01 01:00:00",
|
||||
"2024-07-01 02:00:00",
|
||||
"2024-07-01 03:00:00",
|
||||
"2024-07-01 04:00:00",
|
||||
"2024-07-01 05:00:00",
|
||||
],
|
||||
"open": [100, 102, 101, 103, 104, 105],
|
||||
"high": [102, 103, 102, 104, 105, 106],
|
||||
"low": [99, 101, 100, 102, 103, 104],
|
||||
"close": [101, 102, 103, 104, 105, 101],
|
||||
"volume": [1000, 1500, 2000, 2500, 3000, 3500],
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", MagicMock(return_value=mock_ohlcv_data))
|
||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = PercentChangePairList(
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
|
||||
result = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {})
|
||||
|
||||
assert len(result) == 1
|
||||
assert result == ["ETH/USDT"]
|
||||
|
||||
|
||||
def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers):
|
||||
rpl_config["pairlists"] = [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 2,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
}
|
||||
]
|
||||
|
||||
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = PercentChangePairList(
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
|
||||
result = remote_pairlist.gen_pairlist(tickers.return_value)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result == ["ETH/USDT"]
|
Loading…
Reference in New Issue
Block a user